Temporal Graph Analysis¶
Prerequisites¶
First, we need to set up our Python environment that has PyTorch, PyTorch Geometric and PathpyG installed. Depending on where you are executing this notebook, this might already be (partially) done. E.g. Google Colab has PyTorch installed by default so we only need to install the remaining dependencies. The DevContainer that is part of our GitHub Repository on the other hand already has all of the necessary dependencies installed.
In the following, we install the packages for usage in Google Colab using Jupyter magic commands. For other environments comment in or out the commands as necessary. For more details on how to install pathpyG
especially if you want to install it with GPU-support, we refer to our documentation. Note that %%capture
discards the full output of the cell to not clutter this tutorial with unnecessary installation details. If you want to print the output, you can comment %%capture
out.
%%capture
# !pip install torch
!pip install torch_geometric
!pip install git+https://github.com/pathpy/pathpyG.git
Motivation and Learning Objectives¶
In this tutorial we will introduce the representation of temporal graph data using the TemporalGraph
class and how such data can be used to calculate shortest time respecting paths between nodes as well temporal node cemtralities.
import torch
from torch_geometric.data import Data
import pathpyG as pp
import pandas as pd
pp.config['torch']['device'] = 'cpu'
We can create a temporal graph object from a list of time-stamped edges. Since TemporalGraph
is a subclass of the Graph
class, the internal structures are very similar:
tedges = [('a', 'b', 1),('a', 'b', 2), ('b', 'a', 3), ('b', 'c', 3), ('d', 'c', 4), ('a', 'b', 4), ('c', 'b', 4),
('c', 'd', 5), ('b', 'a', 5), ('c', 'b', 6)]
t = pp.TemporalGraph.from_edge_list(tedges)
print(t.mapping)
print(t.n)
print(t.m)
a -> 0 b -> 1 c -> 2 d -> 3 4 10
By default, all temporal graphs are directed. We can create an undirected version a temporal graph as follows:
x = t.to_undirected()
print(x.mapping)
print(x.n)
print(x.m)
a -> 0 b -> 1 c -> 2 d -> 3 4 20
We can also directly create a temporal graph from an instance of pyG.TemporalData
td = Data(
edge_index = torch.Tensor([[0,1,2,0],[1,2,3,1]]).long(),
time = torch.Tensor([0,1,2,3])
)
print(td)
t2 = pp.TemporalGraph(td)
print(t2)
Data(edge_index=[2, 4], time=[4]) Temporal Graph with 4 nodes, 3 unique edges and 4 events in [0.0, 3.0] {'Edge Attributes': {}, 'Graph Attributes': {}, 'Node Attributes': {}}
/opt/conda/lib/python3.10/site-packages/torch_geometric/data/storage.py:450: UserWarning: Unable to accurately infer 'num_nodes' from the attribute set '{'edge_index', 'time'}'. Please explicitly set 'num_nodes' as an attribute of 'data' to suppress this warning warnings.warn(
We can restrict a temporal graph to a time window, which returns a temporal graph that only contains time-stamped edges in the given time interval.
t1 = t.get_window(0,4)
print(t1)
print(t1.m)
print(t1.start_time)
print(t1.end_time)
Temporal Graph with 4 nodes, 5 unique edges and 7 events in [1.0, 4.0] {'Edge Attributes': {}, 'Graph Attributes': {'num_nodes': "<class 'int'>"}, 'Node Attributes': {}} 7 1.0 4.0
We can also extract a TemporalGraph object for a batch of temporal edges, which is defined by the start and end index of the edges defining the batch.
t1 = t.get_batch(1,6)
print(t1)
print(t1.m)
print(t1.start_time)
print(t1.end_time)
Temporal Graph with 4 nodes, 4 unique edges and 5 events in [2.0, 4.0] {'Edge Attributes': {}, 'Graph Attributes': {}, 'Node Attributes': {}} 5 2.0 4.0
We can easily convert a temporal graph into a weighted time-aggregated static graph, where edge weights count the number of occurrences of an edge across all timestamps.
g = t.to_static_graph(weighted=True)
print(g)
Directed graph with 4 nodes and 6 edges {'Edge Attributes': {'edge_weight': "<class 'torch.Tensor'> -> torch.Size([6])"}, 'Graph Attributes': {'num_nodes': "<class 'int'>"}, 'Node Attributes': {}}
We can also aggregate a temporal graph within a certain time window:
g = t.to_static_graph(time_window=(1, 3), weighted=True)
print(g)
Directed graph with 2 nodes and 1 edges {'Edge Attributes': {'edge_weight': "<class 'torch.Tensor'> -> torch.Size([1])"}, 'Graph Attributes': {'num_nodes': "<class 'int'>"}, 'Node Attributes': {}}
Finally, we can use the class RollingTimeWindow
to perform a rolling window analysis. The class returns an iterable object, where each iteration yields a time-aggregated weighted graph object as well as the corresponding time window.
r = pp.algorithms.RollingTimeWindow(t, window_size=3, step_size=1, return_window=True)
for g, w in r:
print('Time window ', w)
print(g)
print(g.data.edge_index)
print('---')
Time window (1.0, 4.0) Directed graph with 3 nodes and 3 edges {'Edge Attributes': {'edge_weight': "<class 'torch.Tensor'> -> torch.Size([3])"}, 'Graph Attributes': {'num_nodes': "<class 'int'>"}, 'Node Attributes': {}} EdgeIndex([[0, 1, 1], [1, 0, 2]], sparse_size=(3, 3), nnz=3, sort_order=row) --- Time window (2.0, 5.0) Directed graph with 4 nodes and 5 edges {'Edge Attributes': {'edge_weight': "<class 'torch.Tensor'> -> torch.Size([5])"}, 'Graph Attributes': {'num_nodes': "<class 'int'>"}, 'Node Attributes': {}} EdgeIndex([[0, 1, 1, 2, 3], [1, 0, 2, 1, 2]], sparse_size=(4, 4), nnz=5, sort_order=row) --- Time window (3.0, 6.0) Directed graph with 4 nodes and 6 edges {'Edge Attributes': {'edge_weight': "<class 'torch.Tensor'> -> torch.Size([6])"}, 'Graph Attributes': {'num_nodes': "<class 'int'>"}, 'Node Attributes': {}} EdgeIndex([[0, 1, 1, 2, 2, 3], [1, 0, 2, 1, 3, 2]], sparse_size=(4, 4), nnz=6, sort_order=row) --- Time window (4.0, 7.0) Directed graph with 4 nodes and 5 edges {'Edge Attributes': {'edge_weight': "<class 'torch.Tensor'> -> torch.Size([5])"}, 'Graph Attributes': {'num_nodes': "<class 'int'>"}, 'Node Attributes': {}} EdgeIndex([[0, 1, 2, 2, 3], [1, 0, 1, 3, 2]], sparse_size=(4, 4), nnz=5, sort_order=row) --- Time window (5.0, 8.0) Directed graph with 4 nodes and 3 edges {'Edge Attributes': {'edge_weight': "<class 'torch.Tensor'> -> torch.Size([3])"}, 'Graph Attributes': {'num_nodes': "<class 'int'>"}, 'Node Attributes': {}} EdgeIndex([[1, 2, 2], [0, 1, 3]], sparse_size=(4, 4), nnz=3, sort_order=row) --- Time window (6.0, 9.0) Directed graph with 3 nodes and 1 edges {'Edge Attributes': {'edge_weight': "<class 'torch.Tensor'> -> torch.Size([1])"}, 'Graph Attributes': {'num_nodes': "<class 'int'>"}, 'Node Attributes': {}} EdgeIndex([[2], [1]], sparse_size=(3, 3), nnz=1, sort_order=row) ---
We can visualize temporal graphs using the plot function just like static graphs:
pp.plot(t, node_label=t.nodes, edge_color='lightgray');
The source nodes, destination nodes and timestamps of time-stamped edges are stored as a pyG TemporalData
object, which we can access in the following way.
t.data
Data(edge_index=[2, 10], time=[10], num_nodes=4)
print(t.data.edge_index)
EdgeIndex([[0, 0, 1, 1, 3, 0, 2, 2, 1, 2], [1, 1, 0, 2, 2, 1, 1, 3, 0, 1]], sparse_size=(4, 4), nnz=10)
print(t.data.time)
tensor([1., 2., 3., 3., 4., 4., 4., 5., 5., 6.])
With the generator functions edges
and temporal_edges
we can iterate through the time-ordered (temporal) multi-edges of a temporal graph.
for v, w in t.edges:
print(v, w)
a b a b b a b c d c a b c b c d b a c b
for v, w, time in t.temporal_edges:
print(v, w, time)
a b 1.0 a b 2.0 b a 3.0 b c 3.0 d c 4.0 a b 4.0 c b 4.0 c d 5.0 b a 5.0 c b 6.0
Extracting Time-Respecting Paths in Temporal Networks¶
We are often interested in time-respecting paths in a temporal graph. A time-respecting path consists of a sequence of nodes $v_0,...,v_l$ where consecutive nodes are connected by time-stamped edges that occur (i) in the right temporal ordering, and (ii) within a maximum time difference of $\delta\in \N$.
To calculate time-respecting paths in a temporal graph, we can construct a directed acyclic graph (DAG), where each time-stamped edge $(u,v;t)$ in the temporal graph is represented by a node and two nodes representing time-stamped edges $(u,v;t_1)$ and $(v,w;t_2)$ are connected by an edge iff $0 < t_2-t_1 \leq \delta$. This implies that (i) each edge in the resulting DAG represents a time-respecting path of length two, and (ii) time-respecting paths of any lenghts are represented by paths in this DAG.
We can construct such a DAG using the function pp.algorithms.lift_order_temporal
, which returns an edge_index. We can pass this to the constructor of a Graph
object, which we can use to visualize the resulting DAG.
e_i = pp.algorithms.lift_order_temporal(t, delta=1)
dag = pp.Graph.from_edge_index(e_i)
pp.plot(dag, node_label = [f'{v}-{w}-{time}' for v, w, time in t.temporal_edges]);
100%|██████████| 6/6 [00:00<00:00, 2649.31it/s]
For $\delta=1$, this DAG with three connected components tells us that the underlying temporal graph has the following time-respecting paths (of different lengths):
Length one:
a -> b
b -> a
b -> c
c -> b
c -> d
d -> c
Length two:
a -> b -> a (twice, starting at time 2 and time 4)
b -> a -> b
a -> b -> c
b -> c -> b
c -> b -> a
d -> c -> d
Length three:
a -> b -> a -> b
b -> a -> b -> a
a -> b -> c -> b
b -> c -> b -> a
Length four:
a -> b -> a -> b -> a
a -> b -> c -> b -> a
We can can use the function pp.algorithms.temporal.temporal_shortest_paths
to calculate shortest time-respecting path distances between any pair of nodes. This also returns a predecessor matrix, which can be used to reconstruct all shortest time-respecting paths (in analogy to the Dijkstra algorithm for static graphs):
dist, pred = pp.algorithms.temporal.temporal_shortest_paths(t, delta=1)
print(t.mapping)
print(dist)
print(pred)
100%|██████████| 6/6 [00:00<00:00, 2528.47it/s]
a -> 0 b -> 1 c -> 2 d -> 3 [[ 0. 1. 2. inf] [ 1. 0. 1. inf] [ 2. 1. 0. 1.] [inf inf 1. 0.]] [[ 0 0 1 -1] [ 1 1 1 -1] [ 1 2 2 2] [-1 -1 3 3]]
In the example above, the four inf
values indicate that there is no time-respecting paths between the four node pairs (a, d), (b, d), (d,a) and (d, b). This is not something we would expect based on the (strongly connected) topology of the time-aggregated graph, which is shown below:
g = t.to_static_graph(weighted=True)
pp.plot(g, node_label=g.mapping.node_ids.tolist());
Reading and writing temporal graph data¶
tedges = [('a', 'b', 1),('a', 'b', 2), ('b', 'a', 3), ('b', 'c', 3), ('d', 'c', 4), ('a', 'b', 4), ('c', 'b', 4),
('c', 'd', 5), ('b', 'a', 5), ('c', 'b', 6)]
t = pp.TemporalGraph.from_edge_list(tedges)
df = pp.io.temporal_graph_to_df(t)
print(df)
v w t 0 c b 6.0 1 b a 5.0 2 c d 5.0 3 c b 4.0 4 a b 4.0 5 d c 4.0 6 b c 3.0 7 b a 3.0 8 a b 2.0 9 a b 1.0
t = pp.io.df_to_temporal_graph(df)
print(t)
Temporal Graph with 4 nodes, 6 unique edges and 10 events in [1.0, 6.0] {'Edge Attributes': {}, 'Graph Attributes': {'num_nodes': "<class 'int'>"}, 'Node Attributes': {}}
df = pd.DataFrame([['a', 'b', 1], ['b', 'c', 2], ['a', 'c', 3]])
print(df)
t = pp.io.df_to_temporal_graph(df)
print(t)
0 1 2 0 a b 1 1 b c 2 2 a c 3 Temporal Graph with 3 nodes, 3 unique edges and 3 events in [1.0, 3.0] {'Edge Attributes': {}, 'Graph Attributes': {'num_nodes': "<class 'int'>"}, 'Node Attributes': {}}
pp.io.write_csv(t, '../data/test_temporal_graph.csv')
t = pp.io.read_csv_temporal_graph('../data/test_temporal_graph.csv')
print(t)
Temporal Graph with 3 nodes, 6 unique edges and 6 events in [1.0, 3.0] {'Edge Attributes': {}, 'Graph Attributes': {}, 'Node Attributes': {}}
Temporal Centralities in Empirical Temporal Networks¶
pathpyG
's ability to calculate (shortest) time-respecting paths enables us to calulate different notions of temporal centralities for nodes in empirial temporal networks. We can read an empirical temporal graph based on CSV data, where each line contains the source, target, and timestamp of an edge as comma-separated value:
t_ants = pp.io.read_csv_temporal_graph('../data/ants_1_1.tedges', header=False)
print(t_ants)
Temporal Graph with 89 nodes, 1298 unique edges and 3822 events in [0.0, 1438.0] {'Edge Attributes': {}, 'Graph Attributes': {}, 'Node Attributes': {}}
To calculate the temporal closeness centrality, which is defined based on the length of shortest time-respecting paths of a node to all other nodes, we can write the following:
cl = pp.algorithms.centrality.temporal_closeness_centrality(t_ants, delta=60)
print(cl)
mx = max(cl.values())
mn = min(cl.values())
node_size = { v: 50*(x/(mx-mn)) for v, x in cl.items() }
pp.plot(t_ants, node_size=node_size, edge_color='red', edge_size=4);
100%|██████████| 883/883 [00:00<00:00, 3322.84it/s]
{'GBGR': 3024.615873015872, 'GBGW': 2999.502564102565, 'GBG_': 2928.756043956045, 'GGGG': 2859.2000000000007, 'GGGR': 2811.4253968253956, 'GGRR': 4383.866666666668, 'GGRY': 3304.400000000001, 'GGWW': 3418.311111111112, 'GGWY': 5092.2666666666655, 'GGW_': 4230.076190476191, 'GGYW': 2556.571916971917, 'GG_W': 3788.4000000000015, 'GRBR': 2977.3714285714286, 'GRGY': 3039.911111111111, 'GRWG': 4162.714285714285, 'GRYY': 2736.625396825396, 'GR_Y': 3113.7333333333336, 'GR_Y2': 4416.133333333335, 'GR__': 3305.8666666666663, 'GWRG': 3379.2000000000003, 'GYGG': 3321.2977777777783, 'GYYY': 2301.3777777777777, 'GY__': 2260.066666666665, 'G_GW': 3525.866666666667, 'G_R_': 4034.800000000001, 'G_W_': 3010.4380952380957, 'G___': 3100.533333333335, 'G___big': 2068.308913308913, 'G___small': 2351.7999999999993, 'Q': 4177.311111111112, 'RWGY': 3708.5714285714307, 'RWWG': 3030.488888888889, 'WBGG': 3781.0666666666675, 'WBGW': 3166.742857142857, 'WBYG': 2668.5999999999995, 'WGBB': 3440.171428571429, 'WGGB': 3751.1047619047636, 'WGWB': 4185.7619047619055, 'WG_R': 4216.666666666668, 'WRBB': 4256.266666666667, 'WRRY': 3768.600000000001, 'WRR_': 2583.8825396825387, 'WRWR': 3951.200000000001, 'WR__': 3180.7111111111117, 'WWBG': 2788.5206349206346, 'WYGG': 3617.466666666668, 'W___': 3253.0666666666675, 'YGWW': 4867.866666666667, 'YGWY': 2735.542857142857, 'YWGW': 3273.5999999999995, 'YWWW': 2991.999999999999, 'YWW_': 3136.082539682539, 'YW__': 1220.9476986781337, 'YYGG': 4430.8, 'YYGGmid': 4461.6, 'YYGGright': 3284.565079365079, 'YYGW': 3462.844444444446, 'YYRB': 3366.0000000000005, 'YYRG': 2891.30862663906, 'YYWR': 3060.6888888888893, 'YYYY': 2510.339682539682, 'YYY_': 2561.692063492063, 'YY_R': 3472.926984126984, 'YY_W': 3874.9333333333357, 'YY__': 2901.5206349206346, 'Y_WY': 3572.800000000001, 'Y__W': 2301.933333333332, 'Y___': 3097.8444444444453, '_RYG': 1844.680341880342, '_R__': 4439.600000000002, '_WGG': 3781.0666666666684, '_WWW': 2961.6539682539687, '_WWY': 4007.771428571429, '_WYG': 3018.9199999999996, '_WYW': 4252.914285714287, '_W_Y': 3262.742857142857, '_W__': 4009.8666666666677, '_Y__': 2881.7179487179487, '__BB': 3040.9200000000005, '__W_': 3649.4158730158724, '____almost': 3127.809523809524, '____bm': 3655.6, '____bot': 2454.007326007325, '____brood': 3952.0380952380965, '____corner': 3856.638095238096, '____pale': 4692.7047619047635, '____right': 2577.243809523809, '____topleft': 3392.4, '____topright': 2641.047619047619}
The definition of time-respecting paths depends on our maximum time difference parameter $\delta$, which implies that different values of this parameter also yield different centralities. This means that we can calculate temporal node centralities for different "time scales" of a temporal graph.
cl = pp.algorithms.centrality.temporal_closeness_centrality(t_ants, delta=20)
print(cl)
mx = max(cl.values())
mn = min(cl.values())
node_size = { v: 50*(x/(mx-mn)) for v, x in cl.items() }
pp.plot(t_ants, node_size=node_size, edge_color='red', edge_size=4);
100%|██████████| 883/883 [00:00<00:00, 3573.09it/s]
{'GBGR': 2207.0092796092795, 'GBGW': 1574.1780861656403, 'GBG_': 2245.6238095238077, 'GGGG': 2065.6316957552262, 'GGGR': 2052.219608851188, 'GGRR': 3774.042140822139, 'GGRY': 2541.641269841269, 'GGWW': 2203.5236655224166, 'GGWY': 4598.104761904761, 'GGW_': 3827.514285714286, 'GGYW': 1998.2796889101228, 'GG_W': 3159.310780722547, 'GRBR': 2252.3125606823146, 'GRGY': 1961.2073260073257, 'GRWG': 3434.115731505299, 'GRYY': 1769.6720992921016, 'GR_Y': 2458.6663362781, 'GR_Y2': 3750.974603174604, 'GR__': 2287.758730158729, 'GWRG': 2460.425432737198, 'GYGG': 2512.0908424908425, 'GYYY': 1082.0915750915751, 'GY__': 1348.5477329496612, 'G_GW': 2936.1425267542913, 'G_R_': 3429.2739926739923, 'G_W_': 2107.469050754098, 'G___': 2127.2349206349195, 'G___big': 440.0, 'G___small': 1357.2765347758534, 'Q': 3506.6336134453786, 'RWGY': 2859.332112332112, 'RWWG': 2268.550438842203, 'WBGG': 2896.1205924510273, 'WBGW': 2433.333613445378, 'WBYG': 1977.0355670473316, 'WGBB': 2559.064886091109, 'WGGB': 2960.0190476190473, 'WGWB': 3741.2707061969318, 'WG_R': 3544.1333333333337, 'WRBB': 3554.813675213675, 'WRRY': 3153.2702075702073, 'WRR_': 1459.0539682539682, 'WRWR': 3188.4784241901884, 'WR__': 2500.955555555555, 'WWBG': 2036.6218377769462, 'WYGG': 2725.1190476190477, 'W___': 2336.6073260073267, 'YGWW': 4164.021611721611, 'YGWY': 2041.505738705739, 'YWGW': 2504.1366234788234, 'YWWW': 2304.5686202686197, 'YWW_': 2371.849188204297, 'YW__': 176.0, 'YYGG': 3941.017740429505, 'YYGGmid': 3978.1587301587306, 'YYGGright': 2585.841391941392, 'YYGW': 2942.4609600925396, 'YYRB': 2324.445981469511, 'YYRG': 2314.7781799899453, 'YYWR': 2196.252000287295, 'YYYY': 1173.542857142857, 'YYY_': 1451.0821205745692, 'YY_R': 2608.8452541610436, 'YY_W': 3165.41684981685, 'YY__': 2165.776312576312, 'Y_WY': 3077.7333333333327, 'Y__W': 1355.0833598705335, 'Y___': 2157.622222222223, '_RYG': 885.6630036630038, '_R__': 3843.0485958485956, '_WGG': 3243.3761904761905, '_WWW': 1688.0676434676436, '_WWY': 3225.482539682539, '_WYG': 2279.3990591108236, '_WYW': 3684.37142857143, '_W_Y': 2395.1028197945843, '_W__': 3375.862184873949, '_Y__': 2151.667587957515, '__BB': 1831.7949074070343, '__W_': 2927.860263403607, '____almost': 2504.1190835308475, '____bm': 2888.322842445042, '____bot': 1918.2603174603173, '____brood': 3139.037484737485, '____corner': 3197.7706444286337, '____pale': 4135.756532356533, '____right': 1686.4830900989923, '____topleft': 2661.486524002313, '____topright': 1843.6328641362072}
We can also calculate the temporal betweenness centrality, which is based on the number of shortest time-respecting paths between pairs of nodes that pass through a given node. Again, this centrality score is sensitive to the time scale parameter $\delta$.
bw = pp.algorithms.centrality.temporal_betweenness_centrality(t_ants, delta=60)
print(bw)
mx = max(bw.values())
mn = min(bw.values())
node_size = { v: 50*(x/(mx-mn)) for v, x in bw.items() }
pp.plot(t_ants, node_size=node_size, edge_color='red', edge_size=4);
100%|██████████| 883/883 [00:00<00:00, 3475.66it/s] 100%|██████████| 89/89 [00:03<00:00, 27.00it/s]
defaultdict(<function temporal_betweenness_centrality.<locals>.<lambda> at 0x7fdf20a03640>, {'GBGR': 20.192212842427075, 'WGBB': 194.79309833949083, 'WRBB': 365.05013895123216, 'G___': 57.80632230642795, '_WYW': 427.3675796732633, '____topright': 10.760025336141013, 'YYGGmid': 663.4450747248108, '__W_': 46.375038482274306, 'GG_W': 292.080282213412, '_W__': 428.33207844162627, 'WBGW': 71.89857589572318, 'GR__': 64.19763457586924, 'RWWG': 83.25286865190012, '____almost': 134.89554914241063, 'GGGG': 78.97281365765863, 'YYGW': 319.0275353843234, 'YWGW': 84.52883527311734, 'YYWR': 139.62781952597214, 'YYRG': 30.06998326775235, 'WRWR': 130.49639766193505, 'WYGG': 274.1787721126864, 'GGYW': 9.54474098670821, 'GYGG': 206.18561458924665, 'GGWY': 1080.0676471347801, 'G_W_': 30.258200815352502, '_W_Y': 88.46660448533098, '_R__': 724.7535183751095, 'WBGG': 184.56469995815524, 'Y___': 124.00746952439442, '____brood': 261.05135853722055, 'WRRY': 227.56794665132307, '_WWY': 258.17798469631225, 'Y_WY': 168.0214751986637, 'GGWW': 127.47912860736434, 'YYGG': 452.6097076570282, 'YY_W': 372.5488526120025, '____topleft': 72.04751294375436, 'GBG_': 156.55265249530558, 'G_GW': 322.43708957585955, 'YGWY': 17.697241798229907, 'GRBR': 71.17718734804407, 'GGGR': 106.77684001312716, 'GGW_': 756.8587346464093, '____bm': 155.14819522566728, 'WGWB': 353.62165711237867, 'WWBG': 210.3370212595935, 'GRYY': 63.34672574396869, '_WWW': 59.796937914791975, '____right': 6.536713311055403, '_WYG': 159.5168700848977, 'YY__': 48.63844749559088, 'GRGY': 45.365480292377676, 'G_R_': 90.00180925262697, 'Q': 554.8667166042984, 'YYGGright': 329.28472257372005, 'WR__': 38.99166576434191, '____corner': 502.8870906126399, 'WG_R': 269.7153804058385, 'GR_Y2': 337.26045226613337, 'GWRG': 218.44745266674752, 'G___small': 20.205088001429456, 'YGWW': 535.3204311320455, 'YY_R': 143.38496654670294, 'RWGY': 235.07340157680488, 'WGGB': 131.96062903943462, '_WGG': 318.5137155129991, '__BB': 79.02021455742683, 'WBYG': 26.79640022964187, 'GRWG': 385.8105700686363, 'W___': 115.05159194927882, 'GGRR': 487.43937337734934, 'Y__W': 1.0294117647058814, 'GY__': 35.364487473310994, 'GR_Y': 23.23800388929457, 'YWW_': 116.98999358694506, 'YYRB': 32.66941405832229, '_Y__': 25.129282093002608, 'GBGW': 49.133677074004986, 'YYY_': 86.25984566814704, 'GGRY': 269.8419665751779, 'YWWW': 39.83384050331751, '____pale': 591.7102550688354, '____bot': 7.010335960335962, 'GYYY': 9.375127968877965, '_RYG': 5.108461302211301, 'WRR_': 74.76533132490208, 'YYYY': 1.000000000000001, 'G___big': -1.7708057242771247e-14, 'YW__': 0.0})
bw = pp.algorithms.centrality.temporal_betweenness_centrality(t_ants, delta=20)
print(bw)
mx = max(bw.values())
mn = min(bw.values())
node_size = { v: 50*(x/(mx-mn)) for v, x in bw.items() }
pp.plot(t_ants, node_size=node_size, edge_color='red', edge_size=4);
100%|██████████| 883/883 [00:00<00:00, 3813.91it/s] 100%|██████████| 89/89 [00:01<00:00, 69.84it/s]
defaultdict(<function temporal_betweenness_centrality.<locals>.<lambda> at 0x7fdf20ed9b40>, {'GBGR': 271.0549203258132, 'YY__': 260.787489965592, '_WYG': 394.1302318596431, '____right': 28.503246753246753, 'Q': 1457.263110413932, 'GGRR': 393.6459799032444, 'GGGR': 201.48208556149757, 'YYGGright': 534.66060053214, 'YY_R': 266.91765439796467, 'Y___': 172.67107899804276, '____corner': 551.646068862101, 'YGWW': 1201.1725191494913, 'WG_R': 512.4899073911774, '_WGG': 662.2060823813118, 'YWGW': 71.94722222222224, '____bm': 520.5037887625123, '__BB': 146.4024424420817, '_Y__': 116.65703669247497, 'WBYG': 337.50697274935305, 'YYWR': 346.0394146748694, '_W_Y': 126.7397860593513, 'WBGG': 100.16123039327377, 'Y_WY': 709.6990969778257, '_W__': 888.9702395101842, 'GGWW': 252.45108178976466, 'RWGY': 400.3435250525273, 'WR__': 324.7003673342413, 'G_R_': 74.0687563696638, '____bot': 18.876190476190477, '____pale': 1160.042127920021, '____almost': 586.5742800306991, '____topright': 6.242857142857144, 'YYGG': 900.9820998851815, '_WYW': 910.3151884148607, 'GGW_': 1229.7209102391769, 'G_GW': 411.33446741036244, 'YYGGmid': 941.7798494844893, 'GG_W': 893.2237821543014, 'WWBG': 82.09047619047621, 'GRWG': 765.7519205139172, 'WGGB': 562.7259479161665, 'WRWR': 79.9309806206866, '____topleft': 31.47539682539684, 'GGRY': 1009.5761263646133, 'GGWY': 2510.3962123782903, 'RWWG': 238.21048955595802, 'GBG_': 219.31044140486634, 'WGWB': 651.4299970278146, '_R__': 1516.87808629868, 'GGYW': 96.16713560885782, 'WGBB': 458.9464625746425, 'GRBR': 229.0634357985618, '____brood': 241.461111111111, 'G___small': 2.0000000000000027, 'GR_Y2': 782.0649724285778, 'W___': 307.82509532385853, 'YYGW': 851.6365545426578, 'GY__': 74.0, 'GBGW': 47.5015406162465, 'YYY_': 148.44444444444446, '__W_': 184.20506454409792, 'YYRB': 213.21211161607084, 'WYGG': 427.62927891499254, 'WRBB': 343.03298248682995, 'WRRY': 409.20603349701526, 'YWWW': 8.403968253968255, 'WBGW': 28.814786967418545, 'GR_Y': 24.047859363598853, 'YWW_': 32.92341269841275, '_WWY': 660.9893002248847, 'YGWY': 172.38878675703566, 'GWRG': 172.5340550134477, 'GYGG': 596.7332954372962, 'GRYY': 221.22555230251604, '_RYG': 16.17857142857143, 'YY_W': 444.00904725283243, 'GRGY': 140.32892318821007, 'G___': 111.45865644159763, 'GR__': 91.98653846153846, 'GGGG': 148.8203370642651, 'G_W_': 13.97619047619047, '_WWW': 49.37142857142858, 'YYRG': 13.01755106156194, 'WRR_': 67.70574974670724, 'Y__W': 0.9999999999999983, 'YYYY': 2.0, 'GYYY': 0.0, 'G___big': 1.5154544286133387e-14, 'YW__': -1.4210854715202004e-14})