andrey.calm#
- andrey.calm(data, *, seed=None, **params)[source]#
Learn a weighted DAG with CALM, a continuous-optimization structure learner.
CALM (Continuous and Acyclicity-constrained L0-penalized likelihood with an estimated Moral graph) [1] fits a linear-Gaussian DAG with torch. By default it keeps only the candidate edges of an estimated moral graph, then minimizes the Gaussian likelihood plus a penalty per edge under an acyclicity constraint. The optimizer draws random numbers and is seeded on every call; see
seed.- Parameters:
data (
ArrayLike) – Observed data, one row per sample. Coerced tofloat64. A DataFrame’s column names become the structure’slabels.seed (
int|None) – Seed for the optimizer’s torch random number generator.Noneuses theANDREY_SEEDenvironment variable, or0when it is unset, so every call is seeded and repeated calls return the same graph on one machine with the same torch version. The draws go to the columns in order of each column’s rank, as inhc()(the column-order guide).metadata["seed"]records the seed used.**params (
Any) –Optimizer settings passed to the solver; a name not listed here raises
TypeError.lambda1(default0.005): penalty per edge. Larger values give sparser graphs.alpha(default0.01): significance level of the Fisher-Z tests that estimate the moral graph. Only pairs it keeps can become edges; smaller values keep fewer.use_moral_graph(defaultTrue):Falsemakes every pair a candidate edge.standardize(defaultFalse): scale each column to unit variance first.w_threshold(default0.3) andgate_threshold(default0.5): an edge is kept when its weight magnitude and its gate probability reach these values.max_outer(default25),subproblem_iter(default3000), andlr(default0.01): quadratic-penalty rounds, Adam steps per round, and learning rate.tau(default0.5),rho_init(default0.001),rho_mult(default3.0),rho_max(default1e16), andh_tol(default1e-8): gate temperature and the acyclicity-penalty schedule.labels(defaultNone): node names; a DataFrame’s column names take precedence.
- Returns:
Wraps a
dagGraphStructure;weighted_adjacency[i, j]is the coefficient of edgei -> j.metadataholds"algorithm"("CALM"), the"seed"used,"n_edges","objective"(half the sum of the log residual variances pluslambda1times the edge count; lower is better), and"h_final"(the acyclicity residual of the returned graph,0when it is acyclic).- Return type:
- Raises:
ImportError – If the
[torch]extra is not installed.TypeError – If
paramsholds a name the solver does not accept.ValueError – If the fit’s loss or weights stop being finite, for example when the data’s scale overflows its covariance;
standardize=Truerescales the columns.
Notes
CALM first estimates the moral graph with IAMB [2]: for each variable it adds, one at a time, the variable most dependent on it given those already added (Fisher-Z at level
alpha), then drops any member the others make independent of it. The most dependent variable has the largest absolute partial correlation; equal ones are chosen by each column’s rank, as inhc(). A pair is a candidate edge, in either direction, when each is in the other’s set.Each candidate edge
i -> jhas a weight and a gate. During the fit a gate is a Gumbel-Softmax sample [3],sigmoid((logit + noise) / tau), solambda1times the sum of the gates approximates a penalty per edge. WithBthe gated weights andSthe sample covariance, the fit minimizes the Gaussian negative log-likelihood with one noise variance per variable [4],0.5 * sum_j log(((I - B).T @ S @ (I - B))[j, j]) - log|det(I - B)|, plus that penalty and0.5 * rho * h**2. Hereh = tr((I + G / d)**d) - d[5] for the gatesGanddvariables, andhis zero exactly when the gated graph is acyclic. Adam runs up tosubproblem_itersteps per round; a round ends early once, after 50 steps, the loss changes by less than a1e-7fraction from one step to the next. Between roundsrhogrows by the factorrho_mult, the quadratic penalty method [6], untilhof the noise-free gatessigmoid(logit / tau)is at mosth_tol,rhoreachesrho_max, ormax_outerrounds have run.If the edges that
gate_thresholdandw_thresholdkeep still form a cycle, edges are removed in order of increasing weight magnitude until none does.The defaults are a shorter schedule than the paper’s [1], which starts
rhoat1e-5, runs 40,000 steps per round at learning rate0.001, and keeps every edge whose gate reaches0.5.rho_init=1e-5, subproblem_iter=40000, lr=0.001, w_threshold=0.0with a largermax_outerruns that schedule.Limitations. Measured on simulated linear-Gaussian data from sparse random graphs (mean degree 2), from 10 to 50 variables with ten rows per variable. Its median errors (SHD) stayed below the empty graph’s at every size (15 against 50 at 50 variables). The median fit took 18 s at 10 variables and 59 s at 50, on 4 cores. The paper’s schedule (
lr=0.001,max_outer=100,subproblem_iter=40000,rho_init=1e-5,w_threshold=0.0) took 174 s and 472 s for median errors of 6 and 12. Every optimizer step computes a determinant and a matrix power of the full weight matrix, one row and one column per variable, so a step’s cost grows with the cube of the variable count; the defaults allow up to 75,000 steps (max_outertimessubproblem_iter). The fit is nonconvex and can stop at a local optimum: on a collider whose two parents are independent, it can return both edges reversed and an edge between the parents, with the defaults and with the paper’s schedule alike.References
Examples
>>> import numpy as np >>> from andrey import calm >>> rng = np.random.default_rng(0) >>> x = rng.standard_normal(500) >>> y = x + 0.1 * rng.standard_normal(500) >>> out = calm(np.column_stack([x, y]), seed=0) >>> out.structure.kind 'dag'