Dynamic Network Classes¶
Table of Contents¶
- Using a snapshot representation
- Using an interval graph representation
- Using a Link Stream graph representation
If tnerwork library is not installed, you need to install it, for instance using the following command
[1]:
#%%capture #avoid printing output
#!pip install --upgrade git+https://github.com/Yquetzal/tnetwork.git
[2]:
%load_ext autoreload
%autoreload 2
import tnetwork as tn
## Creating simple dynamic graphs and accessing their properties We will represent a graph with similar properties using snapshots and interval graphs
Using a snapshot representation¶
DynGraphSN
is the class used to represent dynamic networks with snapshots (SN). The time at which each snapshot occurs is represented by an integer, which can be numbers in a sequence (1,2,3, etc.) or POSIX timestamps. A Frequency parameter allows to specify the time between each snapshot. By default, its value is 1. It is useful when there are missing snaphsots, e.g., like in SocioPatterns data, a snapshot every 20s, but many snapshots are empty.
[3]:
dg_sn = tn.DynGraphSN(frequency=1)
dg_sn.add_node_presence("a",1) #add node a in snapshot 1
dg_sn.add_nodes_presence_from(["a","b","c"],[2,3,4,5]) #add nodes a,b,c in snapshots 2 to 5
dg_sn.add_nodes_presence_from("d",[1,2,4,5]) #add node d in snapshots 1, 2, 4 and 5
dg_sn.add_interaction("a","b",2) #link a and b in snapshot 2
dg_sn.add_interaction("a","d",2) #link a and d in snapshot 2
dg_sn.add_interactions_from(("b","d"),[4,5]) #link b and d in snapshots 4 and 5
Using an interval graph representation.¶
DynGraphIG
is the class used to represent dynamic networks with Interval Graphs (IG). Nodes and edges are present during time intervals, that are closed on the left and open on the right, e.g., (0,10) corresponds to the interval [0,10[, e.g., the node or edge exist from time 0 (included) to time 10 (excluded).
Note the similarity between the functions used for snapshots
Both graphs are equivalent if the snapshots of dg_sn
have a duration of 1.
[4]:
dg_ig = tn.DynGraphIG()
dg_ig.add_node_presence("a",(1,2)) #add node a from time 1 to 2 (not included, time duration =2-1 = 1)
dg_ig.add_nodes_presence_from(["a","b","c"],(2,6)) # add nodes a,b,c from 2 to 6
dg_ig.add_nodes_presence_from("d",[(1,3),(4,6)]) #add node d from 1 to 3 and from 4 to 6
dg_ig.add_interaction("a","b",(2,3)) # link nodes a and b from 2 to 3
dg_ig.add_interaction("a","d",(2,3)) # link nodes a and d from 2 to 3
dg_ig.add_interactions_from(("b","d"),(4,6)) # link nodes b and d from 4 to 6
Using a Link Stream representation¶
DynGraphLS
is the class used to represent dynamic networks with Link Streams (LS). In a link stream, interactions are ponctual (no duration), but time is continuous. Nodes duration can be represented as intervals, or simply ignored. Note that if time is discrete, a link stream can represent data equivalent to a snapshot sequence: each edge of each snapshot is represented as an interaction at the corresponding time in the link stream. Discrete time can be handled using the frequency
parameter of a link stream. In this example, we create a link stream equivalent to the one represented with other types.
Note the similarity between the functions used.
[5]:
dg_ls = tn.DynGraphLS(frequency=1)
dg_ls.add_node_presence("a",(1,2)) #add node a from time 1 to 2 (not included, time duration =2-1 = 1)
dg_ls.add_nodes_presence_from(["a","b","c"],(2,6)) # add nodes a,b,c from 2 to 6
dg_ls.add_nodes_presence_from("d",[(1,3),(4,6)]) #add node d from 1 to 3 and from 4 to 6
dg_ls.add_interaction("a","b",2) #link a and b at time 2
dg_ls.add_interaction("a","d",2) #link b and d at time 2
dg_ls.add_interactions_from(("b","d"),[4,5]) #link b and d at times 4 and 5
('b', 'd')
Accessing functions¶
Using accessing functions, we can check that both graphs are very similar (Note that intervals are coded using the tnetwork.Intervals class, and are printed as [start,end[. Therefore, 2 snapshots of duration 1 at times 1 and 2 code a situation similar to an interval [1,3[
[6]:
print(dg_sn.graph_at_time(2).edges)
print(dg_ig.graph_at_time(2).edges)
print(dg_ls.graph_at_time(2).edges)
print(dg_sn.graph_at_time(4).edges)
print(dg_ig.graph_at_time(4).edges)
print(dg_ls.graph_at_time(4).edges)
[('a', 'b'), ('a', 'd')]
[('a', 'b'), ('a', 'd')]
[('a', 'b'), ('a', 'd')]
[('b', 'd')]
[('b', 'd')]
[('b', 'd')]
[7]:
print(dg_sn.node_presence())
print(dg_ig.node_presence())
print(dg_ls.node_presence())
{'a': [1, 2, 3, 4, 5], 'd': [1, 2, 4, 5], 'b': [2, 3, 4, 5], 'c': [2, 3, 4, 5]}
{'a': [1,6[ , 'b': [2,6[ , 'c': [2,6[ , 'd': [1,3[ [4,6[ }
{'a': [1,6[ , 'b': [2,6[ , 'c': [2,6[ , 'd': [1,3[ [4,6[ }
Visualization¶
We can use a basic visualization to compare nodes presence of both representation.
See the notebook on visualization to see more possibilities.
[8]:
plot = tn.plot_longitudinal(dg_sn,height=200)
plot = tn.plot_longitudinal(dg_ig,height=200)
plot = tn.plot_longitudinal(dg_ls,height=200)
/usr/local/lib/python3.7/site-packages/numpy/core/numeric.py:2327: FutureWarning: elementwise comparison failed; returning scalar instead, but in the future will perform elementwise comparison
return bool(asarray(a1 == a2).all())
It is also possible to plot the graph at any given time.
[9]:
plot = tn.plot_as_graph(dg_sn,ts=2,auto_show=True,width=300,height=300)
[10]:
plot = tn.plot_as_graph(dg_ig,ts=[1.5,2.5,3.3],auto_show=True,width=200,height=200)
Conversion between snapshots and interval graphs¶
We convert the snapshot representation into an interval graph representation, using a snapshot lenght of 1.
We check that both graphs are now similar
[11]:
converted_to_IG = dg_sn.to_DynGraphIG()
print(converted_to_IG.node_presence())
print(dg_ig.node_presence())
print(converted_to_IG.edge_presence())
print(dg_ig.edge_presence())
{'a': [1,6[ , 'd': [1,3[ [4,6[ , 'b': [2,6[ , 'c': [2,6[ }
{'a': [1,6[ , 'b': [2,6[ , 'c': [2,6[ , 'd': [1,3[ [4,6[ }
{frozenset({'b', 'a'}): [(2, 3)], frozenset({'d', 'a'}): [(2, 3)], frozenset({'d', 'b'}): [(4, 6)]}
{frozenset({'b', 'a'}): [(2, 3)], frozenset({'d', 'a'}): [(2, 3)], frozenset({'b', 'd'}): [(4, 6)]}
Reciprocally, we transform the interval graph into a snapshot representation and check the similarity
[12]:
converted_to_SN = dg_ig.to_DynGraphSN(slices=1)
print(converted_to_SN.node_presence())
print(dg_sn.node_presence())
print(converted_to_SN.edge_presence())
print(dg_sn.edge_presence())
{'a': [1, 2, 3, 4, 5], 'd': [1, 2, 4, 5], 'b': [2, 3, 4, 5], 'c': [2, 3, 4, 5]}
{'a': [1, 2, 3, 4, 5], 'd': [1, 2, 4, 5], 'b': [2, 3, 4, 5], 'c': [2, 3, 4, 5]}
{frozenset({'b', 'a'}): [2], frozenset({'d', 'a'}): [2], frozenset({'b', 'd'}): [4, 5]}
{frozenset({'b', 'a'}): [2], frozenset({'d', 'a'}): [2], frozenset({'b', 'd'}): [4, 5]}
[13]:
converted_to_LS = dg_sn.to_DynGraphLS()
print(converted_to_LS.node_presence())
print(dg_sn.node_presence())
print(converted_to_LS.edge_presence())
print(dg_sn.edge_presence())
{'a': [1,6[ , 'd': [1,3[ [4,6[ , 'b': [2,6[ , 'c': [2,6[ }
{'a': [1, 2, 3, 4, 5], 'd': [1, 2, 4, 5], 'b': [2, 3, 4, 5], 'c': [2, 3, 4, 5]}
{frozenset({'b', 'a'}): SortedSet([2]), frozenset({'d', 'a'}): SortedSet([2]), frozenset({'d', 'b'}): SortedSet([4, 5])}
{frozenset({'b', 'a'}): [2], frozenset({'d', 'a'}): [2], frozenset({'b', 'd'}): [4, 5]}
Aggregation/Slicing¶
Slicing¶
One can conserve only a chosen period using the slice function
[14]:
sliced_SN = dg_sn.slice(2,4) #Keep only the snapshots from 2 to 4
sliced_IG = dg_ig.slice(1.5,3.5) #keep only what happens between 1.5 and 3.5 in the interval graph
plot = tn.plot_longitudinal(sliced_SN,height=200)
plot = tn.plot_longitudinal(sliced_IG,height=200)
Creating cumulated graphs¶
It can be useful to create cumulated weighted graphs to summarize the presence of nodes and edges over a period
[15]:
import networkx as nx
%matplotlib inline
g_cumulated = dg_sn.cumulated_graph()
#Similarly for interval graphs:
#g_cumulated = dg_ig.cumulated_graph()
#Draw with node size and edge width propotional to weights in the cumulated graph
nx.draw_networkx(g_cumulated,node_size=[g_cumulated.nodes[n]['weight']*100 for n in g_cumulated.nodes], width = [g_cumulated[u][v]['weight'] for u,v in g_cumulated.edges])
Graphs can also be cumulated only over a specific period
[16]:
g_cumulated = dg_sn.cumulated_graph([1,2]) # create a static graph cumulating snapshots
g_cumulated = dg_ig.cumulated_graph((1,3))
Resampling¶
Sometimes, it is useful to study dynamic network with a lesser temporal granularity than the original data.
Several functions can be used to aggregate dynamic graphs, thus yielding snapshots covering larger periods.
To exemplify this usage, we use a dataset from the sociopatterns project (http://www.sociopatterns.org) that can be loaded in a single command in the chosen format
[17]:
sociopatterns = tn.graph_socioPatterns2012(tn.DynGraphSN)
graph will be loaded as: <class 'tnetwork.dyn_graph.dyn_graph_sn.DynGraphSN'>
For this original network loaded as a snapshot representation, we print the number of snapshots and the first and last dates (the dataset covers 9 days, including a week-end with no activity)
[18]:
from datetime import datetime
all_times = sociopatterns.snapshots_timesteps()
print("# snapshots:",len(all_times))
print("first date:",datetime.utcfromtimestamp(all_times[0])," laste date:",datetime.utcfromtimestamp(all_times[-1]))
# snapshots: 11273
first date: 2012-11-19 05:36:20 laste date: 2012-11-27 16:14:40
[19]:
#Be careful, the plot takes a few seconds to draw.
to_plot_SN = tn.plot_longitudinal(sociopatterns,height=500,sn_duration=20,to_datetime=True)
We then aggregate on fixed time periods using the aggregate_time_period
function. Although there are several ways to call this function, the simplest one is using a string such as “day”, “hour”, “month”, etc. Note how the beginning of the first snapshot is now on midnight of the day on which the first observation was made
[20]:
sociopatterns_Day = sociopatterns.aggregate_time_period("day")
[21]:
all_times = sociopatterns_Day.snapshots_timesteps()
print("# snapshots:",len(all_times))
print("first date:",datetime.utcfromtimestamp(all_times[0])," laste date:",datetime.utcfromtimestamp(all_times[-1]))
# snapshots: 7
first date: 2012-11-19 00:00:00 laste date: 2012-11-27 00:00:00
[22]:
to_plot_SN = tn.plot_longitudinal(sociopatterns_Day,height=800,to_datetime=True,sn_duration=24*60*60)
Another way to aggregate is to use sliding windows. In this example, we use non-overlapping windows of one hour, but it is possible to have other parameters, such as overlapping windows. Note how, this time, the first snapshot starts exactly at the time of the first observation in the original data
[23]:
sociopatterns_hour_window = sociopatterns.aggregate_sliding_window(bin_size=60*60)
[24]:
all_times = sociopatterns_hour_window.snapshots_timesteps()
print("# snapshots:",len(all_times))
print("first date:",datetime.utcfromtimestamp(all_times[0])," laste date:",datetime.utcfromtimestamp(all_times[-1]))
# snapshots: 86
first date: 2012-11-19 05:36:20 laste date: 2012-11-27 15:36:20
[25]:
plot =tn.plot_longitudinal(sociopatterns_hour_window,height=800,to_datetime=True,sn_duration=60*60)
[ ]:
[ ]: