Actual causation#
Download this page as a Jupyter notebook
This tutorial shows how to use PyPhi to evaluate actual causation — what caused what in a particular observed transition. This is a distinct framework from the integrated information \(\Phi\) of a system in a state; it asks, given that the system went from one state to the next, which events were the actual causes and actual effects of which. The formalism is described in
Albantakis L, Marshall W, Hoel E, Tononi G (2019). What Caused What? A Quantitative Account of Actual Causation Using Dynamical Causal Networks. Entropy, 21 (5), 459. https://doi.org/10.3390/e21050459
The tools are in pyphi.actual. Turning off the progress bars keeps the
output readable here; leave them on for real computations.
import pyphi
pyphi.config.progress_bars = False
from pyphi import actual, Direction
Actual causation needs no configuration: PyPhi’s defaults reproduce the 2019 paper.
The example network#
We work through the basic OR-AND network from Figure 1 of the paper. It is a
two-unit substrate: an OR gate (unit 0) and an AND gate (unit 1), each
receiving input from both.
substrate = pyphi.examples.actual_causation_substrate()
substrate
| Units | OR, AND |
|---|---|
| State space | binary |
| OR | AND | |
|---|---|---|
| OR | 1 | 1 |
| AND | 1 | 1 |
| state | OR | AND |
|---|---|---|
| (0,0) | 0.0 | 0.0 |
| (1,0) | 1.0 | 0.0 |
| (0,1) | 1.0 | 0.0 |
| (1,1) | 1.0 | 1.0 |
The transition probability matrix gives the dynamics — the probability that each unit turns on given the current state of the system:
substrate.tpm
| Units | 2 |
|---|---|
| States | 4 |
| state | OR | AND |
|---|---|---|
| (0,0) | 0.0 | 0.0 |
| (1,0) | 1.0 | 0.0 |
| (0,1) | 1.0 | 0.0 |
| (1,1) | 1.0 | 1.0 |
Name the two units for readability:
OR, AND = 0, 1
The transition of interest#
We observe the whole substrate at time \(t-1\) and again at time \(t\), with OR
on and AND off in both observations:
X = Y = (OR, AND)
X_state = Y_state = (1, 0)
The Transition object is the core of every actual
causation computation. To build one, pass the substrate, its state at \(t-1\) and
at \(t\), and the units of interest on the cause side (\(t-1\)) and the effect side
(\(t\)). The state given must be the state of the entire substrate, not just the
units in the transition. The transition must also satisfy the realization
requirement: if the effect-side occurrence has zero probability given the
state at \(t-1\), the transition cannot have occurred, and construction raises
TransitionUnreachableError.
transition = actual.Transition(substrate, X_state, Y_state, X, Y)
transition
Transition([OR,AND] ━━▶ [OR,AND])
Cause and effect repertoires#
Cause and effect repertoires can be read off the transition. For example, the right side of Figure 2B shows how the occurrence \(\{OR = 1\}\) at \(t-1\) constrains the probability distribution over the purview \(\{OR, AND\}\) at \(t\):
transition.effect_repertoire((OR,), (OR, AND))
array([[0. , 0. ],
[0.5, 0.5]])
Similarly, Figure 2C shows how the occurrence \(\{OR, AND = 10\}\) at \(t\) constrains the purview \(\{OR\}\) at \(t-1\):
transition.cause_repertoire((OR, AND), (OR,))
array([[0.5],
[0.5]])
Note
In all Transition methods the constraining occurrence is
passed as the mechanism argument and the constrained occurrence is the
purview argument, mirroring the terminology used elsewhere in PyPhi.
Cause and effect ratios#
The transition also computes cause and effect ratios — the log-ratio by which an occurrence raises or lowers the probability of a purview state. The effect ratio of \(\{OR = 1\}\) at \(t-1\) constraining \(\{OR\}\) at \(t\) (Figure 3A) is positive:
round(transition.effect_ratio((OR,), (OR,)), 6)
0.415037
The effect ratio of \(\{OR = 1\}\) constraining \(\{AND\}\) is negative — the
occurrence makes the observed AND outcome less likely:
round(transition.effect_ratio((OR,), (AND,)), 6)
-0.584963
And the cause ratio of \(\{OR = 1\}\) at \(t\) constraining \(\{OR, AND\}\) at \(t-1\) (Figure 3B) is:
round(transition.cause_ratio((OR,), (OR, AND)), 6)
0.415037
Finding the minimum information partition#
To find the irreducible cause or effect ratio \(\alpha\) of a particular pair of
occurrences (Figure 3C), use find_mip(). Consider
the candidate effect link \(\{OR, AND\} \rightarrow \{OR, AND\}\):
link = transition.find_mip(Direction.EFFECT, (OR, AND), (OR, AND))
This returns an object describing the minimum information partition. This particular link is reducible, as its \(\alpha\) is zero:
round(link.alpha, 6)
0.0
The partition attribute shows the partition that reduces it:
link.partition
OR/OR × AND/AND × ∅/∅
| Connections cut | 2 |
|---|
| OR | AND | |
|---|---|---|
| OR | · | ✕ |
| AND | ✕ | · |
The candidate cause link \(\{OR, AND\} \rightarrow \{OR, AND\}\) (Figure 3D), by contrast, is irreducible, with positive \(\alpha\):
cause_link = transition.find_mip(Direction.CAUSE, (OR, AND), (OR, AND))
round(cause_link.alpha, 6)
0.169925
To find the actual cause or actual effect of an occurrence — the maximally
irreducible link over it — use
find_actual_cause() or
find_actual_effect():
transition.find_actual_cause((OR, AND))
| α | 0.169925 |
|---|---|
| Direction | CAUSE |
| Mechanism | [OR,AND] |
| Purview | [OR,AND] |
Accounts#
The complete causal account of the transition — every irreducible cause and
effect link — is computed with account():
account = actual.account(transition)
account
| Causal links | 5 |
|---|---|
| Σα | 1.83007 |
| Direction | Mechanism | Purview | α |
|---|---|---|---|
| CAUSE | [OR] | [OR] | 0.415037 |
| CAUSE | [AND] | [AND] | 0.415037 |
| CAUSE | [OR,AND] | [OR,AND] | 0.169925 |
| EFFECT | [OR] | [OR] | 0.415037 |
| EFFECT | [AND] | [AND] | 0.415037 |
These are the causal links shown in Figure 4. The account behaves like a sequence of links:
len(account)
5
Irreducible accounts#
Whether the account of the transition is itself irreducible — whether the
transition, taken as a whole, is more than the sum of its parts — is evaluated
with sia() (system irreducibility analysis):
sia = actual.sia(transition)
round(sia.alpha, 6)
0.169925
As shown in Figure 4, the second-order occurrence \(\{OR, AND = 10\}\) is destroyed by the minimum information partition, so the partitioned account has only the four first-order links:
sia.partitioned_account
| Causal links | 4 |
|---|---|
| Σα | 1.66015 |
| Direction | Mechanism | Purview | α |
|---|---|---|---|
| CAUSE | [OR] | [OR] | 0.415037 |
| CAUSE | [AND] | [AND] | 0.415037 |
| EFFECT | [OR] | [OR] | 0.415037 |
| EFFECT | [AND] | [AND] | 0.415037 |
The partition that achieves this is available on the result:
sia.partition
DirectedJointPartition CAUSE
| Connections cut | 2 |
|---|
| OR | AND | |
|---|---|---|
| OR | · | ✕ |
| AND | ✕ | · |
The causal nexus#
The analysis so far fixed one transition of interest. To search over all
possible transitions between the observed states and keep the irreducible ones,
use nexus(). Each result is a system irreducibility analysis
whose cause and effect units identify the transition it describes:
labels = substrate.node_labels
for candidate in actual.nexus(substrate, X_state, Y_state):
cause = ",".join(labels[i] for i in candidate.cause_indices)
effect = ",".join(labels[i] for i in candidate.effect_indices)
print(f"[{cause}] -> [{effect}] big_alpha = {round(candidate.alpha, 6)}")
[OR] -> [OR] big_alpha = 2.0
[AND] -> [AND] big_alpha = 2.0
[OR,AND] -> [OR,AND] big_alpha = 0.169925
causal_nexus() returns the single maximally irreducible
transition among these:
cn = actual.causal_nexus(substrate, X_state, Y_state)
round(cn.alpha, 6)
2.0
cause = ",".join(labels[i] for i in cn.cause_indices)
effect = ",".join(labels[i] for i in cn.effect_indices)
print(f"[{cause}] -> [{effect}]")
[OR] -> [OR]
The single-unit transition \(\{OR\} \rightarrow \{OR\}\) is the causal nexus of this observed state change.
Where to go next#
Causal reductionism and the frog applies actual causation to the question of which level of description carries the causal power.
Build a substrate builds the substrates these transitions run over.