Skip to content

Causal Discovery Methods

sat edited this page Jul 15, 2026 · 1 revision

Causal Discovery Methods

This page explains the causal-discovery algorithms that logdag can apply when it builds a DAG for one analysis job, what each one means in practice, the single config option that selects between them, and how to choose. Each algorithm lives in its own method-adapter module (*_input.py) that the DAG-generation core (makedag.estimate_dag) calls. The job-splitting, time-binning, and output layout around these calls are covered in Generating DAGs; every setting named here is listed in Configuration Options.

Grounding: this page is written against logdag/makedag.py (estimate_dag), the adapter modules logdag/pc_input.py and logdag/lingam_input.py, the prior-knowledge feed logdag/pknowledge.py, and the [dag] / [lingam] sections of logdag/data/config.conf.default. Method names and option names are 1:1 with that code/config. Nothing below asserts an algorithmic property the code does not implement.

The method-adapter pattern

For each analysis job, logdag assembles an input DataFrame (rows = time bins, columns = event ids) and then calls makedag.estimate_dag(conf, input_df, prior_knowledge). That function is a thin dispatcher: it reads one config value, [dag] cause_algorithm, and forwards the DataFrame to the matching adapter module. Each adapter is a small wrapper around an external causal-discovery library that returns a networkx.DiGraph. From a user's point of view this means:

  • You select a method by setting one option, [dag] cause_algorithm.
  • Each method has its own parameters, read from the relevant config section ([dag] for PC-family options, [lingam] for LiNGAM options).
  • The output type is uniform (a NetworkX directed graph that becomes a LogDAG), regardless of which method ran, so the rest of the pipeline (post-processing, viewing, filtering) does not change with the method.

estimate_dag short-circuits before dispatch: if the input has fewer than two columns (fewer than two event variables), it logs input too small and returns an empty DAG without invoking any method. So all methods below require at least two event variables in the job.

Selecting a method: [dag] cause_algorithm

The selector is a single string option:

[dag]
# Method to estimate causal DAG
# pc in default, and lingam (LiNGAM-fast) is also available
cause_algorithm = pc

The values actually handled by makedag.estimate_dag are exactly:

cause_algorithm Adapter module / function Library Output edges
pc (default) pc_input.pc pcalg + a CI-test backend PC-estimated CPDAG
pc-corr pc_input.pc (depth forced to 0) pcalg + a CI-test backend pairwise (unconditional) PC graph
lingam lingam_input.estimate lingam weighted directed edges (full fit)
lingam-corr lingam_input.estimate_corr lingam weighted directed edges (pairwise fit)

Any value not in the table is looked up as an out-of-tree plugin via the logdag.cause_algorithm entry-point group; only if no plugin is registered for that name does it raise ValueError("invalid dag.cause_algorithm"). See Extending logdag for the plugin mechanism.

The companion option for the PC family is the conditional-independence test:

[dag]
# Method to estimate conditional independency
# [fisherz, fisherz_bin, gsq, gsq_rlib] is available
ci_func = gsq

ci_func is read by estimate_dag and passed to the PC adapter. The adapter's own pc() implements three of these modes — gsq, fisherz, and fisherz_bin — by importing the matching test function (gsq.ci_tests or citestfz.ci_tests). gsq is the default and binarizes the input before the test; the fisherz variants use a Gaussian (Fisher-z) test. The config comment also lists gsq_rlib, but the R-based path in pc_input.py is commented out, so selecting gsq_rlib would fall through to the adapter's ValueError("ci_func invalid"). Treat gsq, fisherz, and fisherz_bin as the supported CI tests.

PC algorithm (pc, pc-corr)

The PC family is logdag's default and is implemented in pc_input.py on top of the pcalg dependency. estimate_dag reads these [dag] options and hands them to pc_input.pc:

Option Meaning in pc_input
skeleton_method method= for pcalg.estimate_skeleton. stable (default) is order-independent; default is the original PC skeleton (the config comment notes it is faster but less accurate on sparse data).
skeleton_threshold alpha, the p-value threshold for the CI test (default 0.01).
skeleton_depth max_reach, the maximum size of the conditioning set. -1 means no limit (only passed through when >= 0).
skeleton_verbose verbose flag for the skeleton search (debugging).
ci_func which CI test backend to use (see above).

What the adapter does:

  1. Build an initial skeleton — a pruned_initial_skeleton() from prior knowledge if supplied (see Prior knowledge), otherwise a complete graph over all event columns.
  2. For gsq, binarize the input (x >= 1 -> 1); for the Fisher-z modes use the data as-is.
  3. Run pcalg.estimate_skeleton then pcalg.estimate_cpdag, returning a CPDAG.

So pc produces a CPDAG (a partially directed graph: undirected edges remain where the algorithm cannot orient them). Edges carry no weights.

pc-corr is the same PC path with skeleton_depth forced to 0. With a conditioning-set depth of zero the algorithm only tests unconditional (marginal) independence between pairs, so it effectively yields a correlation-like adjacency rather than a conditioned causal skeleton. Use it as a fast, pairwise baseline; it does not perform the conditioning that distinguishes PC from simple correlation.

LiNGAM (lingam, lingam-corr)

LiNGAM is implemented in lingam_input.py on top of the lingam package. Parameters come from the [lingam] section plus the LiNGAM-specific defaults:

[lingam]
algorithm = ica
lower_limit = 0.01
ica_max_iter = 1000
Option Meaning
algorithm ica -> lingam.ICALiNGAM; direct -> lingam.DirectLiNGAM. Any other value raises ValueError("invalid lingam algorithm name").
lower_limit edges whose absolute coefficient is <= lower_limit are dropped (default 0.01).
ica_max_iter max_iter for the ICA fit (used by the ica algorithm; passed to estimate, not to estimate_corr).

cause_algorithm = lingam calls lingam_input.estimate, which fits one LiNGAM model over all event columns and returns a DiGraph whose edges carry the fitted coefficient as a weight (and a rounded label). Unlike PC, LiNGAM produces fully oriented, weighted edges.

Prior knowledge behaves differently per LiNGAM variant, as wired in the adapter:

  • algorithm = ica: prior knowledge is not used — the adapter emits a warning (ICA-LiNGAM does not use prior knowledge) if any is supplied.
  • algorithm = direct: prior knowledge, if present, is converted via prior_knowledge.lingam_prior_knowledge() into a DirectLiNGAM prior-knowledge matrix.

The ICA fit is retried up to a few times on numpy.linalg.LinAlgError; if it still fails, estimate returns None. (Callers in makedag therefore must tolerate a None DAG for LiNGAM jobs.)

cause_algorithm = lingam-corr calls lingam_input.estimate_corr, which fits a LiNGAM model pairwise over every pair of event columns and unions the resulting edges. It uses algorithm and lower_limit (not ica_max_iter), and applies direct-mode prior knowledge per pair. Like pc-corr, it is a pairwise variant — heavier than a single global fit because it fits one model per pair, but it avoids conditioning all variables jointly.

Prior knowledge

Several methods can consume prior knowledge (a domain skeleton / orientation hints), configured under [prior_knowledge] (methods, topology files, import rules) and built by logdag/pknowledge.py. estimate_dag receives a prior_knowledge object and forwards it to the adapter. Two feeds exist:

  • pruned_initial_skeleton() — a pruned starting graph for the PC skeleton search; used by pc and pc-corr.
  • lingam_prior_knowledge() — a LiNGAM prior-knowledge matrix; used by lingam and lingam-corr only when [lingam] algorithm = direct.

If [prior_knowledge] methods is empty, no prior knowledge is built and methods start from a complete graph (PC family) or no constraints (LiNGAM). See Configuration Options for the full [prior_knowledge] section.

Choosing a method

The trade-offs below are grounded in what the adapters actually do (edge type, orientation, weighting, conditioning), not in external benchmark claims.

If you want... Use Why
A sensible default for log-event causality pc Default; conditioned independence via pcalg, order-independent skeleton with stable. Produces a CPDAG.
Fully oriented edges with strength values lingam LiNGAM returns directed, weighted edges; PC leaves some edges undirected.
A fast pairwise baseline (no conditioning) pc-corr or lingam-corr Both restrict to pairwise relations (pc-corr forces conditioning depth 0; lingam-corr fits per pair).
To incorporate a known network topology as orientation hints lingam with [lingam] algorithm = direct, or any PC variant DirectLiNGAM and the PC skeleton consume prior knowledge; ICA-LiNGAM ignores it.

Practical notes from the code:

  • CI test choice (ci_func) matters for the PC family. gsq (default) binarizes the input — appropriate for sparse, count-like log-event data. fisherz / fisherz_bin assume a Gaussian setting. gsq_rlib is not implemented in the current adapter.
  • skeleton_threshold controls density for the PC family: a larger alpha keeps more edges (more false positives), a smaller one keeps fewer.
  • skeleton_depth = -1 (no conditioning-set limit) is the default and can be expensive on dense inputs; bound it with a non-negative depth to cap runtime.
  • LiNGAM edge filtering is controlled by [lingam] lower_limit: raise it to suppress weak edges.
  • Empty / tiny jobs never reach any method — fewer than two event variables yields an empty DAG by design.

See also

  • Generating DAGs — how jobs, input DataFrames, and outputs are produced around estimate_dag.
  • Configuration Options — full reference for [dag], [lingam], and [prior_knowledge].

Clone this wiki locally