Temporal Graphs
In PARCS, you can define a temporal graph where the nodes and relations are expanded along the
time axis. This is possible by only making a few small modifications to the current pipeline.
Let’s imagine the following scenario to explain this feature:
The goal is to model the interaction between the blood pressure variable BP and the drug
variable Drug.
Initially, we let the blood pressure be dependent on gaussian noise Noise, while the drug
depends on the age variable Age of the patient as well as the blood pressure. This setup
can be modeled by the non-temporal version of PARCS.
1Age: uniform(mu_=10, diff_=20)
2BP: normal(mu_=Noise, sigma_=1)
3Drug: bernoulli(p_=BPAge)
4Noise: normal(mu_=0, sigma_=1)
5
6Age->Drug: identity()
7Noise->BP: identity()
8BP->Drug: identity()
Temporal Syntax in the Graph Description
Now we discuss the temporal expansion: every temporal component is marked by a subscript where _{t} indicates the current timestep.
1# BLVs
2Age: uniform(mu_=10, diff_=20)
3
4# temporal recursive
5BP_{0}: uniform(mu_=10, diff_=2)
6BP_{t}: normal(mu_=BP_{t-1}+Noise_{t}-Drug_{t-2}, sigma_=1)
7Drug_{-1}: constant(0)
8Drug_{0}: constant(0)
9Drug_{t}: bernoulli(p_=BP_{t}+Drug_{t-1}+Age), correction[]
10
11# temporal non-recursive
12Noise_{t}: normal(mu_=0, sigma_=1)
13
14# interactions BLVs to temporal recursive
15Age->Drug_{t}: identity()
16
17# interactions temporal non-recursive to temporal recursive
18Noise_{t}->BP_{t}: identity()
19
20# interactions temporal recursive to temporal recursive
21# 1) update of same node from previous timestep
22BP_{t-1}->BP_{t}: identity()
23Drug_{t-1}->Drug_{t}: identity()
24# 2) update of another node from same timestep
25BP_{t}->Drug_{t}: identity()
26# 3) update of another node from previous timesteps
27Drug_{t-2}->BP_{t}: identity()
Note, how the Age node lacks the _{t} subscript; it is considered
a baseline variable (BLV) that is created at the beginning and does not change. Additionally, we make a distinction between
temporal recursive and temporal non-recursive variables. Both BP and Drug belong to the former class because
they depend on nodes from previous timesteps. On the other hand, Noise is not conditioned on a previous inputs and
thus is considered temporal non-recursive.
In general, variables can exhibit long-range dependencies that go further back than just the previous timestep.
Therefore, we add another temporal dependency from Drug to BP, where the effect is delayed by two timesteps.
For all temporal variables the first timestep of the simulation is indexed as 1.
All temporal recursive definitions require a set of initial values that have to be specified.
In the case of Drug, for example, one has to specify the values for Drug_-1 and
Drug_0 because we reference Drug_-1 in the definition of BP_1.
Parsing and Calling Temporal Outlines
To actually parse your temporal outline, all you need to do is to instantiate the TemporalDescription class with a temporal outline and a n_timesteps argument.
1from pyparcs.temporal import TemporalDescription
2from pyparcs import Graph
3import numpy as np
4np.random.seed(42)
5
6description = TemporalDescription('temporal_graph_description.yml', n_timesteps=3)
7graph = Graph(description)
8
9samples, _ = graph.sample(size=5)
10print(samples)
11# Age BP_0 Drug_0 Drug_neg1 ... Drug_1 BP_3 Drug_2 Drug_3
12# 0 4.023505 9.160127 0.0 0.0 ... 1.0 4.571410 1.0 1.0
13# 1 15.981643 10.016958 0.0 0.0 ... 1.0 10.498443 1.0 1.0
14# 2 0.496344 10.670488 0.0 0.0 ... 1.0 12.019636 1.0 1.0
15# 3 12.139462 9.377000 0.0 0.0 ... 1.0 9.942749 1.0 1.0
16# 4 11.713087 9.016991 0.0 0.0 ... 1.0 7.224285 1.0 1.0
17#
18# [5 rows x 13 columns]
Deterministic Nodes in Temporal Graphs
As introduced in this section, deterministic nodes are declared by a custom user-defined python function. What if the deterministic node is temporal? e.g. instead of having a function like \(C = A^2 + B\), we have \(C_t = A_{t-2}^2 + B_{t}\)…
Writing the custom function for a deterministic node is almost the same as writing a static function, with a small modification. Below is the temporal description file:
1# nodes
2A: uniform(mu_=1, diff_=2)
3B_{t}: normal(mu_=B_{t-1}, sigma_=1)
4C_{t}: deterministic(customs.py, temporal_custom_func)
5
6# initial values
7B_{-1}: constant(0)
8B_{0}: constant(2)
9C_{0}: constant(1)
10
11# edges
12A->B_{t}: identity()
13B_{t-1}->B_{t}: identity()
14A->C_{t}: identity()
15B_{t-2}->C_{t}: identity()
16C_{t-1}->C_{t}: identity()
Firstly, the data columns in the custom function is just as we must expect it, e.g., data['A_{t-1}'] or data['B_{t}']. Secondly, you must use the temporal decorator for your custom function. An example is depicted below:
1from pyparcs.temporal import temporal_deterministic
2
3
4@temporal_deterministic(['B', 'C'], 't-2')
5def temporal_custom_func(data):
6 return data['A'] + data['B_{t-2}'] + data['C_{t-1}']
In this code, the temporal_deterministic decorator receives two inputs:
list of all temporal nodes in the function;
the earliest time index that appears in the function.
According to this code, the nodes C_1, C_2, C_3 will be calculated as
Finally, here is the PARCS main code:
1from pyparcs import Graph
2from pyparcs.temporal import TemporalDescription
3import numpy as np
4np.random.seed(42)
5
6description = TemporalDescription('description.yml', n_timesteps=3)
7graph = Graph(description)
8samples, _ = graph.sample(size=5)
9
10print(samples[['A', 'B_1', 'B_2', 'B_3', 'C_1', 'C_2', 'C_3']])
11# A B_1 B_2 B_3 C_1 C_2 C_3
12# 0 0.749080 2.618855 2.868731 1.857774 1.749080 4.498160 7.866095
13# 1 0.041169 2.963863 2.165531 1.257099 1.041169 3.082338 6.087370
14# 2 1.223706 1.452870 1.111365 1.001025 2.223706 5.447412 8.123987
15# 3 1.215090 0.486305 2.120447 3.940599 2.215090 5.430179 7.131574
16# 4 0.244076 0.180118 1.516700 0.869589 1.244076 3.488153 3.912348
17
18print((samples['C_1']).equals(samples['A'] + samples['B_neg1'] + samples['C_0']))
19print((samples['C_2']).equals(samples['A'] + samples['B_0'] + samples['C_1']))
20print((samples['C_3']).equals(samples['A'] + samples['B_1'] + samples['C_2']))
21# True
22# True
23# True