andrey.grasp_fci#
- andrey.grasp_fci(data, env=None, *, score_func=None, penalty_discount=1.0, ebic_gamma=0.0, equivalent_sample_size=1.0, grasp_depth=3, indep_test=None, alpha=0.05, collider_rule='sepsets', categorical=None, edge_removal='all', confounded_index=True, seed=None, n_starts=1, max_depth=None, max_parents=None, knowledge=None)[source]#
Learn a PAG with GRaSP-FCI: GRaSP’s adjacencies, pruned by tests, oriented by FCI’s rules.
GRaSP-FCI [1] is GFCI [2] with the greedy relaxation of the sparsest permutation [3] in place of GES: one
grasp()search gives a CPDAG, conditional-independence tests remove the adjacencies whose endpoints are independent given some set of other variables, and FCI’s rules orient the rest into a PAG. It allows hidden common causes, asgfci()does, with GRaSP’s search over variable orders.- Parameters:
data (
ArrayLike) – Observed data, one row per sample. Coerced tofloat64. A DataFrame’s column names become the structure’slabels. A column thatenvnames is not a variable: it leaves the data before the fit, andcategorical,knowledge, and the result’s positions count the other columns. A list of DataFrames or 2-D arrays gives each environment’s rows apart, labeled by position0, 1, ..., and a mapping labels each by its key: the rows are stacked in that order, andenvstaysNone. A position is the environment’s number, so with"bic"and"fisherz"and three or more environments, the order of the list can change the result (see Notes).env (
ArrayLike|str|None) – The environment of each row: one label per row, strings or finite numbers, not both, or the name of a column of the DataFramedatathat holds them. The environment index then joins the search as a variable that no data variable causes; see Notes. Numbers are coerced tofloat64and read as numbers, whole numbers under a categorical or mixed-data setting; string labels are coded0, 1, ...in sorted order, whatever the row order. When every row has the same value, the data come from one environment, and the graph equals the one withoutenv.None, the default, fitsdataas one environment.score_func (
str|None) – Local score driving the GRaSP phase, as forgrasp():"bic", the linear-Gaussian BIC;"bdeu"or"discrete_bic"for categorical data, coded by whole numbers; or"degen_gauss"or"cond_gauss", the degenerate or conditional Gaussian BIC for mixed data.Nonepicks from the declared column types, as forgrasp().metadata["score_func"]names the score the fit used: the one picked from the declared column types when this isNone.penalty_discount (
float) – Multiplier on the BIC penalty oflog(n_samples)per parameter in the GRaSP phase. Larger values give sparser graphs;1.0withebic_gamma=0.0is the standard BIC. Must be finite and non-negative. BDeu ignores it.ebic_gamma (
float) – Weight on the extended-BIC term of the linear-Gaussian BIC: each variable’s score adds2 * ebic_gamma * log C(n_variables - 1, n_parents), which grows with the number of parent sets a variable can choose from. Larger values give sparser graphs;0.0is the standard BIC, the default as forgfci(). Must be from 0 to 1. Only"bic"uses it: passing it with another score raisesValueError.equivalent_sample_size (
float) – The equivalent sample size of the BDeu score: how many rows the uniform prior over each variable’s conditional distributions is worth. A larger value weighs the prior more against the data, and the graph can change with it. Must be finite and positive. Only"bdeu"uses it: passing it with another score raisesValueError.grasp_depth (
int) – Maximum number of tucks in one chain of moves of the GRaSP phase, at least1, as forgrasp(). Larger depths search more chains, which can reach a better score, and run longer.indep_test (
str|None) – Conditional-independence test for the FCI phase, as forgfci():"fisherz","student_t", and"normal_scores"for continuous data,"chisq"and"gsq"for categorical data,"degen_gauss"and"cond_gauss"for mixed data, and"kci","rcot","cmiknn", and"gpdc", which also detect nonlinear dependence in continuous data, at a far higher cost per test. It must read the same kind of data asscore_func.Nonepicks from the declared column types:"gsq"when every column is categorical,"cond_gauss"when some are, else"fisherz". A column is declared categorical bycategoricalor by a DataFrame’scategory,bool, or text type, never by its values.metadata["indep_test"]names the test the fit used: the one picked from the declared column types when this isNone.alpha (
float) – Significance level of the conditional-independence test. Smaller values give sparser graphs.collider_rule (
str) – How a collider is decided on a triplex - z - ywhere the tests removed the GRaSP edge betweenxandy, as forgfci(): by the recorded separating set ("sepsets"), or by a vote over the subsets ofx’s andy’s neighbors that separate them, which, if at least one subset separates them, makesza collider when none of them contains it and a non-collider when all do ("conservative"), or when fewer than half and when more than half do ("majority"). Any other triple is ambiguous, and no rule orients through it. On the other triples whose ends are not adjacent, the GRaSP CPDAG decides.categorical (
Sequence[str|int] |None) – The categorical columns ofdata, for the mixed-data score and test: DataFrame column names or 0-based positions.Nonetakes a DataFrame’scategory,bool, and text columns, never a column by its values.[]marks none. A categorical setting reads every column as categorical, and a continuous setting raisesValueErroron a declared column, named here or by its type. See Data types in the getting-started guide.edge_removal (
str) – Which edges of the GRaSP CPDAG the adjacency search may remove:"all"tests every edge, and"triangles"only the edges in a triangle of the CPDAG, whose ends share a neighbor in it, as Section 5 of GFCI’s paper [2] does. An edge in no triangle then stays in the PAG and still conditions the tests of the other pairs. The triangles come from the CPDAG, so the option adds no dependence on the column order. See Notes.confounded_index (
bool) – Withenv, whether a hidden variable may cause both the index and a data variable, as forfci_jci(): FCI-JCI1.Falseassumes the index is set from outside, as in a randomized experiment, so every edge the index keeps points out of it. Withoutenv, it has no effect.seed (
int|None) –None, the default, is the configured seed,ANDREY_SEEDor0when it is unset, not a fresh random one, so a fit repeats. Seed for the random starting order and the order in which the GRaSP phase visits variables, as forgrasp(), and for the random draws ofindep_test, which only"rcot","cmiknn", and"gpdc"make. Different seeds can reach different graphs.n_starts (
int) – Number of GRaSP searches, at least1, as forgrasp(), each from its own random starting order: the first fromseed’s, each later one from an order drawn fromseedand its own index. The test phase starts from the best-scoring search’s CPDAG.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. It caps every test of the FCI phase, as forgfci(), but not the GRaSP phase.max_parents (
int|None) – A cap on every node’s parents in the GRaSP phase, at least0, asgrasp()applies it. The test phase starts from that CPDAG’s adjacencies and removes edges, so a node of the PAG keeps at most its CPDAG neighbors; the cap does not bound the arrowheads the orientation rules put at a node.Nonesets no cap. Withenv, the index counts as a parent in the GRaSP phase of each variable it points into.knowledge (
Knowledge|None) – Background knowledge, aKnowledge: required and forbidden edges, the pairs allowed to be adjacent, and temporal tiers, all honored. The GRaSP phase honors it asgrasp()does, and the test phase starts from the adjacencies GRaSP found and never removes a required pair. In the PAG, a forbiddena -> bon an adjacent pair puts an arrowhead ataand can leave a circle atb, and a required one isa -> b. Withenv, the environment index is unconstrained.
- Returns:
Wraps a
pagGraphStructure;metadata["algorithm"]is"grasp_fci"andmetadata["seed"]the seed used. Endpoints carry arrowheads, tails, or circle marks (o); a bidirected edge (<->) marks a probable hidden common cause. Under a mixed-data setting,metadata["categorical"]lists the categorical columns it read, by name or 0-based position. Withcollider_rule"conservative"or"majority",metadata["ambiguous_triples"]lists the triples the rule left ambiguous, as[x, z, y]: 0-based column positions, the middle node second andx < y, in ascending order; withenv, the index, in a triple through it, is numberedn_variables. Withenv, the PAG is over the data variables, andmetadata["index_edges"]lists the index’s edges as[variable, kind]pairs, by name or 0-based position, in column order:"directed"forindex -> variable,"partially_directed"forindex o-> variable, and"bidirected"forindex <-> variable; it is empty for a single environment.metadata["tie_break"]is the tie-break rule the fit ran under,andrey.config.tie_break, andmetadataalso records the penalties its score used, under their parameter names.- Return type:
- Raises:
TypeError – If an argument has the wrong type, such as a string for a number; the message names the method, the argument, what it accepts, and the value. Also if
categoricalis not a list of column names and positions, orknowledgeis neitherNonenor aKnowledge.ValueError – If an argument lies outside its range or is not one of its choices; the message names the method, the argument, what it accepts, and the value. Also if
score_funcandindep_testread different data (continuous, categorical, or mixed), or ifdataorcategoricaldoes not suit them, as forgfci(). 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 if a givenmax_parentsis below the parents some graph of the required edges ofknowledgegives a node, or ifknowledgenames a columndatalacks or contradicts itself, asKnowledgelists. Also ifenvdoes not hold one label per row, misses a label or is infinite, or mixes strings and numbers, or, with a categorical or mixed-data setting, holds a number that is not whole.
- Warns:
DataTypeWarning – When the mixed-data score or test finds no declared categorical column and so runs as the linear-Gaussian form, or when a setting reads columns that look like category codes as numbers; each message warns once per process.
Notes
When to use. GRaSP-FCI suits i.i.d. observational continuous data with linear effects and Gaussian noise where unmeasured common causes are plausible [1]. Its authors simulate it in the setting of
boss_fci(): 20 measured variables with 0, 4, or 8 hidden common causes, mean degree 2 to 6, and 200 to 5,000 rows, and 100 measured variables with 10 hidden ones and 1,000 rows. They describe GRaSP’s search as favoring sparse graphs, a trade of accuracy against time beside BOSS’s. It returns a PAG, and withenvtakes data from several environments, asfci_jci()does. When no common cause is hidden, usegrasp();boss_fci()runs the same steps with BOSS’s search, andgfci()runs them with GES’s search and a possible-d-separation step.The steps are
boss_fci()’s, which follow the authors’ Algorithm 5 [1], with the CPDAG fromgrasp(). As in Algorithm 5, they leave out GFCI’s possible-d-separation step [2], which the authors conjecture is not needed. Andrey differs from the printed algorithm in how it removes edges, decides colliders, and derives separating sets, and it reads the final orientation rules as Zhang’s R1-R10, as that function’s Notes describe. No step adds an edge, so the PAG’s adjacencies are a subset of the GRaSP CPDAG’s. Reordering the columns reorders the nodes and changes nothing else, unless two columns are identical.Testing only the edges in triangles. With
edge_removal="triangles", the adjacency search tests only the CPDAG’s edges that lie in a triangle of it, a pair whose ends share a neighbor in the CPDAG. Every other edge stays in the PAG, and its ends still condition the tests of the other pairs, as a required edge’s do. Section 5 of GFCI’s paper [2] tests only these edges to save time, and its authors conjecture that this is always correct. On finite samples the two choices differ:"triangles"keeps both the true edges in no triangle that a test would remove and the false edges in no triangle that the GRaSP phase adds. It helps with a strict penalty and alpha, most on dense graphs, where the GRaSP phase adds few false edges and the tests remove more true ones, and hurts at the defaults on sparse graphs, where the GRaSP phase adds more false edges. On simulated data at the authors’ sizes [1] (see When to use), it lowered the mean endpoint errors (SHD against the true PAG) with theirpenalty_discount=2.0andalpha=0.01at every mean degree and size, from 639 to 586 at 100 variables (withmax_depth=7). At the defaults, it raised them with mean degree 2, from 21.1 to 23.4 at 20 variables, where it kept 2.8 false edges per graph against 1.8, and lowered them with mean degree 6, from 121 to 119. It did not shorten the fit in these runs: the median fit times differ by at most 0.01 s. The default"all"tests every edge, as Algorithm 5 does [1].Several environments. With
env, GRaSP-FCI runs asfci_jci()runs FCI [4]: the environment index joins the data as a last variable that leads every GRaSP order, and before the collider pass every edge the index keeps gets an arrowhead at its data variable, because no data variable causes the index, and, withconfounded_index=False, a tail at the index. Joint Causal Inference assumes no selection bias, so the rules leave out R5-R7. The index removes the edges that pooling adds and can orient more, and it is dropped from the PAG; a variable keeps an edge to it when its distribution changes across the environments and no other variable explains the change."bic"and"fisherz"read the index as a number, and the categorical and mixed-data settings each label as a category, as forfci_jci().Missing values. A
NaNindataraisesValueError, as forgfci(): its GRaSP phase scores every variable over the same rows. Drop the incomplete rows first, which suits values missing completely at random, or usefci(), whose tests delete rows per test.Limitations. Measured on its authors’ simulation [1], linear-Gaussian data with hidden common causes: 20 measured variables with 0, 4, or 8 hidden, mean degree 2, 4, or 6, and 200 to 5,000 rows, and 100 measured variables with 10 hidden, mean degree 6 or 10, and 1,000 rows, at a penalty discount of 2 with an alpha of 0.01 and at the defaults, on one core. Its errors (endpoint SHD against the true PAG) were below the empty graph’s in every setting, but much of the PAG stays wrong on these data, whose hidden nodes leave many pairs joined only through them: at 20 measured variables and mean degree 6 its errors were about three quarters of the empty graph’s, and at mean degree 2 with 8 hidden and 200 rows at the defaults, 33.3 against 37.8. Every fit at 20 measured variables took under 1 s, and at 100 under 3 s at mean degree 6 and up to 46 s at mean degree 10. Its tests also run it on 7 variables simulated on the ASIA network with one hidden, with 2,000 rows; on the 11 Sachs measurements with 853 rows; and, with the mixed-data score and test, on 5 variables with 3,000 rows. It runs a full
grasp()search before any test, so it takes at least as long as GRaSP on the same data. Without the possible-d-separation step, a pair that only a set outside both ends’ remaining neighbors separates keeps its edge, wheregfci()removes it, so the PAG can hold an edge too many and marks that differ from the true PAG’s. Its BIC score and Fisher’s z test assume linear effects with Gaussian noise. Withenv, its tests run it on up to 12 measured variables and 2 hidden ones, with 2,000 rows from two environments, and on Sachs’s 11 proteins, 7,466 cells from nine conditions, with"cond_gauss"."bic"and"fisherz"read a numericenvas a number, so with three or more environments a shift that does not follow the labels’ values can go unnoticed.max_parentscounts the index as a parent in the score phase. The numbers are a reference, not a hard limit: they change with the data generator, the density, the method’s settings, and the machine.References
Examples
>>> import numpy as np >>> from andrey import grasp_fci >>> rng = np.random.default_rng(0) >>> x = rng.standard_normal(500) >>> y = x + 0.1 * rng.standard_normal(500) >>> grasp_fci(np.column_stack([x, y]), seed=0).structure.kind 'pag'