--- title: "Hypergraphs" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Hypergraphs} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>", fig.width = 9, fig.height = 7, out.width = "100%") has_igraph <- requireNamespace("igraph", quietly = TRUE) join_and <- function(v) sub(", ([^,]*)$", " and \\1", toString(v)) n_steps <- length(unique(hypergraphs::tutoring_events$step)) ``` Higher-order network models extend pairwise networks in two directions. One represents interactions among groups of nodes with hypergraphs and simplicial complexes; the other represents memory in paths. This document covers hypergraphs. A hypergraph consists of a set of nodes and a set of hyperedges, and a hyperedge is a subset of the nodes of any size. An edge of a network is the special case of a hyperedge with two nodes. A hypergraph is represented by its incidence matrix, which has one row per node, one column per hyperedge, and an entry of one where the node belongs to the hyperedge. The projection of a hypergraph to a network replaces each hyperedge by the pairs of its nodes, and different hypergraphs can have the same projection. Measures computed from the incidence matrix keep the information that the projection loses. ## Data The examples use `tutoring_events`, a data frame in long format with one row per event, from `r format(n_steps, big.mark = ",")` problem steps that learners worked through with an AI tutor. A step begins with the task, the learner attempts an answer, and the answer is correct or incorrect. After an incorrect answer the tutor responds with guidance, a question, an order, a refutation, a prompt to reflect or comfort, and the learner reattempts or gives up. A step ends with `Completed` after a correct answer and with `Stopped` when it ends without one. A trial is the part of a step between two answers. ```{r data} library(hypergraphs) head(tutoring_events, 12) ``` ```{r data-values, include = FALSE} n_trials <- length(unique(tutoring_events$trial)) n_events <- length(unique(tutoring_events$event)) ``` The data contain `r format(n_trials, big.mark = ",")` trials and `r n_events` distinct events. ## Hypergraphs from observed groups When group membership is observed, each group is a hyperedge. In an event log, the events of a session form such a group, and a trial is a session in this sense. In `hypergraph()`, `action` names the column of the events and `session` the column of the trials, and the events of each trial become one hyperedge. ```{r trials} trial_hg <- hypergraph(tutoring_events, action = "event", session = "trial") trial_hg ``` ```{r trials-values, include = FALSE} trial_edges <- hg_get(trial_hg) size_counts <- table(trial_edges$size) modal_size <- names(size_counts)[which.max(size_counts)] n_sets <- length(unique(trial_edges$members)) ``` Most trials contain `r modal_size` events, and the largest contain `r max(trial_edges$size)`. Many trials contain the same events: the `r format(n_trials, big.mark = ",")` trials hold `r n_sets` distinct sets. A hypergraph whose hyperedges repeat is plotted as its distinct sets. ```{r trials-plot, fig.width = 10, fig.height = 9} plot(trial_hg) ``` Each set is plotted as a hull coloured by its number of trials, and each event as a circle whose area is the number of trials that contain it. Most of the sets are rare and overlap the frequent ones. The minimum support of frequent-set mining keeps the sets that occur in at least a given share of the trials. With `min_share = 0.01`, the sets of at least 1% of the trials are kept. ```{r common-trials} common_trials <- hypergraph(tutoring_events, action = "event", session = "trial", min_share = 0.01) common_trials ``` ```{r common-trials-values, include = FALSE} common_cover <- round(100 * sum(hg_get(common_trials, what = "sets")$share)) ``` ```{r common-trials-plot, fig.width = 10, fig.height = 9} plot(common_trials) ``` The `r common_trials$n_hyperedges` sets cover `r common_cover`% of the trials. The sets of a stopped step, with `Comfort`, `GiveUp` and `Stopped`, lie apart from the sets of the first trial and the reattempts. The degree of an event is the number of hyperedges that contain it. ```{r trial-nodes} trial_nodes <- hg_get(trial_hg, what = "nodes", sort_by = "degree") trial_nodes ``` ## Frequent sets Most trials fall into a few of the `r n_sets` sets. Counting the sets and keeping the most frequent ones as hyperedges summarises the data by its common combinations. With `top = 8`, the eight most frequent sets are kept. ```{r sets} trial_sets <- hypergraph(tutoring_events, action = "event", session = "trial", top = 8) trial_set_table <- hg_get(trial_sets, what = "sets") trial_set_table ``` ```{r sets-values, include = FALSE} top_share <- round(100 * sum(trial_set_table$share)) ``` The eight sets account for `r top_share`% of the trials. The most frequent set, `r trial_set_table$set[1]`, is a step completed on its first trial. The next are a first trial with an incorrect answer, a correct first trial that opens with `Begin`, and reattempts after guidance or a question, which are correct or incorrect. ```{r sets-plot, fig.width = 10, fig.height = 7} plot(trial_sets) ``` With `group`, the sets are counted separately within each value of a comparison variable. The `outcome` column records whether a step is completed with a correct answer or stopped without one. ```{r sets-by} outcome_sets <- hypergraph(tutoring_events, action = "event", session = "trial", group = "outcome", top = 6) outcome_set_table <- hg_get(outcome_sets, what = "sets") outcome_set_table ``` ```{r sets-completed-plot, fig.width = 9, fig.height = 7} plot(outcome_sets, group = "completed") ``` ```{r sets-stopped-plot, fig.width = 9, fig.height = 7} plot(outcome_sets, group = "stopped") ``` The frequent trials of completed steps are correct first trials, incorrect first trials and correct reattempts after guidance or a question. The frequent trials of stopped steps are incorrect first trials and incorrect reattempts after a question, guidance or an order. In the set `Comfort + GiveUp + Stopped` the tutor comforts the learner, the learner gives up and the step ends without a correct answer. ## Hypergraphs from sequences When only the order of events is recorded, hyperedges are formed from windows of consecutive events in the sequence of events of each step. Each window of `window` events is reduced to the set of its distinct events, and identical sets are merged into one hyperedge whose weight is the number of windows that produced it. The argument `action` names the column of the events, `actor` the column of the steps and `time` the column that orders the events. ```{r window} window_hg <- hypergraph(tutoring_events, action = "event", actor = "step", time = "position", window = 3) window_hg window_edges <- hg_get(window_hg, sort_by = "weight", top = 8) window_edges ``` ```{r window-plot, fig.width = 10, fig.height = 9} plot(window_hg) ``` ```{r window-values, include = FALSE} n_windows <- sum(hg_get(window_hg)$weight) ``` The `r format(n_windows, big.mark = ",")` windows give `r window_hg$n_hyperedges` distinct hyperedges. A window with a repeated event contains fewer distinct events than its length, so some hyperedges have two nodes or one. With `min_weight`, only hyperedges produced by at least that many windows are kept, and `hg_subset()` with `size = 3` keeps the windows of three distinct events and drops the events left in none. ```{r window-frequent} frequent_windows <- hypergraph(tutoring_events, action = "event", actor = "step", time = "position", window = 3, min_weight = 2000) frequent_windows <- hg_subset(frequent_windows, size = 3) frequent_windows ``` ```{r window-frequent-plot, fig.width = 8, fig.height = 7} plot(frequent_windows) ``` Each hull is a window of three events, coloured by the number of windows that contain exactly these events. Windows span the boundaries between trials, and the window length is set by the analyst. The remaining sections use the trials, which the data record. ## Projection to a network The projection of a hypergraph is the network in which two nodes are joined when they belong to a common hyperedge, with a weight equal to the number of such hyperedges. ```{r project} trial_network <- pairwise_network(trial_hg) trial_network_edges <- hg_get(trial_network, sort_by = "weight", top = 8) trial_network_edges ``` ```{r project-plot, fig.width = 7, fig.height = 7} plot(trial_network) ``` ```{r project-values-pre, include = FALSE} n_pairs <- nrow(hg_get(trial_network)) ``` Each edge joins two events with a width that grows with the number of trials that contain both. The widest edges join `Task`, `Attempt`, `Correct` and `Completed`, which occur together in most first trials, and the edges of `Comfort`, `GiveUp` and `Stopped` are thin. The network records how often two events occur in the same trial and leaves out which other events occur with them. A hypergraph can also be inferred from a network by taking every clique, a set of nodes that are all joined to each other, as a hyperedge. With `include_pairwise = FALSE` the cliques of three or more nodes are kept, and `max_size` is the size of the largest clique kept, here the size of the largest trial. ```{r project-back} clique_hg <- hypergraph(trial_network, max_size = 6, include_pairwise = FALSE) clique_hg ``` ```{r project-back-plot, fig.width = 9, fig.height = 8} plot(clique_hg) ``` ```{r project-values, include = FALSE} as_keys <- function(members) { vapply(strsplit(members, ", ", fixed = TRUE), \(m) paste(sort(m), collapse = " + "), character(1)) } observed_keys <- as_keys(unique(hg_get(trial_hg)$members)) observed_keys <- observed_keys[lengths(strsplit(observed_keys, " + ", fixed = TRUE)) >= 3] inferred_keys <- as_keys(hg_get(clique_hg)$members) n_true <- sum(inferred_keys %in% observed_keys) n_found <- sum(observed_keys %in% inferred_keys) ``` The network has `r n_pairs` pairs, and its cliques of three to `r max(hg_get(clique_hg)$size)` events give `r clique_hg$n_hyperedges` hyperedges. The trials contain `r length(observed_keys)` distinct sets of three or more events, and `r n_found` of them are among the cliques. The other `r clique_hg$n_hyperedges - n_true` cliques are sets whose pairs each occur in some trial while the whole set occurs in none. Identifying the observed groups among the cliques requires the hypergraph. The cliques cover the events so densely that their hulls hide one another in the plot. ## Node measures Node measures of a hypergraph are computed from its incidence matrix. The hyperdegree of a node is the number of hyperedges that contain it, its strength is the sum of the sizes of those hyperedges, and its number of neighbours is the number of other nodes that share a hyperedge with it. ```{r measures} node_measures <- hg_measures(trial_hg) node_measures trial_summary <- hg_measures(trial_hg, what = "summary") trial_summary ``` ```{r measures-values, include = FALSE} participation <- subset(trial_summary, measure == "pairwise_participation")$value attempt_row <- subset(node_measures, node == "Attempt") ``` `Attempt` belongs to the first trial of every step, `Task` to that trial and to some reattempts, and `Correct` and `Completed` to the trial that ends every completed step, so these events have the highest hyperdegree. Hyperdegree and the number of neighbours describe different aspects of position. `Attempt` belongs to `r format(attempt_row$hyperdegree, big.mark = ",")` trials and shares them with `r attempt_row$n_neighbors` other events, and `Task` shares a trial with every other event. The pairwise participation is the share of pairs of nodes that belong to at least one common hyperedge. It is `r sprintf("%.2f", participation)`, so `r round(100 * (1 - participation))`% of the pairs of events never occur in the same trial. ## Centrality An eigenvector centrality gives a node a score that depends on the scores of the nodes it shares hyperedges with. The clique centrality is the eigenvector centrality of the projection. The Z and H centralities score a node by the product of the scores of the other members of each of its hyperedges. In the H centrality the score of the node itself is raised to the power k - 1, where k is the size of the largest hyperedge, and the scores are more even. ```{r centrality} event_centrality <- hg_centrality(trial_hg, sort_by = "H") event_centrality ``` The clique centrality is highest for the events of a correct first trial, `Task`, `Attempt`, `Correct` and `Completed`, which occur together in most steps. The Z and H centralities rank `Incorrect` third. An incorrect first trial contains `Incorrect` with `Task` and `Attempt`, the two highest-scoring events, and the product of their scores raises the score of `Incorrect`. PageRank on a hypergraph is the stationary distribution of a random walk that moves from a node to a hyperedge that contains it and from the hyperedge to one of its members, with a fixed probability of restarting at a random node. ```{r pagerank} event_pagerank <- hg_pagerank(trial_hg, sort_by = "pagerank") event_pagerank ``` `Comfort`, `GiveUp` and `Stopped` occur mostly together in the last trial of a stopped step, so a walk that reaches one of them moves among the three for several moves before it leaves. They rank seventh to ninth by PageRank and close to zero by the clique and Z centralities. ## Communities and clusters A community is a set of nodes that share more hyperedges with each other than with the rest of the hypergraph. The communities of the events are found with the map equation from 20 random starts, and the partition reported is the medoid, the partition that agrees most with the other 19. ```{r communities, eval = has_igraph} event_communities <- hg_communities(trial_hg, n_runs = 20, seeds = 1:20) event_communities ``` Spectral clustering divides the nodes into `k` clusters by the eigenvectors of the hypergraph Laplacian. Its final k-means step starts from random centres, which `seed` fixes. ```{r cluster} event_clusters <- hg_cluster(trial_hg, k = 3, seed = 1) event_clusters ``` ```{r cluster-values, include = FALSE} members_of <- function(event) { label <- subset(event_clusters, node == event)$cluster join_and(subset(event_clusters, cluster == label)$node) } ``` The clusters are the end of a stopped step (`r members_of("Stopped")`), the events of a correct first trial (`r members_of("Completed")`), and the incorrect answers, the reattempts and the moves of the tutor (`r members_of("Guidance")`). The adjusted Rand index measures the agreement between two partitions of the same nodes, corrected for chance. ```{r agreement, eval = has_igraph} partition_agreement <- hg_agreement(event_communities, event_clusters) partition_agreement partition_table <- hg_agreement(event_communities, event_clusters, what = "table") partition_table ``` The map equation separates the end of a stopped step from the other events. Spectral clustering divides the other events into those of a correct first trial and those of the reattempts. The adjusted Rand index between the two partitions is `r sprintf("%.2f", partition_agreement$ari)`. ## Null models A structural pattern is assessed against random hypergraphs that keep the size of every hyperedge and the degree of every node. Such hypergraphs are generated by repeated swaps of members between pairs of hyperedges, and the observed value of a statistic is compared with its distribution over the random hypergraphs. The comparison is made on the trials of the stopped steps. ```{r null} stopped_events <- subset(tutoring_events, outcome == "stopped") stopped_hg <- hypergraph(stopped_events, action = "event", session = "trial") plot(stopped_hg) stopped_null <- hg_null_test(stopped_hg, n = 99, seed = 1, statistic = c("pairwise_participation", "avg_jaccard", "repeated_edges")) stopped_null ``` ```{r null-values, include = FALSE} null_row <- function(s) subset(stopped_null, statistic == s) pp <- null_row("pairwise_participation") re <- null_row("repeated_edges") aj <- null_row("avg_jaccard") ``` In the random hypergraphs `r sprintf("%.1f", 100 * pp$null_mean)`% of the pairs of events occur together in some trial, against `r round(100 * pp$observed)`% in the data. About `r format(round(re$null_mean), big.mark = ",")` trials repeat the set of an earlier trial, against `r format(re$observed, big.mark = ",")` in the data, and the mean Jaccard index between two trials is `r round(aj$null_mean, 2)`, against `r round(aj$observed, 2)`. The trials of stopped steps repeat a small number of sets, and each set keeps together events that the random hypergraphs spread over many trials. The p-value of `r pp$p_value` is the smallest attainable with 99 random hypergraphs. The observed pairwise participation lies `r round(abs(pp$z))` standard deviations below the mean of the random hypergraphs, and the number of repeated trials `r round(abs(re$z))` standard deviations above it. ## Other higher-order structures A corpus of documents forms a hypergraph in which each document is a hyperedge over its words, built with `text_hypergraph()`. Memory networks are estimated from paths with `hon()`, and simplicial complexes, in which every subset of a relation is also a relation, are built with `simplicial()`. The help page `?hypergraphs` lists the functions of each family.