andrey.ges#
- andrey.ges(data, *, score_func=None, lambda_value=1.0, max_parents=None, categorical=None, max_categories=10, knowledge=None)[source]#
Learn a CPDAG from observational data with greedy equivalence search (GES).
GES searches CPDAGs, which represent equivalence classes of graphs. It adds edges while they improve a score, the BIC (Bayesian information criterion) by default, then removes edges in a backward sweep [1].
- Parameters:
data (
ArrayLike) – Observed data, one row per sample. Coerced tofloat64. A DataFrame’s column names become the structure’slabels.score_func (
str|None) – Local score objective:"local_score_BIC", the linear-Gaussian BIC, for continuous data."local_score_BDeu"is the BDeu score [3] and"local_score_BIC_categorical"the multinomial BIC [7], for categorical data coded by whole numbers."local_score_degen_gauss"(degenerate Gaussian) [4] and"local_score_cond_gauss"(conditional Gaussian) [5] are BIC scores for mixed data, continuous and categorical columns.Nonereads the column types fromcategorical, or infers them, and picks"local_score_BDeu"when every column is categorical or"local_score_cond_gauss"when some are, each with aDataTypeWarning; otherwise it picks"local_score_BIC". Pass"local_score_BIC"to read whole numbers, such as counts, as numbers. See Notes.lambda_value (
float) – Multiplier on the BIC penalty oflog(n_samples)per parameter: per parent for the linear-Gaussian BIC. Larger values give sparser graphs;1.0is the standard BIC. Must be finite and non-negative. BDeu ignores it; for the multinomial BIC it weighslog(n)per free parameter of a variable’s conditional probability table.max_parents (
int|None) – A cap on every node’s parents, at least0: no DAG the returned CPDAG represents gives a node more thanmax_parentsparents, and some DAG reaches each node’s count. A move whose CPDAG would break the cap, in either phase, is skipped for the next best move. A cap can stop the search short of the lowest-scoring graph, which GES otherwise finds.Nonesets no cap; the forward phase then keeps its gate ofn_variables / 2. See Notes.categorical (
Sequence[str|int] |None) – The categorical columns ofdata, for a mixed-data score: 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 other scores 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 score, columns within it get a one-timeDataTypeWarningthat they may be category codes. Withscore_func=None, the columns it infers pick the default score.knowledge (
Knowledge|None) – Background knowledge, aKnowledge: required and forbidden edges, the pairs allowed to be adjacent, and temporal tiers, all honored. No move adds a forbidden edge or removes a required one, and the search starts from the required edges. The result directs every edge the knowledge decides, then Meek’s rules direct the edges that follow, asKnowledgedescribes.
- Returns:
Wraps a
cpdagGraphStructure;metadata["algorithm"]is"GES"andmetadata["score"]the score of the result; lower is better. For BIC, it is the sum over variables ofn * log(residual variance) + lambda_value * log(n) * n_parents; for BDeu,-2times the log of the structure prior times the marginal likelihood; for the multinomial BIC and the mixed-data scores, see Notes. Directed marks (->) are oriented edges, undirected (--) the ones the equivalence class leaves open. Under a mixed-data setting,metadata["categorical"]lists the categorical columns it read, by name or 0-based position.- Return type:
- Raises:
NotImplementedError – If
score_funcnames no available score.TypeError – If
categoricalis not a list of column names and positions, ormax_categoriesis not an int. Also ifknowledgeis neitherNonenor aKnowledge, or a givenmax_parentsis not an int.ValueError – If
lambda_valueis negative or non-finite, if a givenmax_parentsis negative or below the parents some graph of the required edges ofknowledgegives a node, ifmax_categoriesis less than2, with a categorical score, ifdataholds a number that is not whole, or, with a mixed-data score, ifcategoricalnames a columndatalacks, or if a categorical column holds a number that is not whole or a missing value. Also ifcategoricalis given with another score. With the BIC score, also if a column repeats another up to scale and shift given more than two rows or, with more rows than columns, is a linear function of other columns. One message names every group of copies and says to keep one column per group. Also ifknowledgenames a columndatalacks or contradicts itself, asKnowledgelists.
- Warns:
DataTypeWarning – When
score_funcisNoneand the columns are all categorical or mixed, so the search scores with BDeu or the conditional Gaussian BIC; when a mixed-data score 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.
See also
andrey.fciAllows unmeasured common causes and selection bias.
andrey.direct_lingamOrients every edge when the noise is non-Gaussian.
Notes
When to use it. GES suits data with no unmeasured common causes. Its default BIC score assumes continuous data, where each variable depends linearly on its causes with Gaussian noise; in that setting it is a fast, accurate default. When every column is categorical, the default is
"local_score_BDeu", and on mixed data"local_score_cond_gauss";"local_score_BIC_categorical"and"local_score_degen_gauss"also score those data.When not to. When unmeasured common causes are plausible, use
fci(). When the noise is non-Gaussian and every edge should be oriented, usedirect_lingam().Choosing lambda_value. A larger value raises the penalty per parent, giving a sparser graph: fewer false edges and more missed ones.
1.0is the standard BIC. With many variables and about10 * n_variablessamples,lambda_value=2.0gave more accurate graphs than1.0on the published benchmarks. FGES, a fast GES, multiplied the BIC penalty by 4 when searching continuous data on graphs of up to a million variables [2].How it works. Without
max_parents, a variable that already has more thann_variables / 2parents gains no new parents in the forward phase; the backward phase has no such gate. Withmax_parents, that gate gives way to the cap. One move can add several parents to a variable at once, and rebuilding the CPDAG after a move can direct or undirect other edges, so the search tests the CPDAG after each move and takes the best move whose CPDAG keeps every variable within the cap. A variable’s count is its directed parents plus the largest set of its undirected neighbors that are adjacent to one another: a graph of the class directs such a set into it and no larger one, since two neighbors that are not adjacent would add a collider. A cap of1therefore allows a chain or a tree of undirected edges but not a collider. Moves whose score changes are equal after rounding are chosen by each column’s rank, as inhc(); the column-order guide says what the rank keeps under a new column order and on another machine.Categorical data.
"local_score_BDeu"takes each distinct value of a column as one category, so the codes need not start at 0 or be consecutive; a DataFrame’s text,bool, andcategorycolumns are read the same way. It scores a variable given its parents by the marginal likelihood of its counts in each parent configuration, under a Dirichlet prior with an equivalent sample size of 1 spread evenly over the cells [3]. A structure prior with one expected parent per variable multiplies it, from three variables up. Equivalent graphs score the same, as GES requires. Moves are scored in this process, without the worker processes the BIC search can use."local_score_BIC_categorical"reads the columns the same way and scores a variable by-2times the log-likelihood of its counts in each parent configuration, pluslambda_value * log(n)for each of theq * (r - 1)free parameters, withrits categories andqits parents’ joint categories [7]. Both scores are on the scale of the BIC:-2times a log score.Mixed data.
"local_score_degen_gauss"and"local_score_cond_gauss"score data with continuous and categorical columns; the categorical columns are read aspc()describes. Each is a BIC:-2times the fitted log-likelihood of a variable given its parents, pluslambda_value * log(n)per parameter; the degenerate Gaussian’s leaves out a constant per variable, which does not change which graph scores best. Equivalent graphs score the same. The degenerate Gaussian score [4] replaces a categorical column ofkcategories byk - 1indicator columns and scores every column as Gaussian, by the residual covariance of a linear regression on the parents’ columns; on continuous columns only, it equals"local_score_BIC". The conditional Gaussian score [5] fits the continuous variables of a variable and its parents in each joint category of their categorical ones, with a Gaussian of that category’s own mean and a covariance shrunk toward that of all rows by one pseudo-row [6], and divides by the same fit of the parents alone, so a categorical variable can have continuous parents. It counts parameters for every joint category, so a parent with many categories costs many parameters; on continuous columns only, it differs from"local_score_BIC"by a constant per variable, so it finds the same graph. Moves are scored in this process.Limitations. Measured on simulated linear-Gaussian data from sparse random graphs (mean degree 2), from 20 to 200 variables with ten rows per variable and a forward-phase gate of 4 parents rather than
n_variables / 2; the capmax_parentssets is not benchmarked yet. Atlambda_value=1.0its median errors (SHD) stayed below the empty graph’s up to 150 variables (141 against 150) and passed it at 200 (206 against 200), wherelambda_value=2.0brought them to 43. The median fit took 0.15 s at 20 variables and 40 s at 150, on 128 cores. On the 223-variable ANDES network with 1,000 simulated rows and the default limit, its median errors passed the empty graph’s (612 against 338). With fewer rows than columns the BIC score is unreliable. A parent set ofn_samples - 1or more variables fits its child exactly, so the search never selects one, nor any parent set that leaves its child a residual variance of at most1e-12times its variance. On simulated mixed data (conditional linear Gaussian networks on sparse random graphs, the first half of the variables categorical with 2 to 5 categories), from 10 to 40 variables with 2,000 rows, its median errors at 40 variables were 8 with"local_score_degen_gauss"and 6.5 with"local_score_cond_gauss", against 40, and the median fit took 0.75 s and 1.2 s on 4 cores. On simulated categorical networks with 2 or 3 categories per variable, of the same sizes and rows,"local_score_BDeu"gave 6 against 40 at 40 variables, in 0.60 s. The multinomial BIC is not benchmarked with GES yet: its tests check it against the formula and run GES on small fixed categorical networks.References
Examples
>>> import numpy as np >>> import pandas as pd >>> from andrey import ges >>> rng = np.random.default_rng(0) >>> x = rng.standard_normal(500) >>> y = rng.standard_normal(500) >>> z = x + y + 0.5 * rng.standard_normal(500) >>> ges(pd.DataFrame({"x": x, "y": y, "z": z})).structure.oriented_edges() [('x', 'z', 'directed'), ('y', 'z', 'directed')]
On categorical data, with BDeu:
>>> a = rng.integers(0, 2, 2000) >>> b = rng.integers(0, 2, 2000) >>> c = np.where(rng.random(2000) < 0.8, a + b, rng.integers(0, 3, 2000)) >>> data = pd.DataFrame({"a": a, "b": b, "c": c}) >>> ges(data, score_func="local_score_BDeu").structure.oriented_edges() [('a', 'c', 'directed'), ('b', 'c', 'directed')]
On mixed data, with a category column and the conditional Gaussian score:
>>> g = rng.integers(0, 3, 2000) >>> u = rng.standard_normal(2000) >>> v = np.array([-1.0, 0.0, 1.0])[g] + u + 0.5 * rng.standard_normal(2000) >>> data = pd.DataFrame({"g": pd.Categorical(g), "u": u, "v": v}) >>> ges(data, score_func="local_score_cond_gauss").structure.oriented_edges() [('g', 'v', 'directed'), ('u', 'v', 'directed')]