Causal Identification

SkillDev tools

Validate causal claims using DAG adjustment sets, bad-control detection, and refutation tests. Use when distinguishing genuine factor effects from confounded associations.

Available today. Use it from your connected AI after setup.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Then ask your AI: use the Causal Identification skill

What this skill tells your AI

The instructions your AI receives, as published by ml4t/skills in concepts/causal-identification/SKILL.md and read by ahel’s review.

A factor with IC 0.04 could be a real effect or a confounded association. Without a DAG and refutation tests, you cannot tell which. Conditioning on the wrong variables - mediators, colliders, post-treatment - can create or destroy apparent signal.

The Problem

"Kitchen sink regression" - conditioning on every available variable - is the default in ML pipelines. But including a collider (e.g., fund flows driven by both momentum and returns) induces spurious correlation (~-0.25 between independent variables). Including a mediator (the channel through which the treatment operates) attenuates the true effect. Including a post-treatment variable introduces bias of unknown sign. The DAG determines which variables are admissible controls.

The Pattern

WRONG

import numpy as np
from sklearn.linear_model import Ridge

# Kitchen-sink: include everything as controls
# fund_flow is a COLLIDER (driven by both momentum and returns) - induces bias
X = np.column_stack([momentum, volatility, fund_flow, sector_return])
model = Ridge().fit(X, forward_returns)
print(f"Momentum coeff: {model.coef_[0]:.4f}")  # Biased by collider conditioning

CORRECT

from dowhy import CausalModel

# Step 1: Specify DAG - encode your mechanism assumptions
graph = """
digraph {
    volatility -> momentum;
    volatility -> forward_returns;
    momentum -> forward_returns;
    momentum -> fund_flow;
    forward_returns -> fund_flow;
}"""
# fund_flow is a collider (momentum -> fund_flow <- forward_returns)
# It must NOT be in the adjustment set

# Step 2: Identify estimand from the DAG
model = CausalModel(data=df, treatment="momentum",
                     outcome="forward_returns", graph=graph)
estimand = model.identify_effect()
# DoWhy computes the backdoor adjustment set: {volatility}

# Step 3: Estimate with valid controls only
estimate = model.estimate_effect(estimand, method_name="backdoor.linear_regression")
print(f"Causal effect: {estimate.value:.4f}")

Adjustment Set Rules

Variable RoleInclude as Control?Why
Confounder (common cause of T and Y)YesBlocks backdoor paths
Pre-treatment predictor of YYesImproves precision
Mediator (on causal path T→M→Y)NoChanges estimand
Collider (common effect of T and Y)NoInduces spurious correlation
Post-treatment variableNoBias of unknown sign

Pre-treatment timing discipline: only condition on variables determined strictly before treatment time. Do not condition on portfolio outcomes, realized performance, or contemporaneous market variables.

Refutation Tests

Every causal claim must survive refutation before informing trading decisions:

# Placebo treatment: replace momentum with random noise - effect should vanish
placebo = model.refute_estimate(estimand, estimate, method_name="placebo_treatment_refuter")
print(f"Placebo effect: {placebo.new_effect:.4f}")  # Should be ~0

# Sensitivity: how strong must an omitted confounder be to flip the sign?
sensitivity = model.refute_estimate(estimand, estimate,
    method_name="add_unobserved_common_cause",
    confounders_effect_on_treatment="linear", confounders_effect_on_outcome="linear",
    effect_strength_on_treatment=0.5, effect_strength_on_outcome=0.5)

If a confounder at 10-20% effect strength flips the sign, the result is fragile.

Guardrails

  • Specify the DAG before fitting - post-hoc DAGs rationalize results instead of testing assumptions
  • Never condition on colliders - the fund-flow collider trap creates ~-0.25 spurious correlation between independent variables
  • Enforce pre-treatment timing - all controls must be determined strictly before treatment time
  • Placebo tests are mandatory - a pipeline that finds effects with random treatment is broken
  • Sensitivity analysis calibrates confidence - report the confounder strength at which the effect flips sign
  • Causal discovery (PCMCI, NOTEARS) generates hypotheses, not conclusions - validate discovered structure with independent data

Production Implementation

No ml4t-* library covers causal estimation. Use DoWhy for identification and refutation, EconML for Double Machine Learning on continuous treatments, and tfp-causalimpact for discrete event studies:

from econml.dml import LinearDML
from sklearn.ensemble import GradientBoostingRegressor

dml = LinearDML(model_y=GradientBoostingRegressor(), model_t=GradientBoostingRegressor())
dml.fit(Y=returns, T=momentum, W=confounders)  # W = valid adjustment set from DAG
print(f"ATE: {dml.ate():.4f}, 95% CI: {dml.ate_interval()}")

Checklist

  • DAG specified and committed before any estimation
  • Adjustment set derived from backdoor criterion - no colliders, mediators, or post-treatment variables
  • Estimand declared (ATE, ATT, or CATE) before fitting
  • Placebo treatment test returns near-zero effect
  • Sensitivity analysis reports the confounder strength that flips the sign
  • Results stable across subperiods and alternative nuisance model specifications

Signals

GitHub stars
20
Forks
11
Last commit
Sep 2026
Advanced
Item type
skill
Key
ml4t-causal-identification
Source
github.com/ml4t/skills