andrey.cdnod#
- andrey.cdnod(data, domain_index, *, alpha=0.05, indep_test='fisherz', collider_order=None, categorical=None, max_categories=10, dependence_threshold=None, max_depth=None, knowledge=None)[source]#
Learn a partially directed graph over the data variables from multi-domain data with CDNOD.
CDNOD [1] adds a domain/context index as an extra causal parent, so distribution shifts across regimes are modeled, then runs PC-style discovery. It applies to samples pooled across domains, regimes, or time blocks. The index is appended as a trailing pseudo-node and removed from the result.
- Parameters:
data (
ArrayLike) – Observed data, one row per sample. Coerced tofloat64. A DataFrame’s column names become the structure’slabels.domain_index (
ArrayLike) – Domain or context index of each row ofdata, such as0and1for two domains, or the time step for data that drift over time. Coerced tofloat64; whole numbers with"chisq","gsq","degen_gauss", or"cond_gauss". See Notes for more than two domains. When every row has the same value, the data come from one domain, and the result equalspc()’s.alpha (
float) – Significance level of the conditional-independence test. Smaller values give sparser graphs.indep_test (
str) – Conditional-independence test."fisherz"(Fisher’s z) and"student_t"(Student-t) test partial correlation, for continuous data."chisq"(Pearson’s chi-square) and"gsq"(G-square, the likelihood ratio) test categorical data, coded by whole numbers, and"degen_gauss"and"cond_gauss"mixed data."kci","rcot","cmiknn", and"gpdc"also detect nonlinear dependence in continuous data, at a far higher cost per test. Each as forpc(); see Notes for the index.collider_order (
str|None) – The order in which colliders are oriented, as forpc():"p_value"by the largest p-value that separated each collider’s endpoints, so the colliders do not depend on the column order, and"column"in column order.Nonefollowsandrey.config.tie_break, which also sets whether Meek’s rules run in order-free rounds or fire one at a time in column order, as forpc(). Under either order, the edges out of the index are directed first, and no collider puts an arrowhead at the index.categorical (
Sequence[str|int] |None) – The categorical columns ofdata, for a mixed-data test: column labels (strings) or 0-based positions.Noneinfers them: a DataFrame’scategory,bool, and text columns, and every column of at mostmax_categoriesdistinct whole numbers, named in aDataTypeWarning.[]marks none. The domain index is categorical too, sodatamay have no categorical column. The other tests take no column types and reject it. See Data types in the getting-started guide.max_categories (
int) – The most distinct whole numbers a column may hold to be inferred as categorical, at least2. With a continuous test, columns within it get a one-timeDataTypeWarningthat they may be category codes.dependence_threshold (
float|None) – Runs the independent-change step at this threshold, in(0, 1), on its module dependence measure: two modules, the distributions of a candidate cause and of its effect given it, whose measure is at most the threshold count as changing independently.Noneskips the step. See Notes.max_depth (
int|None) – The maximum conditioning-set size of every conditional-independence test, at least0:0runs only unconditional tests, andNonesets no limit. A smaller cap runs fewer tests and can keep edges that a larger conditioning set would remove.knowledge (
Knowledge|None) – Background knowledge, aKnowledge: required and forbidden edges, the pairs allowed to be adjacent, and temporal tiers, all honored. The adjacency search starts from the allowed pairs and never removes a required pair; the tests can remove any other allowed pair. A pair the knowledge makes non-adjacent is tested for a separating set, so colliders over it are oriented as usual; when the search finds none withinmax_depth, no rule reads its unshielded triples as colliders or non-colliders. The result directs every edge the knowledge decides, then Meek’s rules direct the edges that follow, asKnowledgedescribes; knowledge that orients the edges the search kept into a directed cycle raisesValueError. The domain index is unconstrained.
- Returns:
Wraps a
GraphStructureover theddata variables;metadata["algorithm"]is"CDNOD". Directed marks (->) are oriented edges, undirected (--) the ones left open. The graph is partially directed, not the CPDAG of an observational equivalence class: the index orients edges that the true DAG’s CPDAG leaves undirected, and the graph stands for the DAGs that agree on every direction CD-NOD can identify [1]. Andrey has no kind for such a graph, so it is taggedcpdag, andandrey.metricsscores acpdagagainst the truth’s CPDAG; score the result against the graph CD-NOD can identify instead. Under a mixed-data setting,metadata["categorical"]lists the categorical columns it read, by name or 0-based position.- Return type:
- Raises:
NotImplementedError – If
indep_testnames no available test.TypeError – If
categoricalis not a list of column names and positions, ormax_categoriesis not an int. Also ifknowledgeis neitherNonenor aKnowledge, or a givenmax_depthis not an int.ValueError – If
domain_indexdoes not hold one value per row ofdataor holdsNaN/inf, ifdatahas too few rows for a test, ifalphaordependence_thresholdis not in(0, 1), ifdataorcategoricaldoes not suit the test, as forpc(), or, with a categorical or mixed-data test, ifdomain_indexholds a value that is not a whole number. Also ifcollider_orderis not"p_value"or"column". With"fisherz"or"student_t", also if a column ofdata, ordomain_indexwith more than one domain, repeats another up to scale and shift, before any test runs; one message names every group of copies and says to keep one column per group. Also if a givenmax_depthis negative, or ifknowledgenames a columndatalacks or contradicts itself, asKnowledgelists.
- Warns:
DataTypeWarning – When a mixed-data test infers the categorical columns, or finds none and so runs as the linear-Gaussian form, or when a continuous setting meets columns that look like category codes; each message warns once per process.
Notes
The index is added as a variable that can only be a cause. When a variable’s distribution changes with the index, the edge from the index into it is directed, and Meek’s rules can then orient edges that PC leaves undirected on the same data.
"fisherz"and"student_t"test the index as a number, by linear correlation, so they find a change in a variable’s mean that follows the index’s values. With two domains the coding does not matter. With three domains coded0, 1, 2, a shift in domain1alone can go undetected, and recoding the domains can change the result. A change in a variable’s noise variance alone, or in the size of an effect whose cause has mean zero, leaves the mean unchanged, so the index keeps no edge to that variable. The paper uses a kernel conditional-independence test [1] [2] that detects such changes; Andrey has none."chisq","gsq","degen_gauss", and"cond_gauss"take each index value as a category, so they find a shift in any domain, whatever the coding. With"degen_gauss"or"cond_gauss", the index is categorical, so every data column may be continuous.With
dependence_thresholdset, the independent-change step [1] orients undirected edges between two variables that both keep an edge to the index. For a candidate directionx -> y, it measures how strongly the change ofP(x, Z)across the index values and the change ofP(y | x, Z)depend on each other, whereZis a set of neighbors that blocks their common causes. The measure is the normalized HSIC of the two modules’ kernel embeddings at each row’s index value, with the Gaussian kernel widths of [2].x -> yis oriented when that measure is at most the threshold and the measure fory -> xis above it; when both are above it, the edge stays undirected. An orientation that would close a directed cycle is skipped, so that edge stays undirected unless Meek’s rules orient it. Meek’s rules run before and after the step. The measure compares the index values as separate domains, so the step needs many domains, each with many rows: with two domains, the two changes always depend on each other, and nothing is oriented; a per-row index, such as a time step, makes every row its own domain, and the step cannot measure a change. The paper leaves the threshold open. For two modules that change independently, the measure sits near1 / (K - 1)overKdomains, so the threshold has to rise as the number of domains falls; the 0.06 below is the value the measurements used, not a default. The step is off by default, because with"fisherz"or"student_t"only a variable whose mean follows the index reaches it, and such a shared trend makes the changes depend on each other.Limitations. Measured on simulated linear-Gaussian data from sparse random graphs (mean degree 2), with 2,000 rows pooled from four domains of equal size, in which one variable in three shifts its mean with the domain, from 10 to 50 variables with 20 datasets per size. Its median errors (SHD against the CPDAG the domain index makes identifiable) stayed below the empty graph’s at every size (12 against 50 at 50 variables). The median fit took 0.18 s at 50 variables, on 4 cores. The independent-change step is not benchmarked yet; its tests run it on at most 3 variables with 600 rows from 20 domains. It runs the PC search with the index as one more variable, so it costs about as much as
pc()on the data plus one column. Its partial-correlation tests assume linear relations with Gaussian noise and find only a change in a variable’s mean that is linear in the index: on two variables from two domains with 600 rows, a mean shift of the cause oriented the edge on 20 of 20 seeds, and a change of its variance alone, or of the weight of the effect on a mean-zero cause alone, on none. Without the independent-change step, an edge between two variables that both change with the index stays undirected unless the collider or Meek’s rules orient it. With"fisherz"or"student_t", the step reaches only variables whose means follow the index. On two variables from 20 domains with 600 rows, both means rising linearly by about 1 across the domains, the cause’s noise scale drawn per domain, and the effect’s weight on it drawn per domain from (0.5, 2.5), at the threshold 0.06 it oriented the edge on 1 of 20 seeds. Given the true skeleton of ten complete three-variable DAGs from 20 domains with 600 rows, no mean trend, and weights in (-2, 2) drawn per domain, it oriented 18 of their 30 edges correctly and 4 against the truth. The paper fits the kernel widths, the index kernel, and the ridge of the step by Gaussian-process likelihood; Andrey fixes them. With a mixed-data test, it is experimental and not benchmarked yet; its tests run it on 5 variables with 3,000 rows.References
Examples
>>> import numpy as np >>> from andrey import cdnod >>> rng = np.random.default_rng(0) >>> domain_index = np.repeat([0.0, 1.0], 300) >>> x = 2.0 * domain_index + rng.standard_normal(600) >>> y = x + 0.5 * rng.standard_normal(600) >>> cdnod(np.column_stack([x, y]), domain_index).structure.oriented_edges() [(0, 1, 'directed')]