Pairwise direction (ANM / PNL)#
ANM and PNL take two variables and test which one causes the other. Each fits a model in both
directions and tests whether the estimated noise is independent of the proposed cause, which holds
only in the causal direction. ANM fits an additive noise model, y = f(x) + e; PNL fits a
post-nonlinear one, y = f2(f1(x) + e). Both return a two-node DAG and both directions’ p-values.
Start#
Choose a CPU runtime and run this cell first. In Colab it installs andrey-core==0.9.0 from PyPI;
local kernels use the installed environment.
import os
import sys
IN_COLAB = "google.colab" in sys.modules
if IN_COLAB:
os.environ["ANDREY_DEVICE"] = "cpu"
!pip install --quiet "andrey-core[viz]==0.9.0" pandas
import pandas as pd
from IPython.display import display
import andrey
import andrey.viz
from andrey.data import sample_scm
Data#
sample_scm draws a cause-effect pair: two variables and one edge, in a direction the seed draws.
Here the effect is a post-nonlinear function of the cause with Laplace noise, 500 samples.
seed = 3
pair = sample_scm(
graph="erdos_renyi",
functional="post_nonlinear",
noise="laplace",
d=2,
n=500,
seed=seed,
scale="standardize",
density=1.0,
)
X, truth = pair.data, pair.graph
print(f"{X.shape[0]} observations of 2 variables, seed {seed}")
display(andrey.viz.draw(truth))
500 observations of 2 variables, seed 3
Fit ANM and PNL#
Column 0 is x and column 1 is y. pval_forward tests x -> y and pval_backward tests
y -> x; by default the edge points in the direction with the larger p-value. Pass a significance
level such as alpha=0.05 to draw an edge only when exactly one direction’s p-value is above it.
print(andrey.anm(X), end="\n\n")
print(andrey.pnl(X))
ANM dag | 2 nodes | 1 edges (1 directed)
1 -> 0
pval_forward: 9.834e-13
pval_backward: 0.0021
stat_forward: 5.487
stat_backward: 2.195
PNL dag | 2 nodes | 1 edges (1 directed)
1 -> 0
pval_forward: 0.0001507
pval_backward: 0.01781
stat_forward: 2.71
stat_backward: 1.811
Compare over ten pairs#
Each method runs on the pairs its model describes: ANM on additive-noise pairs with Gaussian noise, and PNL on post-nonlinear pairs with Laplace noise. One pair can favor either answer, so the table covers ten seeds.
models = {
"ANM": ("additive_noise", "gaussian", lambda x: andrey.anm(x)),
"PNL": ("post_nonlinear", "laplace", lambda x: andrey.pnl(x)),
}
rows = {}
for s in range(10):
rows[s] = {}
for name, (functional, noise, fit) in models.items():
draw = sample_scm(
graph="erdos_renyi",
functional=functional,
noise=noise,
d=2,
n=500,
seed=s,
scale="standardize",
density=1.0,
)
found = fit(draw.data).structure.oriented_edges()
if found == draw.graph.oriented_edges():
rows[s][name] = "true direction"
else:
rows[s][name] = "reversed" if found else "no edge"
table = pd.DataFrame(rows).T
table.index.name = "seed"
table
| ANM | PNL | |
|---|---|---|
| seed | ||
| 0 | true direction | true direction |
| 1 | true direction | true direction |
| 2 | true direction | true direction |
| 3 | true direction | true direction |
| 4 | true direction | true direction |
| 5 | reversed | true direction |
| 6 | true direction | true direction |
| 7 | true direction | true direction |
| 8 | reversed | true direction |
| 9 | true direction | true direction |
The 0.4.0 release card compares both methods with causal-learn on pairs of several sizes.
Use your own data#
Pass a two-column table, such as a DataFrame whose column names become the graph’s labels, or
change seed and n above and rerun.