--- name: 20261108-andrey-0-15-0 description: "What multi-domain data are, why data from several environments help causal discovery, and the methods Andrey has for them: CD-NOD, FCI-JCI, and J-PCMCI+." meta: type: post blogpost: true date: 2026-11-08 author: Shu Wan tags: [Release, Multi-domain data] version: 0.15.0 cover: crystal --- # 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. ```{raw} html :file: 0.15.0/environments.html ``` - 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](20261105-andrey-0-14-0.md#interventional-data-in-andrey) 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: ```python # "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`. ```{raw} html :file: 0.15.0/contexts.html ``` 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. ```{raw} html :file: 0.15.0/latent.html ``` 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. ```{literalinclude} 0.15.0/example.py :language: python :caption: example.py ``` ```{literalinclude} 0.15.0/example-output.txt :language: text :caption: example.py ``` 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. ```{raw} html :file: 0.15.0/sachs.html ``` 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](../docs/benchmarks/0.15.0.md) 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 ```{code-block} shell :class: window pip install -U andrey-core ``` The [constraint-based example](../docs/examples/constraint_based.ipynb) runs CD-NOD and FCI-JCI, the [permutation-based search example](../docs/examples/permutation_based.ipynb) runs BOSS-FCI and GRaSP-FCI, and the [time-series example](../docs/examples/time_series.ipynb) 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](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. [Hank Zhu](https://github.com/HouZhu2001) 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](https://proceedings.mlr.press/v216/gunther23a.html). - 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](https://jmlr.org/papers/v21/19-232.html). - Mooij, J. M., Magliacane, S., and Claassen, T. (2020). Joint causal inference from multiple contexts. *JMLR* 21(99), 1-108. [jmlr.org](https://jmlr.org/papers/v21/17-123.html). - 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](https://proceedings.mlr.press/v52/ogarrio16.html). - Ramsey, J., Andrews, B., and Spirtes, P. (2025). Efficient latent variable causal discovery: combining score search and targeted testing. [arXiv:2510.04263](https://arxiv.org/abs/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](https://doi.org/10.1126/science.1105809). --- - [Getting started](../docs/guides/getting-started.md) - [Release cards](../docs/benchmarks.md) - [Changelog](../docs/changelog.md) - [GitHub](https://github.com/Abel-ai-lab/andrey)