--- name: 20261103-andrey-0-13-0 description: "What a functional causal model is, why a curve shows which way an effect runs, and the methods Andrey has for it: LiNGAM, ANM, PNL, CAM, and RESIT." meta: type: post blogpost: true date: 2026-11-03 author: Shu Wan tags: [Release, Functional causal models] version: 0.13.0 cover: gears --- # Functional causal models ## TL;DR - **What it is:** a functional causal model writes each variable as a function of its causes plus independent noise. When the function or the noise has the right shape, only the true direction fits the data. - **What Andrey has:** DirectLiNGAM and ICA-LiNGAM for linear effects, ANM and PNL for a pairwise relationship, and CAM and RESIT for nonlinear effects across a whole graph, with experimental CAM-UV for hidden variables. - **As accurate as the authors' code, and faster:** CAM makes no more errors than CAM 1.0 in any of 16 settings and is 6.5 to 14.7 times faster; RESIT makes no more errors than its authors' code in any of 12 and is 3.2 to 4.6 times faster. --- Most causal discovery methods can tell which variables are connected, but not always which way. Functional causal models read the direction from the shape of an effect, even when the effect is a curve. ## Structural causal models A structural causal model writes each variable as a function of its parents (its direct causes) and a noise term of its own: $$x_j = f_j(\mathrm{pa}_j, e_j), \qquad j = 1, \dots, d,$$ with the noise terms independent of each other. The graph draws an arrow from each parent to its child, and the data are draws of all the $x_j$. When nothing restricts the functions $f_j$ or the noise, the data reveal only which variables are independent given which others. Graphs that differ in the direction of some edges imply the same independences, so PC and GES, which read independences, return those edges undirected. ## Functional causal models Take the smallest graph, $x \to y$, with $y = f(x) + e$. One way to find its direction is to fit each variable from the other and test whether what is left, the residual, is independent of the variable it was fit on. In the true direction, the residual is the noise $e$, which is independent of $x$. The figure runs this test on three pairs, (a) to (c), one per column. The top row shows the data, the middle row the residual of a fit of $y$ from $x$, and the bottom row the residual of a fit of $x$ from $y$. Under each residual is the verdict of a kernel independence test (HSIC). ```{raw} html :file: 0.13.0/direction.html ``` In (a), $y = 0.8\,x + e$ with $x$ and $e$ both Gaussian, and the data form a symmetric Gaussian cloud. Both residuals are shapeless clouds, and the test finds both independent. The reverse model $x = b\,y + e'$ fits the data as well as the true one, so no method can tell the direction from these data. In (b), the line is the same, but $x$ and $e$ are uniform. The residual of $y$ from $x$ is still shapeless, but the residual of $x$ from $y$ forms a diamond: it spreads widest at middle values of $y$ and narrows at the ends, so it depends on $y$. Only the true direction fits. This is what LiNGAM methods use: linear effects with non-Gaussian noise. In (c), $y = 0.5\,x^3 - x + e$, with Gaussian noise again. The residual of $y$ from $x$ is shapeless, but the residual of $x$ from $y$ follows the bends of the curve, and the test finds it dependent. This is what CAM and RESIT use: nonlinear effects. A functional causal model restricts the form of each $f_j$ and its noise so that, as in (b) and (c), only the true direction leaves a residual independent of the cause ([Hoyer et al., 2008](https://papers.nips.cc/paper/3548-nonlinear-causal-discovery-with-additive-noise-models)). Each functional causal model in Andrey assumes its own form of $f_j$: | Method | Model of each variable | The direction comes from | Output | |---|---|---|---| | DirectLiNGAM, ICA-LiNGAM | $x_j = \sum_{k \in \mathrm{pa}_j} b_{jk}\, x_k + e_j$, with $e_j$ non-Gaussian | the shape of the noise | DAG | | ANM | $y = f(x) + e$ | a nonlinear $f$ | pairwise relationship | | PNL | $y = g(f(x) + e)$, with $g$ invertible | a nonlinear $f$ and $g$ | pairwise relationship | | CAM | $x_j = \sum_{k \in \mathrm{pa}_j} f_{jk}(x_k) + e_j$ | a nonlinear $f_{jk}$ | DAG and causal order | | RESIT | $x_j = f_j(\mathrm{pa}_j) + e_j$, with $e_j$ of any distribution | a nonlinear $f_j$ | DAG and causal order | | CAM-UV, experimental | CAM's model, with some variables hidden | a nonlinear $f_{jk}$ | PAG, `o-o` where a hidden variable joins a pair | The LiNGAM methods, CAM, and RESIT form one family in Andrey, "Functional causal models"; ANM and PNL, which take one pair at a time, are under "Pairwise". **CAM** ([Bühlmann, Peters, and Ernest, 2014](https://doi.org/10.1214/14-AOS1260)) makes each effect a sum of one smooth function per parent and scores a graph by the Gaussian likelihood of its residuals. It picks each variable's possible parents, adds the edges that most reduce the residual variance, and prunes the smooth terms that are not significant. A score is cheap to compute, so CAM reaches 100 variables. **RESIT** ([Peters, Mooij, Janzing, and Schölkopf, 2014](https://jmlr.org/papers/v15/peters14a.html)) assumes nothing about the noise distribution. It builds the causal order from the last variable up, each time taking the variable whose residual a kernel test finds most independent of all the remaining variables. It then drops each parent whose removal keeps the residual independent. Andrey fits each $f_j$ as a sum of smooth terms, as CAM does. Each test reads all the remaining variables at once, so RESIT suits a handful of them. **CAM-UV** ([Maeda and Shimizu, 2021](https://proceedings.mlr.press/v161/maeda21a.html)) keeps CAM's model and allows hidden variables. Where a hidden common cause or a hidden step on a causal path joins two variables, no regression can tell their relation, and it marks the pair `o-o` instead of guessing. ## A nonlinear example We simulate 1,000 rows for ten variables on a known graph, each a nonlinear function of its parents plus Gaussian noise. We ask four methods for the graph. ```{literalinclude} 0.13.0/example.py :language: python :caption: example.py ``` ```{literalinclude} 0.13.0/example-output.txt :language: text :caption: example.py ``` On this draw, PC with a kernel test finds the right connections but leaves two of them undirected. DirectLiNGAM orients every edge, but its linear model does not fit, and it makes more errors than the empty graph. CAM recovers the true graph, every direction included. ## Checked against the authors' code We compared each method with the reference code on simulated data from the model it assumes. CAM, RESIT, and CAM-UV make no more errors than their reference code, and run faster: - CAM: CAM 1.0, 6.5 to 14.7 times faster. - RESIT: the authors' R code, 3.2 to 4.6 times faster; lingam, 2.1 to 2.9 times faster, with fewer errors in 11 of 12 settings. - CAM-UV: lingam, 2.7 to 3.7 times faster; causal-learn, 7.5 to 16 times faster. The [release card](../docs/benchmarks/0.13.0.md) has every comparison. ## Simulating functional causal models `andrey.data.sample_scm` draws data from these models, with `functional` naming the form of each $f_j$: `"linear"`, `"additive_noise"` (the example above), `"post_nonlinear"`, and `"location_scale"`, whose noise scale depends on the parents. The last breaks the model of CAM and RESIT. We use it to see how they fail when their model does not hold. ## Limits **The model must fit.** CAM and RESIT assume nonlinear effects with additive noise and no hidden common causes. With linear effects and Gaussian noise, the data identify only the equivalence class; noise whose scale depends on the parents can reverse edges. **RESIT is for small graphs.** Its accuracy claim holds up to 10 variables. At 20, on Gaussian noise, no implementation we measured clearly beats the empty graph. **Many hidden variables defeat CAM-UV.** Where hidden variables carry most of the edges, neither CAM-UV nor the reference implementations make fewer errors than the empty graph, and all of them mark few of the pairs a hidden variable joins. ## Try it ```{code-block} shell :class: window pip install -U andrey-core ``` The [functional causal models example](../docs/examples/functional.ipynb) runs CAM and RESIT on simulated data. ## 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 the development environment. --- - [Getting started](../docs/guides/getting-started.md) - [Release cards](../docs/benchmarks.md) - [Changelog](../docs/changelog.md) - [GitHub](https://github.com/Abel-ai-lab/andrey)