Multi-domain data#

TL;DR#
Multi-domain data come from several environments, such as sites, batches, or the conditions of an experiment, that share one causal structure and differ in the mechanisms of some variables, where a mechanism is the distribution of a variable given its causes. With each row’s environment recorded as an environment index, a method can tell a dependence that changing mechanisms create from a causal edge, and it can orient edges that one environment leaves undirected.
Andrey has CD-NOD and FCI-JCI for these data and J-PCMCI+ for time series from several environments, and GES, hill climbing, Tabu search, GFCI, BOSS, GRaSP, BOSS-FCI, and GRaSP-FCI take the environment too, as an experimental option. Each lists the environment index’s edges, and CD-NOD, GES, hill climbing, Tabu search, BOSS, and GRaSP also list the variables with changing mechanisms.
FCI-JCI ties pcalg, its authors’ implementation, in all 12 comparisons and is 4 to 79 times faster. BOSS-FCI and GRaSP-FCI each tie Tetrad 7.6.10, their authors’ current implementation, in 72 of 73 comparisons and are better in one, on data with hidden common causes and no
env. J-PCMCI+ ties tigramite, its authors’ package, in all 28 comparisons and is 14 to 168 times faster.
Data rarely come from one fixed process. fMRI recordings change with the stimulus, the task, and the subject’s attention; stock returns change with economic policy; remote-sensing images change with the ground, the vegetation, and the light of each area and season (Huang et al., 2020). Experiments change conditions on purpose, such as a drug or a placebo, or a reagent that inhibits part of a cell, and other contexts are set from outside, such as a ban that holds in some countries and not in others (Mooij, Magliacane, and Claassen, 2020). If you combine such data without recording each row’s environment, these differences can mislead causal discovery. Recording the environment lets a method account for them.
CD-NOD (Huang et al., 2020) calls these data heterogeneous or multi-domain and uses a domain or time index as a surrogate variable. Joint Causal Inference (Mooij, Magliacane, and Claassen, 2020) calls the environments contexts and the index a context variable. Both describe changing mechanisms, which CD-NOD calls changing causal modules. Here we use environment, environment index, and mechanism.
Why use data from several environments#
Figure 1 of Huang et al. (2020) is a chain, V1 -> V2 -> V3 -> V4, whose environments shift V2
and V4 through one shared change, as a subject’s attention might act on two brain regions. The
figure simulates it with four environments.
(a) The data's graph
(b) PC, one environment
(c) PC, pooled
(d) CD-NOD, with C
From one environment (b), PC finds the chain’s adjacencies but cannot direct them: with no collider, every orientation of a chain fits the data alike.
Pooled without the environment (c), the shared change acts as a hidden common cause of
V2andV4. No set of the other variables makesV4independent ofV2, which shares the change, or ofV1, which conditioning onV2links to the change, so PC keeps two edges the data’s graph does not have.With the environment as one more variable,
C, the index (d), the change isC’s edges intoV2andV4, and the two extra edges go. Nothing in the data causesC: an experimenter, a site, or a time period sets it.CandV1are independent, soC -> V2 <- V1is a collider, which directsV1 -> V2;V2 -> V3follows, andC -> V4 <- V3is another collider. CD-NOD returns the whole chain directed.
The variables C points into have changing mechanisms. The distribution of V3 changes too,
through V2, but its mechanism does not, and C is not adjacent to it.
Joint Causal Inference applies this to any method that takes such background knowledge: add the environment index as a variable that no data variable causes, and run the method on the rows of every environment together. CD-NOD does the same for PC.
Multi-domain data in Andrey#
Every method here but J-PCMCI+ takes env, each row’s environment, in the forms the interventional
methods take it: a label per row, the name
of a DataFrame column, or each environment’s rows as a table apart. They read it as the environment
index and report its edges in the result’s metadata; CD-NOD, GES, hill climbing, Tabu search,
BOSS, and GRaSP also report the variables with changing mechanisms there:
# "site" is a column of text labels, which is not a variable. The default Fisher z test and BIC
# score would read the labels as numbers, so these calls read them as categories.
out = andrey.cdnod(data, "site", indep_test="cond_gauss")
out.metadata["changing_variables"] # the variables whose mechanism changes across the sites
out.metadata["index_edges"] # the index's edges, such as ["x", "directed"]
out = andrey.boss(data, "site", score_func="cond_gauss", categorical=[]) # BOSS, same index
out = andrey.fci_jci(data, "site", indep_test="cond_gauss", confounded_index=False)
Method |
Search |
Hidden common causes |
Output |
|---|---|---|---|
CD-NOD |
PC’s tests |
not allowed |
CPDAG, index edges, changing variables |
FCI-JCI |
FCI’s tests |
allowed |
PAG, index edges |
BOSS-FCI, GRaSP-FCI |
a BOSS or GRaSP search, then tests |
allowed |
PAG, index edges |
GFCI |
a GES search, then tests |
allowed |
PAG, index edges |
GES, hill climbing, Tabu search |
a score search over graphs |
not allowed |
CPDAG, index edges, changing variables |
BOSS, GRaSP |
a search over variable orders |
not allowed |
CPDAG, index edges, changing variables |
J-PCMCI+ |
PCMCI+’s tests on time series |
allowed if they are contexts |
temporal graph, context links, index edges |
CD-NOD (Huang et al., 2020) runs PC with the index as one more variable that can only be a cause. Every edge it keeps at the index points into a changing variable.
FCI-JCI (Mooij, Magliacane, and Claassen, 2020) runs FCI the same way, so it allows hidden common
causes; it is new in this release, and supported. confounded_index says whether a hidden variable
may cause both the environment and a data variable. True, the default, leaves the index’s end of
an edge open until a rule orients it; False, for an environment set from outside as in an
experiment, points every edge the index keeps out of it. Each index edge is directed,
partially_directed, or bidirected.
Mooij, Magliacane, and Claassen (2020) open with a question that observational data cannot answer:
does playing violent games, X1, cause aggressive behavior, X2, or does something hidden cause
both? FCI returns X1 o-o X2 either way. A ban on violent games in some countries, C, lowers X1
from outside. If X1 causes X2, the ban reaches X2 through X1, and FCI-JCI finds X1 -> X2.
If a hidden U causes both and X1 does not cause X2, the ban leaves X2 alone, X1 is a
collider, C -> X1 <-o X2, and FCI-JCI finds that X1 does not cause X2. The paper adds a second
context, school programs that act on X2 alone, which also tells a hidden cause from X2 -> X1.
(a) Truth: X1 causes X2
(b) FCI, no ban
(c) FCI-JCI, with C
(d) Truth: U causes both
(e) FCI, no ban
(f) FCI-JCI, with C
BOSS-FCI and GRaSP-FCI (Ramsey, Andrews, and Spirtes, 2025) start from the CPDAG of a BOSS or GRaSP
search, remove with conditional-independence tests the edges whose endpoints are independent given
some set of other variables, and orient the rest with FCI’s rules. They follow the steps of the
paper’s Algorithm 5, and both are new in this release, and supported. With env, the index leads
every variable order of their search, and their tests and rules treat it as FCI-JCI does.
edge_removal="triangles" tests only the edges that lie in a triangle of the CPDAG, the rule of
Section 5 of the GFCI paper (Ogarrio, Spirtes, and Ramsey, 2016); GFCI takes it too.
Ramsey, Andrews, and Spirtes (2025) show why a score search needs the tests after it with the
smallest case: X -> Y <- L -> Z <- W with L hidden. No DAG over the four measured variables has
exactly their independences, so a DAG search adds an adjacency, X - Z or W - Y, to cover the
collider at Y or Z; on the figure’s data BOSS adds W - Y. BOSS-FCI’s tests find W and Y
independent, remove the edge, and orient the rest into the PAG X o-> Y <-> Z <-o W: Y and Z
share a hidden common cause. Starting from the score search keeps the tests few: FCI alone searches
many conditioning sets, and the more it tries, the likelier one shows an independence by chance.
(a) Truth, L hidden
(b) BOSS
(c) BOSS-FCI
J-PCMCI+ (Günther, Ninad, and Runge, 2023) learns from time series, one per environment, such as
sites or subjects, so it takes a list of tables instead of env. The paper calls the environments
datasets and what differs between them contexts: a time context changes over time the same way in
every environment, such as the weather over a region, and a space context is fixed within one, such
as a site’s altitude. J-PCMCI+ runs PCMCI+ with the contexts you measured as columns, and adds one
index of the environment and one of the time step that stand in for the contexts nobody measured.
It lists the links from the contexts in metadata["context_links"] and the indices’ edges in
metadata["index_edges"] and metadata["time_index_edges"]. It is new in this release, and
supported.
GES, hill climbing, Tabu search, BOSS, and GRaSP add the index as a variable that no data variable
causes and search over the graphs that keep every edge at it pointing out. GFCI does the same and
runs its tests and rules as FCI-JCI does. In these six methods, and in BOSS-FCI and GRaSP-FCI, env
is experimental. Without env, every method returns what it returned before.
Sachs’s nine conditions#
Sachs and colleagues (2005) measured 11 proteins and phospholipids in single human immune cells:
853 under a general stimulation, the observational cells, and the rest under eight more
conditions, each with a reagent that stimulates or inhibits part of the signaling network. The
known network has 17 arcs, a consensus that Mooij, Magliacane, and Claassen (2020) note is
contested. load_dataset("sachs", conditions=True) loads all 7,466 cells with each cell’s
condition as env.
Mooij, Magliacane, and Claassen (2020) call these data a compelling motivation for Joint Causal Inference: signaling networks have strong feedback loops, and some of the measured variables likely share hidden causes.
The condition codes 0 to 8 have no order, so the example reads them as categories, with the
conditional Gaussian test and score. The default Fisher z test and BIC score read env as a number,
and text labels as their codes 0, 1, … in sorted order: they find a change in a mean that follows
the codes’ order, and recoding the conditions can change the result. The experimenters set each
cell’s condition, so nothing hidden causes it, and confounded_index=False tells FCI-JCI and
BOSS-FCI so. The example fits the measurements as they load, without a log transform.
import andrey
from andrey.data import load_dataset
if __name__ == "__main__": # worker processes re-import this script
# 7,466 cells in nine conditions; env gives each cell's condition, 0 for the observational one.
sachs = load_dataset("sachs", conditions=True)
data, env = sachs.data, sachs.env
consensus = {frozenset(edge[:2]) for edge in sachs.graph.oriented_edges()}
def summary(out):
edges = out.structure.oriented_edges()
known = sum(frozenset(edge[:2]) in consensus for edge in edges)
directed = sum(kind == "directed" for *_, kind in edges)
heads = {"directed", "partially_directed", "bidirected"} # the edges with an arrowhead
arrowheads = sum(kind in heads for *_, kind in edges)
line = f"{len(edges):2} edges, {known} in the consensus network, {directed:2} directed"
line += f", {arrowheads:2} with an arrowhead"
if "index_edges" in out.metadata: # the methods given env
line += f", {len(out.metadata['index_edges'])} index edges"
return line
# PC on the observational cells alone, and on data from all nine conditions without env.
print(f"pc, observational: {summary(andrey.pc(data[env == 0]))}")
print(f"pc, pooled: {summary(andrey.pc(data))}")
# The conditions are categories, not numbers: these tests and scores read env as a category.
cdnod = andrey.cdnod(data, env, indep_test="cond_gauss")
print(f"cdnod: {summary(cdnod)}")
scores = {"score_func": "cond_gauss", "categorical": []}
ges = andrey.ges(data, env, **scores)
print(f"ges: {summary(ges)}")
boss = andrey.boss(data, env, **scores)
print(f"boss: {summary(boss)}")
# The experimenters set each cell's condition, so no hidden variable causes it.
fci_jci = andrey.fci_jci(data, env, indep_test="cond_gauss", confounded_index=False)
print(f"fci_jci: {summary(fci_jci)}")
boss_fci = andrey.boss_fci(data, env, indep_test="cond_gauss", confounded_index=False, **scores)
print(f"boss_fci: {summary(boss_fci)}")
# The variables whose mechanism changes across the conditions.
print(f"changing variables: {cdnod.metadata['changing_variables']}")
pc, observational: 8 edges, 8 in the consensus network, 2 directed, 2 with an arrowhead
pc, pooled: 25 edges, 12 in the consensus network, 22 directed, 22 with an arrowhead
cdnod: 10 edges, 8 in the consensus network, 4 directed, 4 with an arrowhead, 11 index edges
ges: 10 edges, 9 in the consensus network, 0 directed, 0 with an arrowhead, 11 index edges
boss: 10 edges, 9 in the consensus network, 0 directed, 0 with an arrowhead, 11 index edges
fci_jci: 10 edges, 8 in the consensus network, 0 directed, 6 with an arrowhead, 11 index edges
boss_fci: 9 edges, 8 in the consensus network, 0 directed, 2 with an arrowhead, 11 index edges
changing variables: ['raf', 'mek', 'plc', 'pip2', 'pip3', 'erk', 'akt', 'pka', 'pkc', 'p38', 'jnk']
From the 853 observational cells, PC keeps 8 edges, all in the consensus network. Using data from
all nine conditions without env gives PC almost nine times the rows and 25 edges, but only 12 of
them are in the consensus network. The conditions shift many of the measured variables at once, and
in the pooled rows those variables move together.
The figure draws the consensus network beside PC’s graph on the pooled cells and CD-NOD’s with
env, the edges between pairs the consensus network does not join highlighted.
(a) Consensus network: 17 edges
(b) PC, pooled: 25 edges, 13 not in (a)
(c) CD-NOD, with env: 10 edges, 2 not in (a)
With env, every method in the example keeps 9 or 10 edges, 8 or 9 of them in the consensus
network. GES and BOSS each recover 9 adjacencies of the consensus network, one more than PC on the
observational cells. Each method keeps an edge between the index and all 11 variables, and CD-NOD
identifies all 11 as having changing mechanisms.
That is also why the index orients no edge here. The index orients an edge when it is adjacent to one end and not the other; here it is adjacent to both ends of every edge. GES and BOSS leave all 10 edges undirected. FCI-JCI and BOSS-FCI fully direct none of theirs, though colliders among the measured variables put arrowheads on 6 and 2 of them; CD-NOD directs 4 the same way.
Limits#
Each method but J-PCMCI+ takes one environment index, so a design with several context variables runs with them merged into one label. The card reports FCI-JCI on such designs, outside the setting it is judged on, where it tied pcalg too.
With the default Fisher z test or BIC score,
envis read as a number, and text labels as their codes 0, 1, … in sorted order. With three or more environments, a shift in one of them alone can go unnoticed, and recoding the labels can change the result.indep_test="cond_gauss", orscore_func="cond_gauss"withcategorical=[], reads each label as a category.When the index is adjacent to every variable, as on Sachs’s conditions, it orients no edge.
CD-NOD, GES, hill climbing, Tabu search, BOSS, and GRaSP assume no hidden common cause beyond the environment. When one is plausible, use FCI-JCI, GFCI, BOSS-FCI, or GRaSP-FCI.
With
env, give hill climbingn_startsabove 1. One climb can direct a chain against the index and list every variable on it as changing.envin GES, hill climbing, Tabu search, GFCI, BOSS, GRaSP, BOSS-FCI, and GRaSP-FCI is experimental; no benchmark has measured it yet.BOSS-FCI and GRaSP-FCI recover only part of the PAG on their paper’s simulation, and so does Tetrad. Hidden variables leave many pairs joined only through them. At mean degree 6, their errors were about three quarters of the empty graph’s.
The J-PCMCI+ paper’s script draws no link between two variables in the same time step, so the comparisons in its setting count only false same-step links. On
andrey.data.sample_context_series, where half the links are in the same step, Andrey ties tigramite too. With the GPDC test, J-PCMCI+ is slower than tigramite from 20 environments on, and of its 24 comparisons with that test it made more errors in two and fewer in three.
Try it#
pip install -U andrey-core
The constraint-based example runs CD-NOD and FCI-JCI, the permutation-based search example runs BOSS-FCI and GRaSP-FCI, and the time-series example runs J-PCMCI+.
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 or which method you need. We also welcome pull requests. The contributing guide explains how you can set up the development environment.
Hank Zhu built much of this release with us: Sachs’s nine conditions, the variables CD-NOD reports as changing, FCI-JCI, BOSS-FCI, and GRaSP-FCI, the environment index in the score and permutation searches, and the comparisons with the authors’ code. Thank you, Hank.
References#
Günther, W., Ninad, U., and Runge, J. (2023). Causal discovery for time series from multiple datasets with latent contexts. UAI, PMLR 216, 766-776. proceedings.mlr.press.
Huang, B., Zhang, K., Zhang, J., Ramsey, J., Sanchez-Romero, R., Glymour, C., and Schölkopf, B. (2020). Causal discovery from heterogeneous/nonstationary data. JMLR 21(89), 1-53. jmlr.org.
Mooij, J. M., Magliacane, S., and Claassen, T. (2020). Joint causal inference from multiple contexts. JMLR 21(99), 1-108. jmlr.org.
Ogarrio, J. M., Spirtes, P., and Ramsey, J. (2016). A hybrid causal search algorithm for latent variable models. PGM, PMLR 52, 368-379. proceedings.mlr.press.
Ramsey, J., Andrews, B., and Spirtes, P. (2025). Efficient latent variable causal discovery: combining score search and targeted testing. arXiv:2510.04263.
Sachs, K., Perez, O., Pe’er, D., Lauffenburger, D. A., and Nolan, G. P. (2005). Causal protein-signaling networks derived from multiparameter single-cell data. Science 308(5721), 523-529. doi:10.1126/science.1105809.