Package: networkx-temporal
networkx-temporalPython package to build and manipulate temporal NetworkX graphs.RequirementsPython>=3.7networkx>=2.1pandas>=1.1.0InstallPackage is available to install onPyPI:pipinstallnetworkx-temporalUsageThe code provided as example below is also available as an interactiveJupyter notebook(open onColab).Build temporal graph: basics on manipulating anetworkx-temporalgraph object;Common metrics: common metrics available fromnetworkx;Convert from static to temporal graph: convertingnetworkxgraphs tonetworkx-temporal;Transform temporal graph: convertingnetworkx-temporalto other graph formats or representations;Detect temporal communities: example of temporal community detection with anetworkx-temporalobject.Build temporal graphTheTemporal{Di,Multi,MultiDi}Graphclass uses NetworkX graphs internally to allow easy manipulation of its data structures:importnetworkx_temporalastxfromnetworkx_temporal.tests.exampleimportdraw_temporal_graphTG=tx.TemporalDiGraph(t=4)TG[0].add_edge("a","b")TG[1].add_edge("c","b")TG[2].add_edge("d","c")TG[2].add_edge("d","e")TG[2].add_edge("a","c")TG[3].add_edge("f","e")TG[3].add_edge("f","a")TG[3].add_edge("f","b")draw_temporal_graph(TG,figsize=(8,2))Slice into temporal binsOnce initialized, a specified number of bins can be returned in a new object of the same type usingslice:TGS=TG.slice(bins=2)draw_temporal_graph(TGS,figsize=(4,2))By default, created bins are composed of non-overlapping edges and might have uneven size. To balance them, passqcut=True:TGS=TG.slice(bins=2,qcut=True)draw_temporal_graph(TGS,figsize=(4,2))Note that in some cases,qcutmay not be able to split the graph into the number of bins requested and will instead return the maximum number of bins possible. Other exceptions can be worked around by settingduplicates=Trueto allow duplicate edges in bins, orrank_first=Trueto balance snapshots considering the order in which nodes or edges appear.Convert to directed or undirectedWe can easily convert the edge directions by calling the same methods available fromnetwork:TG.to_undirected()# TemporalGraph(t=4)TG.to_directed()# TemporalDiGraph(t=4)Common metricsAll methods implemented bynetworkx, e.g.,degree, are also available to be executed sequentially on the stored time slices.A few additional methods that consider all time slices are also implemented for convenience, e.g.,temporal_degreeandtemporal_neighbors.Degree centralityTG.degree()# TG.in_degree()# TG.out_degree()TG.temporal_degree()# TG.temporal_in_degree()# TG.temporal_out_degree()Or to obtain the degree of a specific node:TG[0].degree("a")# TG[0].in_degree("a")# TG[0].out_degree("a")TG.temporal_degree("a")# TG.temporal_in_degree("a")# TG.temporal_out_degree("a")Node neighborhoodsTG.neighbors("c")To obtain the temporal neighborhood of a node considering all time steps, use the methodtemporal_neighbors:TG.temporal_neighbors("c")Order and sizeThe temporal order and size are defined as the number of unique nodes and edges, respectively, across all time steps.TG.order(),TG.size()TG.temporal_order(),TG.temporal_size()Note that we are also able to consider nodes or edges with distinct attributes as not unique by passingdata=Truetotemporal_{order,size}.Lastly, to obtain the total (non-unique) number of nodes and edges across all time steps, we may use thetotal_orderandtotal_sizemethods:TG.total_order(),TG.total_size()# sum(TG.order()), sum(TG.size())Convert from static to temporal graphStatic graphs can also carry temporal information either in the node- or edge-level attributes.Slicing a graph into bins usually result in the same number of edges, but a higher number of nodes, as they may appear in more than one snapshot to preserve edge information.In the example below, we create a static multigraph in which both nodes and edges are attributed with the time steptin which they are observed:importnetworkxasnxG=nx.MultiDiGraph()G.add_nodes_from([("a",{"t":0}),("b",{"t":0}),("c",{"t":1}),("d",{"t":2}),("e",{"t":3}),("f",{"t":3}),])G.add_edges_from([("a","b",{"t":0}),("c","b",{"t":1}),("d","c",{"t":2}),("d","e",{"t":2}),("a","c",{"t":2}),("f","e",{"t":3}),("f","a",{"t":3}),("f","b",{"t":3}),])Node-level time attributesConverting a static graph with node-level temporal data to a temporal graph object (node_levelconsiders the source node's time by default when slicing edges):TG=tx.from_static(G).slice(attr="t",attr_level="node",node_level="source",bins=None,qcut=None)draw_temporal_graph(TG,figsize=(8,2))Note that considering node-level attributes resulted in placing the edge(a, c, 2)in $t=0$ instead, as the source nodeaattribute is set tot=0:G.nodes(data=True)["a"]Edge-level time attributesConverting a static graph with edge-level temporal data to a temporal graph object (edge's time applies to both source and target nodes):TG=tx.from_static(G).slice(attr="t",attr_level="edge",bins=None,qcut=None)draw_temporal_graph(TG,figsize=(8,2))In this case, considering edge-level attributes results in placing the edge(a, c, 2)in $t=2$, as expected.Transform temporal graphOnce a temporal graph is instantiated, some methods are implemented that allow converting it or returning snaphots, events or unified temporal graphs.to_static: returns a single graph with unique nodes, does not support dynamic node attributes;to_unified: returns a single graph with non-unique nodes, supports dynamic node attributes;to_snapshots: returns a list of graphs with possibly repeated nodes among snapshots;to_events: returns a list of edge-level events as 3-tuples or 4-tuples, without attributes.Convert to different object typeTemporal graphs may be converted to a different object type by callingconvert_toor passingto={package}to the above methods, providedpackageis locally installed. Supported formats:PackageParameterAliasDeep Graph Librarydgl-graph-toolgraph_toolgtigraphigraphigNetworKitnetworkitnkPyTorch Geometrictorch_geometricpygtx.convert_to(G,"igraph")Static graphBuilds a static or flattened graph containing all the edges found at each time step:G=TG.to_static()draw_temporal_graph(G,suptitle="Static Graph")Snapshot-based temporal graphThe snapshot-based temporal graph (STG) is a list of graphs directly accessible underdatain the temporal graph object:STG=TG.to_snapshots()# STG == TG.dataUnified temporal graphThe unified temporal graph (UTG) is a single graph that contains the original data plus proxy nodes and edge couplings connecting sequential temporal nodes.UTG=TG.to_unified()nodes=sorted(TG.temporal_nodes())pos={node:(nodes.index(node.rsplit("_")[0]),-int(node.rsplit("_")[1]))fornodeinUTG.nodes()}draw_temporal_graph(UTG,pos=pos,figsize=(4,4),connectionstyle="arc3,rad=0.25",suptitle="Unified Temporal Graph")Event-based temporal graphAn event-based temporal graph (ETG) is a sequence of 3- or 4-tuple edge-based events.3-tuples:(u, v, t), where elements are the source node, target node, and time step of the observed event (also known as a stream graph);4-tuples:(u, v, t, e), whereeis either a positive (1) or negative (-1) unity for edge addition and deletion, respectively.ETG=TG.to_events()# stream=True (default)ETG# [('a', 'b', 0),#  ('c', 'b', 1),#  ('a', 'c', 2),#  ('d', 'c', 2),#  ('d', 'e', 2),#  ('f', 'e', 3),#  ('f', 'a', 3),#  ('f', 'b', 3)]ETG=TG.to_events(stream=False)ETG# [('a', 'b', 0, 1),#  ('c', 'b', 1, 1),#  ('a', 'b', 1, -1),#  ('a', 'c', 2, 1),#  ('d', 'c', 2, 1),#  ('d', 'e', 2, 1),#  ('c', 'b', 2, -1),#  ('f', 'e', 3, 1),#  ('f', 'a', 3, 1),#  ('f', 'b', 3, 1),#  ('a', 'c', 3, -1),#  ('d', 'c', 3, -1),#  ('d', 'e', 3, -1)]Convert back to TemporalGraph objectFunctions to convert a newly created STG, ETG, or UTG back to a temporal graph object are also implemented.tx.from_snapshots(STG)tx.from_events(ETG,directed=False,multigraph=True)tx.from_unified(UTG)Detect temporal communitiesTheleidenalgpackage implements community detection algorithms on snapshot-based temporal graphs.Depending on the objectives, temporal community detection may bring significant advantages on what comes to descriptive tasks and post-hoc network analysis.Let's first use theStochastic Block Modelto construct a temporal graph of 4 snapshots, in which each of the five clusters of four nodes each continuously mix together:snapshots=4# Temporal snapshots to creaete.clusters=5# Number of clusters/communities.order=4# Nodes in each cluster.intra=.95# High probability of intra-community edges.inter=.05# Low initial probability of inter-community edges.change=.25# Change in intra- and inter-community edges over time.# Get probabilities for each snapshot.probs=[[[(intraifi==jelseinter)+(t*(change/snapshots)*(-1ifi==jelse1))forjinrange(clusters)]foriinrange(clusters)]fortinrange(snapshots)]# Create graphs from probabilities.graphs={}fortinrange(snapshots):graphs[t]=nx.stochastic_block_model(clusters*[order],probs[t],seed=10)graphs[t].name=t# Create temporal graph from snapshots.TG=tx.from_snapshots(graphs)Static community detectionOn the static graph (flattened)Running the Leiden algorithm on the static graph to obtain the community modules fails to retrieve the five communities in the network:importleidenalgaslac=plt.cm.tab10.colorsmembership=la.find_partition(TG.to_static("igraph"),la.ModularityVertexPartition,n_iterations=-1,seed=0,)node_color=[c[m]forminmembership.membership]draw_temporal_graph(TG.to_static(),figsize=(4,4),node_color=node_color,suptitle="Static Communities")We can plot all four generated snapshots, while keeping the community assignments from the previous run:draw_temporal_graph(TG,figsize=(12,4),node_color=node_color,suptitle="Static Communities")Note that running the same algorithm on the unified temporal graph also yields no significant advantages in terms of correctly retrieving the five clusters.On the snapshots (individually)Running the same algorithm on each of the generated snapshots instead retrieves the correct clusters on the first snapshot only.Although results may seem initially better, we lose the community indices previously assigned to nodes in previous snapshots, represented by their different colors:temporal_opts={}fortinrange(len(TG)):membership=la.find_partition(TG[t:t+1].to_static("igraph"),la.ModularityVertexPartition,n_iterations=-1,seed=0,)temporal_opts[t]={"node_color":[c[m]forminmembership.membership]}draw_temporal_graph(TG,nrows=1,ncols=4,figsize=(12,4),suptitle="Snapshot Communities",temporal_opts=temporal_opts)Temporal community detectionDetecting temporal communities instead allows us to correctly retrieve the clusters in all snapshots, while maintaining their indices/colors over time.Theinterslice_weightamong temporal nodes in a sequence of snapshots defaults to1.0in unweighted graphs and may be adjusted accordingly:temporal_membership,improvement=la.find_partition_temporal(TG.to_snapshots("igraph"),la.ModularityVertexPartition,interslice_weight=1.0,n_iterations=-1,seed=0,vertex_id_attr="_nx_name")temporal_opts={t:{"node_color":[c[m]formintemporal_membership[t]]}fortinrange(len(TG))}draw_temporal_graph(TG,nrows=1,ncols=4,figsize=(12,4),suptitle="Temporal Communities",temporal_opts=temporal_opts)ReferencesDeep Graph Librarygraph-tooligraphLeidenNetworKitNetworkXPandasPyTorch Geometric