# Adaptive Causal Discovery

> very_simple_matmcd: Causal Discovery with Statistical Tools + Domain Knowledge

- Skill: `tencent/adaptive-causal-discovery` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add tencent/adaptive-causal-discovery`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tencent/adaptive-causal-discovery/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Product & Planning
- Author: tencent (https://skillmd.com/u/tencent)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tencent/adaptive-causal-discovery

---

# 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:

```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 edge `i → j` (PC: forces edge into skeleton AND orients it; ES: treated as allowed)
  - `0` = forbid edge `i → j`
  - `-1` = no constraint (algorithm decides)
- **alpha**: significance level for PC independence tests (default `0.05`; use `0.1` for large/dense networks to improve recall — see Section 7)
- **Returns**: `adjacency_matrix` (n×n, standard convention: `[i][j]=1` means `i → 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]=1` forces 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 → B` that you are confident is wrong (e.g., reversed causation, clearly spurious given domain knowledge), set `C[A,B]=0` to forbid it and optionally `C[B,A]=1` to 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` (value `1`): statistically suggested directed edge
- `i — j` (value `-1`): undirected edge — statistically linked but direction unresolved by conditional independence tests
- `i ↔ j` (value `2`): 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:
1. All undirected edges from PC output (`-1` entries) — direction needs resolving
2. All directed edges from PC output — verify each against domain knowledge
3. High-confidence required edges (definitional, temporal, physical) that PC missed
4. 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 > 15` and the network is expected to be moderately dense (edges > n), **start with alpha=0.1**.
- If `n ≤ 15` or 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:
```python
# 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
```

