Multi-domain data#

Cover: an engraved crystal lit by three lamps, one lamp and the facet it lights amber, drawn in navy lines

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

V1V2V3V4C

(b) PC, one environment

V1V2V3V4C

(c) PC, pooled

V1V2V3V4C

(d) CD-NOD, with C

V1V2V3V4C
A chain whose environments shift V2 and V4 through one shared change, after Figure 1 of Huang et al. (2020). C is the environment, dashed where the method is not given it. In (c), the edges between pairs that (a) does not join are highlighted.
  • 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 V2 and V4. No set of the other variables makes V4 independent of V2, which shares the change, or of V1, which conditioning on V2 links 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 is C’s edges into V2 and V4, and the two extra edges go. Nothing in the data causes C: an experimenter, a site, or a time period sets it. C and V1 are independent, so C -> V2 <- V1 is a collider, which directs V1 -> V2; V2 -> V3 follows, and C -> V4 <- V3 is 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

X1X2C

(b) FCI, no ban

X1X2C

(c) FCI-JCI, with C

X1X2C

(d) Truth: U causes both

X1X2CU

(e) FCI, no ban

X1X2CU

(f) FCI-JCI, with C

X1X2CU
After Figure 1 of Mooij, Magliacane, and Claassen (2020): X1 is hours of violent games, X2 aggressive behavior, and C a ban on violent games. Without the ban, FCI returns the same PAG in both rows. With C, FCI-JCI finds that X1 causes X2 in (c) and that it does not in (f). Dashed nodes are not given to the method.

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

XYZWL

(b) BOSS

XYZWL

(c) BOSS-FCI

XYZWL
After Section 4 of Ramsey, Andrews, and Spirtes (2025). L is hidden. BOSS's CPDAG over the measured variables joins a pair that (a) does not (highlighted) to cover a collider; BOSS-FCI's test removes it and returns the PAG: Y and Z share a hidden common cause.

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.

example.py#
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']}")
example.py#
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

rafmekplcpip2pip3erkaktpkapkcp38jnk

(b) PC, pooled: 25 edges, 13 not in (a)

rafmekplcpip2pip3erkaktpkapkcp38jnk

(c) CD-NOD, with env: 10 edges, 2 not in (a)

rafmekplcpip2pip3erkaktpkapkcp38jnk
Sachs's 11 proteins and phospholipids under nine conditions. An edge between a pair the consensus network does not join is highlighted. CD-NOD's index, adjacent to all 11 variables, is not drawn.

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.

Checked against the authors’ code#

We compared the four methods this release makes supported with their authors’ implementations, on data simulated the way their papers simulate it. FCI-JCI’s and J-PCMCI+’s data come from several environments; BOSS-FCI’s and GRaSP-FCI’s come from one, with hidden common causes.

  • FCI-JCI ties pcalg 2.7-12’s FCI-JCI in all 12 comparisons on the JCI paper’s simulation at 4 and 10 variables, under two assumption sets at alpha 0.01 and 0.05. It is 4.0 to 4.6 times faster at 4 variables and 72 to 79 times faster at 10. At 100 variables with 100 rows per environment, every pcalg fit on the two datasets of a dry run ran past 1,800 s, so we ran Andrey alone there; each of Andrey’s fits took under half a second and made fewer errors than the empty graph.

  • BOSS-FCI ties Tetrad 7.6.10’s BOSS-FCI in 72 of 73 comparisons and is better in one, at 100 variables. The data follow the BOSS-FCI paper’s simulation at 20 measured variables with up to 8 hidden, run at the paper’s settings and at Andrey’s defaults, and at 100 with 10, run at the paper’s settings. It is 1.05 to 5.8 times faster at 20 variables and 1.85 times faster at 100.

  • GRaSP-FCI ties Tetrad 7.6.10’s GRaSP-FCI in 72 of 73 comparisons on the same data and is better in one. It is 1.16 to 5.8 times faster at 20 variables and 7.6 times faster at 100.

  • J-PCMCI+ ties tigramite 5.2.10.1’s J-PCMCI+ in all 28 comparisons on the model of the J-PCMCI+ paper’s experiment script: 5 variables, every context or one of each kind measured, and 5 to 200 environments of 10 to 200 time steps. It is 14 to 168 times faster. tigramite stopped with an error on most of the simulated data with every context measured, at a same-step link between two measured contexts, so we ran it with a fix that leaves that link unoriented.

Tetrad 7.6.3, the authors’ earlier release, removes edges by the GFCI paper’s triangle rule. At the paper’s settings it made fewer errors at mean degree 4 and 6 and at 100 variables; at Andrey’s defaults at mean degree 2 it made more. Andrey and Tetrad 7.6.10 test every edge by default, and edge_removal="triangles" runs the triangle rule.

The release card has every comparison and how we measured them. A tie means the comparison found no reproducible difference in errors; it does not mean the methods returned identical graphs.

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, env is 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", or score_func="cond_gauss" with categorical=[], 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 climbing n_starts above 1. One climb can direct a chain against the index and list every variable on it as changing.

  • env in 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.