andrey.score_matching#
- andrey.score_matching(data, *, alpha=0.001, knowledge=None)[source]#
Learn a DAG with SCORE, a causal order from the Jacobian of the score (additive noise).
SCORE [1] models each variable as a nonlinear function of its parents plus Gaussian noise,
x_j = f_j(parents) + e_j. Under that model the score of the joint density,grad log p(x), marks the leaves: the derivative of its entryjinx_jis the same at every point exactly whenjhas no children [1]. SCORE estimates those derivatives at the rows by a kernel estimator built on Stein’s identity [2], takes the variable whose derivative varies least as the next leaf, repeats on the others, and prunes the complete DAG of the order by the smooth-term tests of CAM [3]. It returns a DAG and a causal order.- Parameters:
data (
ArrayLike) – Observed data, one row per sample. Coerced tofloat64. A DataFrame’s column names become the structure’slabels.alpha (
float) – Significance level of the pruning test, in(0, 1): a parent stays when its smooth term’s p-value is at mostalpha. The causal order does not read it.0.001is the value of the paper’s experiments.knowledge (
Knowledge|None) – Background knowledge, aKnowledge: required and forbidden edges, the pairs allowed to be adjacent, and temporal tiers, all honored. A variable is the next leaf only when no remaining variable is in a later tier or is the effect of a required edge out of it. Forbidden edges, edges into an earlier tier, and pairs outside the allowed set are in no pruning fit and never in the result; the score estimate still reads every remaining variable. The pruning never removes a required edge.
- Returns:
Wraps a
dagGraphStructure;metadata["algorithm"]is"score_matching".orderingis the causal order of the leaf search, as a tuple of column indices, causes first.metadata["tie_break"]is the tie-break rule the fit ran under,andrey.config.tie_break.- 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
knowledgeis 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
datais not a usable 2-D matrix: not numeric, holdingNaNorinf, with fewer than twice as many rows as columns, or with a constant column, a column of fewer than 3 distinct values, or a column that repeats another up to scale and shift, each named in the message; or if more than half of the pairs of rows are equal on the remaining variables at a step, which leaves the kernel no bandwidth. Also ifknowledgenames a columndatalacks or contradicts itself, asKnowledgelists.
- Warns:
DataTypeWarning – When a column holds at most 10 distinct whole numbers and so may hold category codes, which the fit reads as numbers. Each message warns once per process.
Notes
When to use. SCORE suits i.i.d. continuous observational data whose effects are nonlinear in every parent, with additive Gaussian noise and no unmeasured common causes [1]; under that model the DAG is identified [4], and it returns the DAG with a causal order. Its order search fits no regression: each step is one kernel estimate over all the remaining variables, so its cost grows with the number of variables only linearly, and with the cube of the rows. The paper shows it on 10 to 50 variables and 1,000 rows. A link that is linear in a parent leaves that parent’s derivative constant too, so it can be taken for a leaf: with linear effects and Gaussian noise use
pc()orges(), which return the Markov equivalence class. For additive noise that is not Gaussian useresit(), and for the same model searched by regressions,cam().The steps. The paper’s two phases [1]:
Causal order. The score’s Jacobian has the diagonal entries
d^2 log p(x) / d x_j^2. They are estimated at every row of the remaining variables by the paper’s extension of the Stein gradient estimator [2] to second derivatives (its Eq. 12), with the Gaussian kernelexp(-||x - y||^2 / (2 s^2)), the bandwidthsthe median distance between the rows, and one ridge term for both of its regressions. The variable whose entry has the smallest variance over the rows is the next leaf; it is removed, and the estimate, bandwidth included, is made again on the others. The causal order is the leaves in reverse.Pruning. Each variable is fit on all the variables before it in the order, an additive model of penalized thin plate regression splines with ten basis functions per parent and smoothing chosen by GCV [5], with
ceil(n_samples / (3 n_parents))functions per term, at least 3, when there are fewer than 30 rows per term, and at most one per distinct value. A parent stays when its smooth term’s p-value is at mostalpha, the pruning ofcam()[3].
The choices the paper leaves open. It states no value of the ridge term: it is
0.001, the value in the code published with the paper, added to the kernel matrix as the paper’s Eq. 12 writes it, a matrix with ones on its diagonal. The median distance is taken over the pairs of distinct rows. The variances are compared as estimated, as the paper’s Algorithm 1 states, not divided by the entries’ means.One kernel reads all the remaining columns, so the estimate depends on their units, which the model does not: a leaf’s entry is constant in any units. The columns are therefore centered and divided by their standard deviations before the order search, so rescaling or shifting a column changes no step; the pruning reads each variable through an affine map to a fixed interval. The paper’s experiments run on the columns as simulated. The fit draws no random numbers and runs on one BLAS thread.
Ties. The variances are compared rounded to 9 decimal places, as
hc()compares its score changes per row, so two variances that differ only in their rounding tie. A tie goes to the variable with the lower rank computed from the data, as forhc(); the fit runs on the columns in that rank order, so the result does not depend on the column order unless two columns are identical. The column-order guide says what the rank keeps under a new column order.Missing values. A
NaNindataraisesValueError: every fit reads every row of its variables, so a row with a missing value has no place in it. Drop the incomplete rows first, which suits values missing completely at random, or usepc()orfci(), whose tests delete rows per test.Limitations. The numbers below were measured on the paper’s simulation with standardized columns, Gaussian noise, and 1,000 rows, on one core, 20 graphs per setting: Erdos-Renyi graphs with as many edges as variables or four times as many, each link one draw of a Gaussian process, at 10, 20, and 50 variables. With as many edges as variables the order put 81%, 73%, and 65% of the true edges in the true direction at 10, 20, and 50 variables, and the DAG was 2.8, 7.8, and 25.0 edges from the true one (SHD), where the empty graph is 10, 20, and 50 away; with four times as many edges the order put 97%, 90%, and 85% in the true direction, and the SHD was 22.1, 51.9, and 142.7 against the empty graph’s 40, 80, and 200. On the graphs with as many edges as variables
cam()was more accurate on the same data: its order put 96% to 98% of the true edges in the true direction and its SHD was 0.7, 2.1, and 5.5. At 50 variables with four times as many edges, over 40 graphs, the order put 0.007 to 0.010 less of the true edges in the true direction than the code published with the paper did (86% against 87%). The paper’s tables are on data as simulated, and the order search reads standardized columns, where it orders fewer edges of a sparse graph correctly: in a separate check on ten graphs of 10 variables with as many edges, the same search put 99% of the true edges in the true direction when it read the columns as simulated and 80% on standardized columns. On standardized columns the error of the search’s estimate for a variable grows with about the square of that variable’s variance-to-noise ratio (its variance over the variance of its noise term). Where links have a strong linear part, variance builds up along the causal order, the sinks have the largest ratios, and the search ranks them last: on additive-noise data with linksz + sin z(1,000 rows, 10 and 20 variables, as many edges as variables), where a sink’s ratio had a median of 10 to 13 against 1.6 to 1.7 in the paper’s simulation, the order put 41% to 53% of the true edges in the true direction, a random order’s share, and 16,000 rows (52%), another ridge term, or another bandwidth did not change that. Usecam()on such data (76% to 94% on the same datasets). One kernel bandwidth serves every variable, and links it does not resolve leave a cause’s entry as flat as a leaf’s: onx0 -> x2 <- x1,x2 -> x3withx2 = 2 x0 / (1 + x0^2) + 2 / (1 + x1^2),x3 = x2^2 / 2, and noise of standard deviation 0.7 or 1, over 400 and 800 rows, the search tookx0andx1for the leaves in 24 of 24 fits. The leaf rule is exact only for Gaussian noise: under other noise a leaf’s entry changes with its residual, andnogam()andresit()drop that assumption. A fit took 1.0 s at 10 variables, 3.0 s at 20, and 28 to 32 s at 50, within 0.16 GB. Each step factors a matrix with one row and one column per sample, so the order search’s time grows with the cube ofn_samplesand its memory with the square. The pruning starts from the complete DAG of the order, so its last fits have nearlyn_variablessmooth terms each: it needs at least twice as many rows as columns, and it takes most of a fit’s time as the variables grow. Binary columns, and any column with fewer than 3 distinct values, are not supported: the pruning fits a smooth term of each parent, and such a column raisesValueErrorbefore the fit. 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 score_matching >>> rng = np.random.default_rng(0) >>> x = rng.standard_normal(300) >>> y = np.sin(2 * x) + 0.3 * rng.standard_normal(300) >>> out = score_matching(np.column_stack([x, y])) >>> out.structure.kind, out.ordering ('dag', (0, 1))