Hidden common causes#

A hidden common cause, or latent confounder, is an unmeasured variable that affects two measured variables. This release adds RCD and BottomUpParceLiNGAM as supported methods. With FCI, they cover data with hidden common causes. Below, we run all three on one small dataset and report benchmark results. Other updates make small fits several times faster.
Why hidden causes matter#
Many causal discovery methods, including PC, GES, and DirectLiNGAM, assume causal sufficiency: every common cause of two measured variables is measured too. Real data often violate this assumption. A hidden common cause can make two variables dependent even without an edge between them. A method that assumes causal sufficiency can then draw an edge that is not there. Methods that allow hidden common causes can mark the pair or leave the edge’s ends unresolved.
Methods#
We benchmark each supported method against another package and check its results on known graphs before each release. RCD and BottomUpParceLiNGAM are new in this release.
Method |
Assumes |
|---|---|
Linear relations with roughly Gaussian noise, through its Fisher-z test |
|
Linear relations with non-Gaussian noise |
|
Linear relations with non-Gaussian noise |
Reading a PAG#
All three return a partial ancestral graph (PAG) over the variables you measured. Each end of an edge carries a tail, an arrowhead, or a circle. A circle means that end is unresolved.
x -> y:xcausesy.x <-> y: a hidden variable linksxandy, such as a common cause.x o-> y:ydoes not causex; whetherxcausesyis unresolved.x o-o y: both ends are unresolved.
FCI marks an end with a tail or an arrowhead only when every graph consistent with the
data’s conditional independences agrees on that mark. RCD and BottomUpParceLiNGAM use
the same marks for their findings: -> for a direct cause and <-> for a pair linked by a hidden
variable. BottomUpParceLiNGAM also uses o-o between variables it cannot order.
See Graph types for details.
Example#
Here h causes x0 and x1 but is not in the data. Also, x0 causes x3, and x1 causes
x2. The noise is uniform, so it is not Gaussian.
import numpy as np
import pandas as pd
import andrey
# h is a common cause of x0 and x1, and it is not in the data.
rng = np.random.default_rng(0)
h, e0, e1, e2, e3 = rng.uniform(-1, 1, (5, 2000))
x0, x1 = 1.5 * h + e0, 1.5 * h + e1
df = pd.DataFrame({"x0": x0, "x1": x1, "x2": 0.6 * x1 + e2, "x3": 0.6 * x0 + e3})
methods = (andrey.fci, andrey.rcd, andrey.bottom_up_parce_lingam)
print(*(method(df) for method in methods), sep="\n\n")
# For comparison: DirectLiNGAM assumes every common cause is in the data.
print()
print(andrey.direct_lingam(df))
What each method returns
RCD returns the true graph:
x0 <-> x1for the pair linked byh, and both direct edges.BottomUpParceLiNGAM finds both direct edges. It cannot order
x0andx1, which shareh, so it groups them in one block joined byo-o. Incausal_order, this block appears as the nested list[0, 1].FCI finds the three edges but leaves every end a circle. Its orientations rely on conditional independences. These four variables have the same independences as a graph with no hidden cause and no collider (a variable that two arrows point into), such as
x3 <- x0 -> x1 -> x2, so the independences cannot settle any mark. Given an observed cause ofx0and another ofx1, FCI can mark the pairx0 <-> x1.
RCD and BottomUpParceLiNGAM also test whether regression residuals are independent.
With non-Gaussian noise, this test settles the directions in this example. RCD and
BottomUpParceLiNGAM also estimate the edge weights: 0.598 and 0.61, compared with the true 0.6.
DirectLiNGAM assumes causal sufficiency. On the same data, it returns x0 -> x1 (the last graph
above), an edge absent from the true graph.
The script prints each result in full:
FCI pag | 4 nodes | 3 edges (3 circle)
x0 o-o x1
x0 o-o x3
x1 o-o x2
RCD pag | 4 nodes | 3 edges (1 bidirected, 2 directed)
x0 <-> x1
x0 -> x3 (+0.598)
x1 -> x2 (+0.61)
confounded_pairs: [[0, 1]]
BottomUpParceLiNGAM pag | 4 nodes | 3 edges (1 circle, 2 directed)
x0 o-o x1
x0 -> x3 (+0.598)
x1 -> x2 (+0.61)
causal_order: [[0, 1], 3, 2]
DirectLiNGAM dag | 4 nodes | 3 edges (3 directed)
x0 -> x1 (+0.686)
x0 -> x3 (+0.598)
x1 -> x2 (+0.61)
order: x0 -> x1 -> x2 -> x3
Benchmarks#
We compared the three supported methods with packages that implement them.
Method |
Compared with |
Variables |
Andrey’s speedup |
Same graph |
|---|---|---|---|---|
RCD |
lingam |
4-24 observed |
1.7-2.9x |
25 of 25 datasets |
BottomUpParceLiNGAM |
lingam |
4-24 observed |
2.5-10.8x |
25 of 25 datasets |
FCI |
causal-learn |
20-800, hidden ones included |
3.4-16.5x at 50 to 800; 0.62x at 20 |
13 of 18 datasets |
The 0.2.0 release card reports measurements for RCD and BottomUpParceLiNGAM on linear data with uniform noise, one node in five hidden, and 2,000 rows, on 2 cores.
Each bar is lingam's time divided by Andrey's on the same dataset, the median over five datasets, on 2 cores. Each method's bars share its own scale.
Agreement between packages does not establish accuracy: from 8 observed variables, BottomUpParceLiNGAM’s directed edges have more errors than the empty graph’s, in both packages.
Directed-edge errors
Andrey and lingamEmpty graphMore errors than the empty graph
FCI’s numbers come from the 0.1.0 release card: linear-Gaussian data with one variable in ten hidden and ten rows per variable, on 16 cores. At 20 variables, Andrey is slower, but both packages take under 0.1 s. Their median endpoint errors are within two of each other. At 800 variables, both have more endpoint errors than the empty graph.
Speed and accuracy depend on your data, settings, and hardware.
Other highlights#
Faster small fits. PC, FCI, GES, and hill climbing now find the Basic Linear Algebra Subprograms (BLAS) libraries once per process instead of on every fit. At 10 variables, PC and FCI take about a twelfth of the time they took before, hill climbing an eighth, and GES two fifths. Larger fits are no slower, and the graphs are unchanged.
Release cards. The Release cards page has one card per release: its benchmarks, the methods it adds or promotes, their known limits, and how to rerun the benchmarks.
Limitations. Every method’s docstring has a Limitations paragraph stating where the method was measured to work or that it is not benchmarked yet.
Checks before the fit. Every method checks your data and settings before fitting. Data with
NaNor infinity, a constant column, or fewer than 2 rows raises aValueErrorthat names the argument. An out-of-range setting raises the same error.
For more details, read the changelog.
Try it#
pip install -U andrey-core
Two new notebooks fit RCD and BottomUpParceLiNGAM on the example’s model, read the edge weights, and check the result over ten datasets. You can open each notebook in Colab, then try the methods on your own data.
Help us build it#
Andrey is in alpha, and the API may change between releases. When you try it on your own data, tell us what breaks or which method you need. We also welcome pull requests. The contributing guide explains how to set up.