XGI in 15 minutes#
Hello! If you are new to XGI you might want to check out the XGI in 1 minute or the XGI in 5 minutes tutorials for a quick introduction.
The starting point is always to import our Python library and other standard libraries, this is simply done using:
[24]:
import matplotlib.pyplot as plt
import xgi
Uploading a dataset#
In this tutorial we will construct a hypergraph describing real world data! With XGI we provide a companion data repository, xgi-data, with which you can easely load several datasets in standard format:
[25]:
H_enron = xgi.load_xgi_data("email-enron")
The ‘email-enron’ dataset, for example, has a corresponding datasheet explaining its characteristics. The nodes (individuals) in this dataset contain associated email addresses and the edges (emails) contain associated timestamps. These attributes can be accessed by simply typing H.nodes[id]
or H.edges[id]
respectively.
[26]:
print(f"The hypergraph has {H_enron.num_nodes} nodes and {H_enron.num_edges} edges")
The hypergraph has 148 nodes and 10885 edges
We can also print a summary of the hypergraph:
[27]:
print(H_enron)
Hypergraph named email-Enron with 148 nodes and 10885 hyperedges
The dataset is completely formatted. You can access nodes and edges or their attributes in a very simple way:
[28]:
print("The first 10 node IDs are:")
print(list(H_enron.nodes)[:10])
print("The first 10 edge IDs are:")
print(list(H_enron.edges)[:10])
print("The attributes of node '4' are")
print(H_enron.nodes["4"])
print("The attributes of edge '6' are")
print(H_enron.edges["6"])
The first 10 node IDs are:
['4', '1', '117', '129', '51', '41', '65', '107', '122', '29']
The first 10 edge IDs are:
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
The attributes of node '4' are
{'name': 'robert.badeer@enron.com'}
The attributes of edge '6' are
{'timestamp': '2000-02-22T08:07:00'}
It is also possible to access nodes of edges in particular variable types, for example we can create a dictionary containing the edges of our hypergraph and their members:
[29]:
edges_dictionary = H_enron.edges.members(dtype=dict)
print(list(edges_dictionary.items())[:5])
[('0', {'4', '1'}), ('1', {'117', '1', '129'}), ('2', {'1', '51'}), ('3', {'1', '51'}), ('4', {'41', '1'})]
Cleaning up a hypergraph dataset#
You can check if your hypergraph is connected using the function:
[30]:
xgi.is_connected(H_enron)
[30]:
False
We can count the number of isolated nodes and multi-edges in the following way:
[31]:
isolated_nodes = H_enron.nodes.isolates()
print("Number of isolated nodes: ", len(isolated_nodes))
duplicated_edges = H_enron.edges.duplicates()
print("Number of duplicated edges: ", len(duplicated_edges))
Number of isolated nodes: 5
Number of duplicated edges: 9371
We can clean up this dataset to remove isolated nodes and multi-edges, and replace all IDs with integer IDs using the cleanup
function:
[32]:
H_enron_cleaned = H_enron.cleanup(
multiedges=False, singletons=False, isolates=False, relabel=True, in_place=False
)
print(H_enron_cleaned)
Hypergraph named email-Enron with 143 nodes and 1459 hyperedges
We can see that 5 isolated nodes were removed and 9371 duplicated edges were removed. We can check it:
[33]:
len(H_enron.nodes) == len(H_enron_cleaned.nodes) + len(isolated_nodes)
[33]:
True
[34]:
len(H_enron.edges) == len(H_enron_cleaned.edges) + len(duplicated_edges)
[34]:
False
We can check that the hypergraph is now connected:
[35]:
xgi.is_connected(H_enron_cleaned)
[35]:
True
Drawing#
Visualization is crucial for understanding complex data structures. You can use the default drawing function:
[36]:
xgi.draw(H_enron_cleaned);

When dealing with large structures like this e-mail dataset the visualization can be cumberstome to interpret. To help you with that XGI provides options for plotting hypergraph using the features of nodes and edges, for example:
[37]:
fig, ax = plt.subplots(figsize=(10, 10))
xgi.draw(
H_enron_cleaned,
node_size=H_enron_cleaned.nodes.degree,
node_lw=H_enron_cleaned.nodes.average_neighbor_degree,
node_fc=H_enron_cleaned.nodes.degree,
ax=ax,
);

In this case we are plotting the hypergraph with the size and color of nodes depending on their degrees and the width of the edges nodes markers depending on their average neighbor degree.
Histograms of edges sizes and nodes’ degrees#
It might me useful for a first analysis of you dataset to plot some histrograms representing relevant feautures of you higher-order structure. For example if you want to plot a histogram for the edges sizes:
[38]:
list_of_edges_sizes = H_enron_cleaned.edges.size.aslist()
ax = plt.subplot(111)
ax.hist(
list_of_edges_sizes,
bins=range(min(list_of_edges_sizes), max(list_of_edges_sizes) + 1, 1),
)
ax.set_xlabel("Edge size")
ax.set_ylabel("Frequency");

Or you can plot a histogram for the nodes’ degrees (the degree of a node is the number of edges it belongs to):
[39]:
list_of_nodes_degrees = H_enron_cleaned.nodes.degree.aslist()
ax = plt.subplot(111)
ax.hist(
list_of_nodes_degrees,
bins=range(min(list_of_nodes_degrees), max(list_of_nodes_degrees) + 1, 1),
)
ax.set_xlabel("Degree")
ax.set_ylabel("Frequency");

Incidence and Adjacency Matrices#
Any hypergraph can be expressed as an \(N \times M\) incidence matrix, \(I\), where \(N\) is the number of nodes and \(M\) is the number of edges. Rows indicate the node ID and the columns indicate the edge ID. \(I_{i,j}=1\) if node \(i\) is a member of edge \(j\) and zero otherwise. XGI allows you to access the incidence matrix in the following way:
[40]:
I = xgi.incidence_matrix(H_enron_cleaned, sparse=False)
Then you can visualize it:
[41]:
plt.spy(I, aspect="auto")
plt.xlabel("Hyperedges")
plt.ylabel("Nodes")
plt.show()

We can represent a hypergraph with an \(N\times N\) adjacency matrix, \(A\), where \(N\) is the number of nodes. Notice that the adjacency matrix is a lossy format: different hypergraphs can create the same adjacency matrix. \(A_{i,j} = 1\) if there is at least one hyperedge containing both nodes \(i\) and \(j\). XGI allows you to access the incidence matrix and visualize it in the following way:
[42]:
A = xgi.adjacency_matrix(H_enron_cleaned, sparse=False)
plt.spy(A);

If you are interested in other hypergraph matrices such as Laplacians, you can check the documentatation about the linear algebra package.
Algorithms#
The algorithms package contains different algorithms you can run on your higher-order structure. For example you can compute the density and degree assortativity of your structure:
[43]:
print("The density of the hypergraph is:", xgi.density(H_enron_cleaned))
print(
"The assortativity of the hypergraph is:", xgi.degree_assortativity(H_enron_cleaned)
)
The density of the hypergraph is: 1.3084764540479412e-40
The assortativity of the hypergraph is: 0.19079161924112412
Or you can access a dictionary containing the local clustering coefficient (overlap of the edges connected to a given node, normalized by the size of the node’s neighborhood, for more details you can see this paper) of your structures:
[44]:
local_clustering_dict = xgi.local_clustering_coefficient(H_enron_cleaned)
print(local_clustering_dict)
{0: 0.6112820852842796, 1: 0.6339562501152698, 2: 0.5837947373260839, 3: 0.6270767577062006, 4: 0.6497241898978037, 5: 0.6883028005666035, 6: 0.7179414488945046, 7: 0.5888506735657011, 8: 0.4407894736842105, 9: 0.6371499962535367, 10: 0.5050316490232456, 11: 0.6948362161100078, 12: 0.5376442220598063, 13: 0.664016042014978, 14: 0.7624893307053918, 15: 0.497096150343852, 16: 0.5910577243910577, 17: 0.634900381721316, 18: 0.766068236413062, 19: 0.6477387500991035, 20: 0.6690373322537638, 21: 0.6939042793534276, 22: 0.7150213237411382, 23: 0.7253351022853525, 24: 0.5731174220962014, 25: 0.7200429707439118, 26: 0.6174282057811469, 27: 0.6241125385660331, 28: 0.4005992715737551, 29: 0.5194849584118213, 30: 0.526652998109372, 31: 0.5844262531858526, 32: 0.5939397597517252, 33: 0.6858414113751181, 34: 0.507683862031688, 35: 0.5562193576874984, 36: 0.7702243211334115, 37: 0.576778940186673, 38: 0.6533890357032772, 39: 0.7565836877802451, 40: 0.6918456352561674, 41: 0.5647748459257745, 42: 0.5959885296057119, 43: 0.5941667576775036, 44: 0.5629877830492899, 45: 0.6482156013342742, 46: 0.6541550096846735, 47: 0.5555127808699237, 48: 0.31104175598450734, 49: 0.4818336052031706, 50: 0.44600245605261396, 51: 0.6291583565982627, 52: 0.4496236345226553, 53: 0.5321914288819476, 54: 0.5337357983377805, 55: 0.5701672379111409, 56: 0.5537683502866153, 57: 0.5655941364651768, 58: 0.6128947368421053, 59: 0.5022964378811156, 60: 0.5899685818651338, 61: 0.5529141568662711, 62: 0.4563533960592784, 63: 0.5848431625725427, 64: 0.6249894179894181, 65: 0.397861082186479, 66: 0.2835156585156585, 67: 0.34703000064077183, 68: 0.656276054192721, 69: 0.4, 70: 0.6166290815798693, 71: 0.7164102564102562, 72: 0.5388272654109755, 73: 0.6593915343915344, 74: 0.28759259259259257, 75: 0.5397187957323805, 76: 0.875, 77: 0.4584558823529412, 78: 0.5970418470418469, 79: 0.35079365079365077, 80: 0.7332732132848235, 81: 0.7524439150592998, 82: 0.7053656249978023, 83: 0.4829893839252128, 84: 0.6206113933284421, 85: 0.6643496682196373, 86: 0.10185185185185186, 87: 0.3972038705372038, 88: 0.14285714285714285, 89: 0.3817197668606982, 90: 0.5420164471691373, 91: 0.5307575816103237, 92: 0.4805555555555555, 93: 0.3333333333333333, 94: 0.6553560857095746, 95: 0.6660073260073266, 96: 0.6761991343753234, 97: 0.649158139983656, 98: 0.5404067609949962, 99: 0.6276284346409021, 100: 0.6479876903142192, 101: 0.7582776809910954, 102: 0.7985069668893183, 103: 0.7943813131313133, 104: 0.7972575768658715, 105: 0.6705467372134041, 106: 0.8068542568542567, 107: 0.6445237524094896, 108: 0.8040543775497484, 109: 0.6694284236616167, 110: 0.6320387000954226, 111: 0.4628384457130079, 112: 0.5089933780529755, 113: 0.38124097458215633, 114: 0.5388888888888889, 115: 0.5914639751747766, 116: 0.0, 117: 0.49485930735930733, 118: 0.25396825396825395, 119: 0.6259623015873018, 120: 0.542920838056306, 121: 0.3244949494949495, 122: 0.4056037626850434, 123: 0.6099122807017543, 124: 0.734848484848485, 125: 0.4683826358826358, 126: 0.5149134518735854, 127: 0.5472838504088505, 128: 0.5418831168831169, 129: 0.3392115964843238, 130: 0.5075465185759305, 131: 0.4756335282651072, 132: 0.48122373949579833, 133: 0.4385533953226262, 134: 0.5279958137100994, 135: 0.5370535714285715, 136: 0.46230936819172114, 137: 0.3531844081844078, 138: 0.5267857142857143, 139: 0.3333333333333333, 140: 0.3333333333333333, 141: 0.6666666666666666, 142: 0.26666666666666666}
Stats#
The stats package is one of the features that sets XGI apart from other libraries. It provides a common interface to all statistics that can be computed from a network, its nodes, or edges. This package allows you, for example, to filter the nodes of a hypergraph with a certain degree:
[45]:
nodes_degree_2 = H_enron_cleaned.nodes.filterby("degree", 20)
print(nodes_degree_2)
[8, 58, 123]
Or you can perform more complex tasks such as creating a dataframe containing different statistics:
[46]:
df = H_enron_cleaned.nodes.multi(["degree", "clustering_coefficient"]).aspandas()
print(df)
degree clustering_coefficient
0 44 0.548792
1 101 0.452685
2 57 0.529268
3 36 0.606272
4 50 0.569712
.. ... ...
138 8 0.535714
139 6 0.333333
140 4 1.000000
141 6 1.000000
142 6 1.000000
[143 rows x 2 columns]
You can learn more about the stats package with the focus tutorial on statistics or checking the documentation.
Wrapping Up#
Well done! 👏 You’ve covered a lot in just 15 minutes with XGI. We hope you enjoyed this tutorial, and there’s much more to explore! Check out other tutorials here!