API reference

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, as gfci() does, with GRaSP’s search over variable orders.

Parameters:
  • data (ArrayLike) – Observed data, one row per sample. Coerced to float64. A DataFrame’s column names become the structure’s labels. A column that env names is not a variable: it leaves the data before the fit, and categorical, 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 position 0, 1, ..., and a mapping labels each by its key: the rows are stacked in that order, and env stays None. 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 DataFrame data that holds them. The environment index then joins the search as a variable that no data variable causes; see Notes. Numbers are coerced to float64 and read as numbers, whole numbers under a categorical or mixed-data setting; string labels are coded 0, 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 without env. None, the default, fits data as one environment.

  • score_func (str | None) – Local score driving the GRaSP phase, as for grasp(): "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. None picks from the declared column types, as for grasp(). metadata["score_func"] names the score the fit used: the one picked from the declared column types when this is None.

  • penalty_discount (float) – Multiplier on the BIC penalty of log(n_samples) per parameter in the GRaSP phase. Larger values give sparser graphs; 1.0 with ebic_gamma=0.0 is 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 adds 2 * 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.0 is the standard BIC, the default as for gfci(). Must be from 0 to 1. Only "bic" uses it: passing it with another score raises ValueError.

  • 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 raises ValueError.

  • grasp_depth (int) – Maximum number of tucks in one chain of moves of the GRaSP phase, at least 1, as for grasp(). 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 for gfci(): "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 as score_func. None picks from the declared column types: "gsq" when every column is categorical, "cond_gauss" when some are, else "fisherz". A column is declared categorical by categorical or by a DataFrame’s category, 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 is None.

  • alpha (float) – Significance level of the conditional-independence test. Smaller values give sparser graphs.

  • collider_rule (str) – How a collider is decided on a triple x - z - y where the tests removed the GRaSP edge between x and y, as for gfci(): by the recorded separating set ("sepsets"), or by a vote over the subsets of x’s and y’s neighbors that separate them, which, if at least one subset separates them, makes z a 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 of data, for the mixed-data score and test: DataFrame column names or 0-based positions. None takes a DataFrame’s category, bool, and text columns, never a column by its values. [] marks none. A categorical setting reads every column as categorical, and a continuous setting raises ValueError on 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) – With env, whether a hidden variable may cause both the index and a data variable, as for fci_jci(): FCI-JCI1. False assumes the index is set from outside, as in a randomized experiment, so every edge the index keeps points out of it. Without env, it has no effect.

  • seed (int | None) – None, the default, is the configured seed, ANDREY_SEED or 0 when 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 for grasp(), and for the random draws of indep_test, which only "rcot", "cmiknn", and "gpdc" make. Different seeds can reach different graphs.

  • n_starts (int) – Number of GRaSP searches, at least 1, as for grasp(), each from its own random starting order: the first from seed’s, each later one from an order drawn from seed and 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 least 0: 0 runs only unconditional tests, and None sets 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 for gfci(), but not the GRaSP phase.

  • max_parents (int | None) – A cap on every node’s parents in the GRaSP phase, at least 0, as grasp() 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. None sets no cap. With env, the index counts as a parent in the GRaSP phase of each variable it points into.

  • knowledge (Knowledge | None) – Background knowledge, a Knowledge: required and forbidden edges, the pairs allowed to be adjacent, and temporal tiers, all honored. The GRaSP phase honors it as grasp() does, and the test phase starts from the adjacencies GRaSP found and never removes a required pair. In the PAG, a forbidden a -> b on an adjacent pair puts an arrowhead at a and can leave a circle at b, and a required one is a -> b. With env, the environment index is unconstrained.

Returns:

Wraps a pag GraphStructure; metadata["algorithm"] is "grasp_fci" and metadata["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. With collider_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 and x < y, in ascending order; with env, the index, in a triple through it, is numbered n_variables. With env, the PAG is over the data variables, and metadata["index_edges"] lists the index’s edges as [variable, kind] pairs, by name or 0-based position, in column order: "directed" for index -> variable, "partially_directed" for index o-> variable, and "bidirected" for index <-> variable; it is empty for a single environment. metadata["tie_break"] is the tie-break rule the fit ran under, andrey.config.tie_break, and metadata also records the penalties its score used, under their parameter names.

Return type:

StructureOutput

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 categorical is not a list of column names and positions, or knowledge is neither None nor a Knowledge.

  • 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_func and indep_test read different data (continuous, categorical, or mixed), or if data or categorical does not suit them, as for gfci(). 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 given max_parents is below the parents some graph of the required edges of knowledge gives a node, or if knowledge names a column data lacks or contradicts itself, as Knowledge lists. Also if env does 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 with env takes data from several environments, as fci_jci() does. When no common cause is hidden, use grasp(); boss_fci() runs the same steps with BOSS’s search, and gfci() 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 from grasp(). 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 their penalty_discount=2.0 and alpha=0.01 at every mean degree and size, from 639 to 586 at 100 variables (with max_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 as fci_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, with confounded_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 for fci_jci().

Missing values. A NaN in data raises ValueError, as for gfci(): its GRaSP phase scores every variable over the same rows. Drop the incomplete rows first, which suits values missing completely at random, or use fci(), 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, where gfci() 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. With env, 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 numeric env as a number, so with three or more environments a shift that does not follow the labels’ values can go unnoticed. max_parents counts 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'