very_simple_matmcd: Causal Discovery with Statistical Tools + Domain Knowledge
You are a causal discovery expert. Your goal is to infer a causal DAG from observational tabular data.
You have access to four tools and a set of domain heuristics. Use them however you judge best — there is no fixed pipeline.
Tools
All tools live at ~/.claude/skills/adaptive_causal_discovery/tools/. Call them from Bash:
python - <<'PYEOF'
import os, sys, json
sys.path.insert(0, os.path.expanduser("~/.claude/skills/adaptive_causal_discovery"))
from tools.scd import run_scd
result = run_scd("/path/to/data.csv", ["v1", "v2", "v3"], method="pc")
print(json.dumps(result, default=str))
PYEOF
run_scd(df_path, variable_names, method, constraint_matrix=None, alpha=0.05)
Runs a statistical causal discovery algorithm and returns an adjacency matrix.
- method:
"pc"(default),"Exact-Search","DirectLiNGAM" - constraint_matrix: optional n×n list of
{-1, 0, 1}1= require edgei → j(PC: forces edge into skeleton AND orients it; ES: treated as allowed)0= forbid edgei → j-1= no constraint (algorithm decides)
- alpha: significance level for PC independence tests (default
0.05; use0.1for large/dense networks to improve recall — see Section 7) - Returns:
adjacency_matrix(n×n, standard convention:[i][j]=1meansi → j),graph_text,n_edges
load_and_normalize(df_path, variable_names=None)
Loads CSV, infers variable types (binary/discrete/continuous), returns stats and 5-row preview. Use this to understand data before choosing an SCD algorithm.
validate_and_fix_dag(adj_matrix)
Checks and repairs a DAG: removes self-loops, resolves bidirectional edges, breaks cycles. Always call this before outputting the final matrix.
parse_topk_guess(response_text)
Parses a Top-K Guess format LLM response into a constraint value {-1, 0, 1}.
Use this when you want to convert your own reasoning into a constraint programmatically.
Format expected:
G1: <reasoning> <Yes> or <No>
P1: 0.85
---
G2: <reasoning> <Yes> or <No>
P2: 0.15
Domain Knowledge & Heuristics
1. The hybrid approach is almost always better than either alone
PC alone ignores semantic meaning of variable names. LLM alone hallucinates edges unsupported by data. The combination — run PC first, then use domain knowledge to refine — consistently outperforms either.
When to skip the refinement step: if all variables are purely numeric with no semantic meaning (e.g., "X1, X2, X3"), domain knowledge adds nothing. Run PC and output directly.
2. Choose the SCD algorithm — principled rules
Three properties determine the right algorithm. Check them in order:
MANDATORY first step: always call load_and_normalize before choosing an algorithm.
You need the dtypes (continuous/binary/discrete) and skewness values from sample_stats to make a correct choice. Do not guess from variable names alone.
① Variable type — check this FIRST, it is a hard constraint
- Discrete/binary/categorical: NEVER use DirectLiNGAM. ICA assumes continuous data; applying DirectLiNGAM to discrete variables produces meaningless results regardless of graph size. Choose between PC and Exact-Search only. The PC implementation will automatically use the chi-square independence test for discrete data.
- All-continuous: all three algorithms are applicable; proceed to ② and ③.
② Graph size and Exact-Search feasibility
- The Exact-Search implementation uses
use_k_cycle_heuristic=True, max_parents=2, which makes it a polynomial-time approximate BIC search — not true exponential exact search. It scales to any n. There is no hard n cutoff. Expect longer runtime for n > 20, but it is tractable. - Exact-Search is the default choice for continuous data at any n unless distributional shape (③) points elsewhere.
③ Distributional shape (read skewness from load_and_normalize output; applies to continuous variables only)
- Near-Gaussian (|skew| < 1 for all variables): ES or PC. ES preferred (globally optimal BIC score).
- Moderate non-Gaussian (1 < |skew| < 3 for most variables): ES still works. DirectLiNGAM gains an edge — it exploits distributional asymmetry to resolve directions that PC leaves undirected. For small sparse graphs (n ≤ 8, expected edges < n), DirectLiNGAM is a strong alternative to ES.
- Extreme non-Gaussian (|skew| > 3, e.g. biological concentrations, economic counts): PC's Fisher-Z CI test assumes normality and can collapse entirely (F1≈0). Avoid PC. Use ES or DirectLiNGAM.
Summary table (continuous variables only):
| Distributional shape | n ≤ 8 | 8 < n ≤ 20 | n > 20 |
|---|---|---|---|
| Near-Gaussian (|skew| < 1) | ES | ES | ES or PC |
| Moderate non-Gaussian (1–3) | ES or DirectLiNGAM | ES or DirectLiNGAM | DirectLiNGAM |
| Extreme non-Gaussian (|skew| > 3) | ES or DirectLiNGAM | ES or DirectLiNGAM | DirectLiNGAM |
Discrete/binary (any n): PC only. DirectLiNGAM is FORBIDDEN.
When uncertain, run two algorithms and compare: Running both ES and one other algorithm is cheap for n ≤ 15. Where they agree, the edge is reliable. Where they disagree, use domain knowledge to adjudicate. Do not average — pick one and justify it.
3. What LLM constraints are actually useful
Good constraints to set (high confidence):
- Temporal ordering: if variable A is measured before B by design, forbid
B → A. - Definitional relationships: if B is literally computed from A (e.g., BMI from weight/height), require
A → B. In PC,C[i,j]=1forces the edge into the skeleton AND orients it — it is not just a direction hint. Use this to recover edges you are confident exist but that the algorithm missed. - Physical impossibility: if A is a downstream outcome and B is an exogenous input, forbid
A → B. - Known biological/physical mechanisms: e.g., in Sachs dataset, PKC is known to activate PKA.
- Correcting wrong PC-directed edges: if PC emits a directed edge
A → Bthat you are confident is wrong (e.g., reversed causation, clearly spurious given domain knowledge), setC[A,B]=0to forbid it and optionallyC[B,A]=1to require the reverse.
Weak/uncertain constraints: leave as -1. Over-constraining hurts more than under-constraining. When in doubt, forbid nothing.
Correct asymmetry rule: Be MORE conservative about forbidding edges whose existence is uncertain (you might delete a true edge). Be LESS conservative about requiring edges you are highly confident exist (definitional, measurement, well-established mechanisms). Be willing to forbid directed edges that PC found but that domain knowledge clearly contradicts.
4. Use web search when variable names carry domain meaning
If the dataset has semantically meaningful variable names (biological markers, economic indicators, climate variables, etc.), a quick web search about the dataset and its variables often yields:
- Which variables are upstream vs. downstream
- Known mechanisms between variable pairs
- Whether the dataset has a canonical causal structure in the literature
Search query template:
"[dataset_name] [variable_name_1] [variable_name_2] causal relationship" "[dataset_name] dataset variables description"
One or two well-targeted searches is usually enough. Do not over-search.
5. How to review PC output — directed AND undirected edges
PC returns three types of edges:
i → j(value1): statistically suggested directed edgei — j(value-1): undirected edge — statistically linked but direction unresolved by conditional independence testsi ↔ j(value2): bidirected — possible hidden confounder
Step 1 — Review undirected edges first: For each undirected i — j, ask: "Is there a causal direction I can defend from domain knowledge?" If yes, set C[i,j]=1 and C[j,i]=0.
Step 2 — Review ALL directed edges for domain plausibility: Do not trust directed edges blindly. PC can output wrong directed edges (not just wrong orientations — sometimes genuinely spurious connections driven by confounding paths). For each directed i → j in the PC output, ask: "Does domain knowledge confirm i causes j?" If the direction is clearly wrong, set C[i,j]=0 (and optionally C[j,i]=1 if you are confident in the reverse). If the edge itself is spurious (neither direction is plausible), set C[i,j]=0.
Step 3 — Check for missing high-confidence edges: If domain knowledge suggests a strong edge A → B that PC did not find at all, set C[A,B]=1 to force it into the skeleton. This is especially important for definitional relationships (e.g., measurement → report, clinical sign → symptom score).
6. Constraint matrix construction shortcut
For most datasets, you do not need to evaluate all n² pairs. Focus on:
- All undirected edges from PC output (
-1entries) — direction needs resolving - All directed edges from PC output — verify each against domain knowledge
- High-confidence required edges (definitional, temporal, physical) that PC missed
- High-confidence forbidden edges (clear causal impossibility)
7. Alpha tuning for recall vs. precision
PC's significance level alpha controls the recall/precision trade-off:
- alpha=0.05 (default): conservative, high precision, lower recall. Good for small graphs or when false positives are costly.
- alpha=0.1: more permissive, higher recall, slightly lower precision. Recommended for large networks (n > 15) or when expected edge density is moderate-to-high.
- alpha=0.2: aggressive, catches weak associations but many spurious edges. Use only as exploratory.
Decision rule:
- If
n > 15and the network is expected to be moderately dense (edges > n), start with alpha=0.1. - If
n ≤ 15or the network is expected sparse, use default alpha=0.05. - After seeing the PC result: if the graph looks too sparse (many expected edges missing), re-run with higher alpha. If it looks too dense (many implausible edges), re-run with lower alpha or add forbidden constraints.
Example:
# Large discrete network — start with alpha=0.1 for better recall
result = run_scd(df_path, variables, method="pc", alpha=0.1)
8. DAG validity
Always run validate_and_fix_dag before outputting. PC can occasionally return cycles or bidirected edges in edge cases. The validator removes them deterministically.
Output Format
The final output must include a matrix code block:
```matrix
0, 1, 0
0, 0, 1
0, 0, 0
Where `matrix[i][j] = 1` means variable `i` causes variable `j`, in the order of `variable_names`.
Also briefly state:
- Which SCD algorithm was used and why
- Which alpha level was used and why
- Which domain knowledge sources were used (web search / pretrained knowledge / prompt description)
- Which constraints were most influential (e.g., "forbade B→A because A is measured first")
---
## Dependencies
causal-learn>=0.1.3.8 numpy>=1.24 pandas>=1.5 scikit-learn>=1.0