--- name: 20261015-andrey-0-4-0 description: "Pairwise direction in Andrey 0.4.0: ANM and PNL test which of two variables causes the other, and both are now supported." meta: type: post blogpost: true date: 2026-10-15 author: Shu Wan tags: [Release, Pairwise direction] version: 0.4.0 cover: hourglass --- # Pairwise direction Does `x` cause `y`, or does `y` cause `x`? With only two variables, independence tests cannot tell: both directions fit the data equally well. ANM and PNL instead use the shape of the relationship to infer direction. This release adds ANM. As of 0.4.0, we support both methods. We explain how two variables can reveal their direction, run both tests on two pairs, and report benchmark results. ## Direction from two variables PC and GES can orient an edge only through a third variable, such as a common effect. A pair has no third variable, so `x -> y` and `y -> x` explain the data equally well. These methods leave the edge undirected. A model of the mechanism can break the tie. Suppose `y = f(x) + e`, where the noise `e` is independent of `x`. Regress `y` on `x`, and the residual looks independent of `x`. Regress `x` on `y`, and in general no residual is independent of `y`, unless `f` is linear and the noise is Gaussian. Each test fits both directions and checks whether the residual is independent of the proposed cause. It uses HSIC, a kernel test that detects nonlinear dependence as well as correlation. The direction whose residual looks independent is the causal one. The methods fit different models: - **ANM** (additive noise model) fits `y = f(x) + e` by Gaussian-process regression. The direction is generally identifiable when `f` is nonlinear or the noise is non-Gaussian. - **PNL** (post-nonlinear model) fits `y = f2(f1(x) + e)`, where `f2` is invertible. This is an additive noise model observed through a distortion, such as a sensor that saturates. ANM is the case where `f2` changes nothing. PNL fits each transform as a sum of smooth basis functions. ## Supported methods Before each release, we benchmark each supported method against another package and check its results on known data. ANM is new in this release. PNL was experimental until now. | Method | Model | Returns | |---|---|---| | [ANM](../docs/api/andrey.anm.rst) | `y = f(x) + e` | A two-node DAG, both p-values, and both test statistics | | [PNL](../docs/api/andrey.pnl.rst) | `y = f2(f1(x) + e)` | A two-node DAG, both p-values, and both test statistics | Choose a method for your data: - **ANM** when the effect is a smooth function of the cause plus independent noise. Its regression time grows with the cube of the number of rows, so it suits up to a few thousand rows. - **PNL** when the effect may pass through a distortion or you have more rows. In our benchmarks, its median fit took under 2 s at 5,000 rows. Both assume that no hidden common cause links the pair. When one is plausible, use the methods in the [0.2.0 post](20261012-andrey-0-2-0.md). ## Reading the result Each test returns a two-node DAG and four values in `metadata`: - `pval_forward` and `pval_backward`: the p-values of the independence tests for `x -> y` and `y -> x`. A large p-value means the residual looks independent of the proposed cause, so that direction fits. - `stat_forward` and `stat_backward`: the two HSIC statistics, each divided by its mean under independence. Smaller values mean weaker dependence. By default, the edge points in the direction with the larger p-value. On a large sample, both p-values can round to zero. The smaller statistic then decides. This rule always selects a direction, even for unrelated variables. With a significance level such as `alpha=0.05`, a direction fits when its p-value exceeds the level. The test draws an edge only when exactly one direction fits. ## Example pairs In both pairs, `x` causes `y`, and `e` is Laplace noise: - **Additive**: `y = x + 0.3 * x**3 + 0.5 * e`. The noise adds to a function of `x`, so both models hold. - **Saturating**: a sensor reads `y = tanh(x + 0.5 * e)`. The noise passes through the saturation, so only the post-nonlinear model holds. ```{literalinclude} 0.4.0/example.py :language: python :caption: example.py ``` ```{raw} html :file: 0.4.0/pair.html ``` On the additive pair, ANM and PNL both select the true direction, `x -> y`, and reject `y -> x`. On the saturating pair, the distortion breaks ANM, and PNL models it: - ANM rejects both directions. Both p-values are near zero because the saturation is not additive noise. Its default rule still selects a direction, here the reversed one, `y -> x`. With `alpha=0.05`, ANM draws no edge (the last result below). - PNL models the saturation as its outer transform. Its p-value for `x -> y` is large. Its p-value for `y -> x` is near zero, so it selects the true direction. The script prints each result in full: ```{literalinclude} 0.4.0/example-output.txt :language: text :caption: example.py ``` ## Benchmark results We compared both methods with causal-learn, a Python package that implements them, on simulated cause-effect pairs. We tested ANM on additive-noise pairs and PNL on post-nonlinear pairs. | Method | Compared with | Andrey's speedup | Direction | |---|---|---|---| | ANM | causal-learn | about 2x | the same as causal-learn's on every pair | | PNL | causal-learn | up to 1,470x | true on more pairs than causal-learn's | The [0.4.0 release card](../docs/benchmarks/0.4.0.md) reports every measurement, including the sizes, machine, and settings. ```{raw} html :file: 0.4.0/speedup.html ``` Our ANM implementation fits the same Gaussian process and uses the same independence test as causal-learn, so both return the same graph on every pair. The PNL implementations differ more in how they fit and test each direction. Ours selects the true direction on 94% of pairs; causal-learn's selects it on 45%. ```{raw} html :file: 0.4.0/accuracy.html ``` At 200 rows, both methods select the reversed direction on a quarter of the pairs. A direction test selects a direction on every pair, right or wrong. Check both p-values before you trust it. ## Other highlights - **Statistics break ties.** On a large sample, both p-values can round to zero. PNL now selects the direction with the smaller HSIC statistic, relative to its mean under independence, instead of drawing no edge. ANM uses the same rule. Both statistics are in `metadata`. - **ANM in any units.** ANM standardizes each column first, so its answer does not depend on either variable's units. For details, read the [changelog](../docs/changelog.md). ## Try it ```{code-block} shell :class: window pip install -U andrey-core ``` The [pairwise notebook](../docs/examples/pairwise) runs both tests on simulated pairs. You can open it in Colab, then pass your own two-column table to `andrey.anm` or `andrey.pnl`. ## Help us build it Andrey is in alpha. The API may change between releases. When you try it on your own data, [tell us what breaks](https://github.com/Abel-ai-lab/andrey/issues/new) or which method you need. We also welcome pull requests. The [contributing guide](https://github.com/Abel-ai-lab/andrey/blob/main/CONTRIBUTING.md) explains how you can set up. --- - [Getting started](../docs/guides/getting-started.md) - [Examples](../docs/examples/index.md) - [Release cards](../docs/benchmarks.md) - [Changelog](../docs/changelog.md) - [GitHub](https://github.com/Abel-ai-lab/andrey)