Install
$ agentstack add skill-tondevrel-scientific-agent-skills-dowhy ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
DoWhy — Causal Inference
DoWhy answers the question every analyst actually wants answered: "Does X cause Y, or is it just correlated?" Correlation is everywhere. Causation requires structure — a causal graph that encodes which variables influence which. DoWhy's workflow is three steps: Identify (is the effect estimable from this graph?) → Estimate (compute the effect) → Refute (is this estimate robust?).
Core Mental Model
CORRELATION: X and Y move together. Could be:
X → Y (X causes Y)
Y → X (Y causes X)
X ← C → Y (C confounds both — spurious!)
CAUSATION: We need to know WHY they move together.
A causal graph (DAG) encodes our assumptions.
Then math tells us: "Given this graph,
CAN we estimate the causal effect from data?"
→ If yes: which variables to control for?
→ If no: what additional data do we need?
When to Use
- "Does this ad campaign actually increase sales, or do people who see ads already buy more?"
- "Does smoking cause cancer?" (observational data, can't randomize)
- "What would revenue have been if we hadn't changed the pricing?" (counterfactual)
- Any analysis where confounders exist and you have a theory about the causal structure.
When NOT to use: Pure prediction (use sklearn). Randomized controlled trials with no confounders (simple A/B test suffices). When you have no theory about the causal structure — you need at least a hypothesis about the DAG.
Reference Documentation
DoWhy docs: https://dowhy.readthedocs.io/en/latest/ GitHub: https://github.com/py-why/dowhy Causal graph tutorials: https://dowhy.readthedocs.io/en/latest/tutorials.html Search patterns: CausalModel, identify_effect, estimate_effect, refute_estimate
Core Principles
The Causal Graph (DAG)
A Directed Acyclic Graph where arrows mean "causes". A → B means A is a cause of B. This is your assumptions about the world — not learned from data. You draw it based on domain knowledge. The graph is what makes causal inference possible.
The Identify-Estimate-Refute Loop
- Identify: Given the DAG, is the causal effect of treatment on outcome estimable from observational data? Which variables must be controlled? (Backdoor criterion, frontdoor criterion, instrumental variables.)
- Estimate: Compute the effect using the identified strategy and a statistical method.
- Refute: Test robustness — would the estimate survive if our assumptions were wrong?
Confounders Are the Enemy
A confounder C is a common cause of both treatment and outcome: T ← C → Y. It creates spurious correlation. The entire point of causal inference is to block these backdoor paths.
Treatment Effect Types
- ATE (Average Treatment Effect): Effect across the whole population.
- ATT (Average Treatment Effect on the Treated): Effect on those who actually received treatment.
- CATE (Conditional ATE): Effect varies by subgroup — heterogeneous treatment effects.
Quick Reference
Installation
pip install dowhy networkx matplotlib pandas scikit-learn
Standard Imports
import dowhy
from dowhy import CausalModel
import pandas as pd
import numpy as np
Basic Pattern — Full Causal Pipeline
import numpy as np
import pandas as pd
from dowhy import CausalModel
# 1. Simulate data with a known causal structure:
# Confounder C → Treatment T, C → Outcome Y, T → Y (true effect = 2.0)
np.random.seed(42)
n = 2000
C = np.random.randn(n) # Confounder
T = (C + np.random.randn(n) > 0).astype(int) # Treatment influenced by C
Y = 2.0 * T + 1.5 * C + np.random.randn(n) # Outcome: true causal effect of T is 2.0
data = pd.DataFrame({'T': T, 'Y': Y, 'C': C})
# 2. Define the causal graph (YOUR ASSUMPTIONS)
graph = """
digraph {
C -> T;
C -> Y;
T -> Y;
}
"""
# 3. Create causal model
model = CausalModel(
data=data,
treatment=['T'],
outcome='Y',
graph=graph
)
# 4. IDENTIFY: Can we estimate the effect? How?
identified_effect = model.identify_effect()
print(identified_effect)
# → Backdoor criterion: control for C
# 5. ESTIMATE: Compute the causal effect
estimate = model.estimate_effect(
identified_effect,
estimation_method='backdoor.linear_regression'
)
print(f"Estimated causal effect of T on Y: {estimate.value:.3f}")
# → Should be close to 2.0 (the true effect we simulated)
# 6. REFUTE: Is this estimate robust?
refutation = model.refute_estimate(
estimate,
refutation_method='refute_placebo_treatment'
)
print(refutation)
# → Placebo effect should be ~0. If it is, our estimate is credible.
Critical Rules
✅ DO
- Draw the graph BEFORE looking at data — The DAG encodes your causal assumptions. It must come from domain knowledge, not from data patterns. Drawing it after seeing correlations defeats the purpose.
- Include ALL known confounders in the graph — Missing a confounder = biased estimate. When in doubt, include it.
- Always run at least 2 refutation tests — Placebo treatment + random common cause is the minimum. A single estimate without refutation is untrustworthy.
- Use multiple estimators and compare — If backdoor.linearregression and backdoor.propensityscore give wildly different answers, your model assumptions may be wrong.
- Check positivity (overlap) — Propensity score methods fail if treatment and control groups don't overlap in covariate space. Visualize distributions.
- Report confidence intervals, not just point estimates — Use
estimate.get_confidence_intervals(). - Interpret ATE in context — An ATE of 0.5 means nothing without knowing the scale of Y.
❌ DON'T
- Don't assume correlation = causation and skip the graph — This is the entire reason DoWhy exists.
- Don't use DoWhy without a causal hypothesis — If you have no theory about why X might cause Y, you cannot draw a valid DAG. Go do qualitative research first.
- Don't ignore refutation failures — If placebo treatment gives a non-zero effect, your estimate is suspect. Investigate, don't just report.
- Don't confuse "not statistically significant" with "no causal effect" — Underpowered studies fail to detect real effects. Sample size matters.
- Don't use backdoor adjustment when backdoor criterion isn't satisfied —
identify_effect()tells you which strategy is valid. Trust it. - Don't assume linearity —
backdoor.linear_regressionassumes linear relationships. If the truth is nonlinear, usebackdoor.propensity_score_weightingorbackdoor.propensity_score_matching.
Anti-Patterns (NEVER)
import numpy as np
import pandas as pd
from dowhy import CausalModel
# ❌ BAD: No graph — just "estimate" the effect (this is regression, not causal inference)
data = pd.DataFrame({'T': [0,1,0,1], 'Y': [1,3,2,4], 'C': [0.5,1.2,0.8,1.5]})
# This is NOT causal inference — it's just OLS regression. No confounders controlled.
# model = CausalModel(data=data, treatment=['T'], outcome='Y', graph=None) # ← WRONG
# ✅ GOOD: Explicit graph with confounders
graph = "digraph { C -> T; C -> Y; T -> Y; }"
model = CausalModel(data=data, treatment=['T'], outcome='Y', graph=graph)
# ─────────────────────────────────────────────────────────────
# ❌ BAD: Graph drawn AFTER looking at data correlations
# "T and C correlate strongly, so let's put C → T"
# "Y and C correlate, so C → Y"
# This is data-driven graph construction — circular reasoning!
# The graph must come from DOMAIN KNOWLEDGE.
# ✅ GOOD: Graph from theory
# "We know from epidemiology that smoking history (C) affects both
# whether someone enrolls in the program (T) and health outcomes (Y)"
# THEN encode: C -> T; C -> Y; T -> Y
# ─────────────────────────────────────────────────────────────
# ❌ BAD: Single estimator, no refutation — "done"
identified = model.identify_effect()
estimate = model.estimate_effect(identified, estimation_method='backdoor.linear_regression')
print(f"Effect = {estimate.value}") # And that's it. Trust this number? Why?
# ✅ GOOD: Multiple estimators + refutation battery
est_lr = model.estimate_effect(identified, estimation_method='backdoor.linear_regression')
est_psm = model.estimate_effect(identified, estimation_method='backdoor.propensity_score_matching')
est_psw = model.estimate_effect(identified, estimation_method='backdoor.propensity_score_weighting')
print(f"Linear Regression: {est_lr.value:.3f}")
print(f"Propensity Matching: {est_psm.value:.3f}")
print(f"Propensity Weighting: {est_psw.value:.3f}")
# If all three agree → high confidence. If they diverge → investigate.
# Refutation battery
model.refute_estimate(est_lr, refutation_method='refute_placebo_treatment')
model.refute_estimate(est_lr, refutation_method='refute_random_common_cause')
model.refute_estimate(est_lr, refutation_method='refute_data_subset')
# ─────────────────────────────────────────────────────────────
# ❌ BAD: Ignoring the identification result
identified = model.identify_effect()
# identified says: "Backdoor criterion NOT satisfied with these variables"
# But we proceed anyway with backdoor adjustment → biased estimate!
# ✅ GOOD: Check identification, switch strategy if needed
identified = model.identify_effect()
if identified.get_backdoor_variables():
estimate = model.estimate_effect(identified, estimation_method='backdoor.linear_regression')
elif identified.get_instrumental_variables():
estimate = model.estimate_effect(identified, estimation_method='iv.instrumental_variable')
else:
print("Effect is NOT identifiable from this graph. Need more data or stronger assumptions.")
Causal Graphs — Building Blocks
DAG Syntax
from dowhy import CausalModel
# Arrows: A -> B means "A causes B"
# Multiple paths: C -> T and C -> Y means C confounds T and Y
# Simple: treatment, outcome, one confounder
graph_simple = """
digraph {
C -> T;
C -> Y;
T -> Y;
}
"""
# Multiple confounders
graph_multi = """
digraph {
C1 -> T; C1 -> Y;
C2 -> T; C2 -> Y;
C3 -> Y;
T -> Y;
}
"""
# C1, C2 confound. C3 affects only Y (not a confounder, but still important).
# Mediator: T → M → Y (T affects Y *through* M)
graph_mediator = """
digraph {
T -> M;
M -> Y;
T -> Y;
C -> T; C -> Y;
}
"""
# Total effect of T on Y = direct (T→Y) + indirect (T→M→Y)
# Collider: T → S ← Y (S is caused by BOTH T and Y)
# NEVER condition on a collider — it opens a spurious path!
graph_collider = """
digraph {
T -> S;
Y -> S;
T -> Y;
}
"""
# Instrumental Variable: Z → T → Y, Z does NOT directly affect Y
graph_iv = """
digraph {
Z -> T;
T -> Y;
C -> T; C -> Y;
}
"""
# Z is valid instrument if: (1) Z affects T, (2) Z affects Y ONLY through T
Graph Visualization
from dowhy import CausalModel
import pandas as pd
import numpy as np
data = pd.DataFrame({'T': [0,1]*100, 'Y': np.random.randn(200),
'C': np.random.randn(200), 'Z': np.random.randn(200)})
graph = "digraph { C -> T; C -> Y; Z -> T; T -> Y; }"
model = CausalModel(data=data, treatment=['T'], outcome='Y', graph=graph)
model.view_graph() # Renders the DAG — opens in browser or saves .png
Identification Strategies
Backdoor Criterion — Control for Confounders
import numpy as np
import pandas as pd
from dowhy import CausalModel
np.random.seed(0)
n = 3000
# Data generating process:
# C1, C2 are confounders. True effect of T on Y = 3.0
C1 = np.random.randn(n)
C2 = np.random.randn(n)
T = (0.5*C1 + 0.3*C2 + np.random.randn(n) > 0).astype(int)
Y = 3.0*T + 1.0*C1 - 0.5*C2 + np.random.randn(n)
data = pd.DataFrame({'T': T, 'Y': Y, 'C1': C1, 'C2': C2})
graph = "digraph { C1 -> T; C1 -> Y; C2 -> T; C2 -> Y; T -> Y; }"
model = CausalModel(data=data, treatment=['T'], outcome='Y', graph=graph)
# Identify: DoWhy finds which variables satisfy the backdoor criterion
identified = model.identify_effect()
print("Backdoor variables:", identified.get_backdoor_variables())
# → ['C1', 'C2'] — control for both confounders
# Estimate with backdoor adjustment
estimate = model.estimate_effect(
identified,
estimation_method='backdoor.linear_regression'
)
print(f"Causal effect (backdoor): {estimate.value:.3f}") # ≈ 3.0
print(f"95% CI: {estimate.get_confidence_intervals()}")
Frontdoor Criterion — When You Can't Control Confounders
import numpy as np
import pandas as pd
from dowhy import CausalModel
np.random.seed(42)
n = 5000
# Frontdoor structure: T → M → Y, with unobserved confounder U
# U affects both T and Y, but we DON'T observe U
# M (mediator) is observed and fully mediates T's effect on Y
U = np.random.randn(n) # UNOBSERVED
T = (U + np.random.randn(n) > 0).astype(int) # U confounds T
M = 2.0 * T + np.random.randn(n) # T causes M (effect = 2.0)
Y = 1.5 * M + U + np.random.randn(n) # M causes Y (effect = 1.5)
# True total effect of T on Y = 2.0 * 1.5 = 3.0
data = pd.DataFrame({'T': T, 'Y': Y, 'M': M})
# Note: U is NOT in data — we can't control for it directly
# Graph includes U as unobserved
graph = """
digraph {
U -> T;
U -> Y;
T -> M;
M -> Y;
}
"""
model = CausalModel(data=data, treatment=['T'], outcome='Y', graph=graph)
identified = model.identify_effect()
print("Frontdoor variables:", identified.get_frontdoor_variables())
# → ['M'] — the mediator satisfies frontdoor criterion
estimate = model.estimate_effect(
identified,
estimation_method='frontdoor.two_stage_linear_regression'
)
print(f"Causal effect (frontdoor): {estimate.value:.3f}") # ≈ 3.0
Instrumental Variables — Exogenous Variation
import numpy as np
import pandas as pd
from dowhy import CausalModel
np.random.seed(42)
n = 5000
# IV structure: Z (instrument) → T → Y
# Unobserved confounder U affects T and Y
# Z affects Y ONLY through T
Z = np.random.randn(n) # Instrument (e.g., random assignment)
U = np.random.randn(n) # Unobserved confounder
T = 1.0*Z + 0.8*U + np.random.randn(n) # T affected by Z and U
Y = 2.5*T + 1.2*U + np.random.randn(n) # True causal effect of T = 2.5
data = pd.DataFrame({'T': T, 'Y': Y, 'Z': Z})
# U is unobserved — not in data
graph = """
digraph {
Z -> T;
U -> T;
U -> Y;
T -> Y;
}
"""
model = CausalModel(data=data, treatment=['T'], outcome='Y', graph=graph)
identified = model.identify_effect()
print("Instrumental variables:", identified.get_instrumental_variables())
# → ['Z']
estimate = model.estimate_effect(
identified,
estimation_method='iv.instrumental_variable',
method_params={'instrument_variables': ['Z']}
)
print(f"Causal effect (IV): {estimate.value:.3f}") # ≈ 2.5
Estimation Methods
from dowhy import CausalModel
# After identification, choose an estimator based on assumptions:
# ─── LINEAR: Fast, interpretable, assumes linearity ───
est = model.estimate_effect(identified, estimation_method='backdoor.linear_regression')
# ─── PROPENSITY SCORE MATCHING: Pairs similar treated/control units ───
# Better when treatment assignment is non-random but depends on observables
est = model.estimate_effect(identified, estimation_method='backdoor.propensity_score_matching')
# ─── PROPENSITY SCORE WEIGHTING (IPW): Re-weights population ───
# Creates a "pseudo-population" where T is independent of confounders
est = model.estimate_effect(identified, estimation_method='backdoor.propensity_score_weighting')
# ─── INSTRUMENTAL VARIABLES: When backdoor is blocked ───
est = model.estimate_effect(identified,
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [tondevrel](https://github.com/tondevrel)
- **Source:** [tondevrel/scientific-agent-skills](https://github.com/tondevrel/scientific-agent-skills)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.