-
Notifications
You must be signed in to change notification settings - Fork 3
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 moduleslogdag/pc_input.pyandlogdag/lingam_input.py, the prior-knowledge feedlogdag/pknowledge.py, and the[dag]/[lingam]sections oflogdag/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.
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.
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 = pcThe 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 = gsqci_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.
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:
- Build an initial skeleton — a
pruned_initial_skeleton()from prior knowledge if supplied (see Prior knowledge), otherwise a complete graph over all event columns. - For
gsq, binarize the input (x >= 1 -> 1); for the Fisher-z modes use the data as-is. - Run
pcalg.estimate_skeletonthenpcalg.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 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 viaprior_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.
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 bypcandpc-corr. -
lingam_prior_knowledge()— a LiNGAM prior-knowledge matrix; used bylingamandlingam-corronly 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.
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_binassume a Gaussian setting.gsq_rlibis not implemented in the current adapter. -
skeleton_thresholdcontrols density for the PC family: a largeralphakeeps 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.
-
Generating DAGs — how jobs, input DataFrames, and outputs
are produced around
estimate_dag. -
Configuration Options — full reference for
[dag],[lingam], and[prior_knowledge].