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