Skip to content

Add causal inference: bn.inference.fit(..., do={...}) (closes #79) - #137

Open
dbolser wants to merge 2 commits into
erdogant:masterfrom
dbolser:feature/do-intervention
Open

Add causal inference: bn.inference.fit(..., do={...}) (closes #79)#137
dbolser wants to merge 2 commits into
erdogant:masterfrom
dbolser:feature/do-intervention

Conversation

@dbolser

@dbolser dbolser commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Adds Pearl's do-operator to bn.inference.fit, so interventions are available from Python the way mutilated() is in the R version of bnlearn. Closes #79.

model = bn.import_DAG('sprinkler')
bn.inference.fit(model, variables=['Wet_Grass'], evidence={'Sprinkler': 1})  # P(W=1|S=1)    = 0.927
bn.inference.fit(model, variables=['Wet_Grass'], do={'Sprinkler': 1})        # P(W=1|do(S=1))= 0.945

Observing Sprinkler=1 is evidence that it was probably not cloudy; setting it by intervention says nothing about the weather. do combines freely with evidence, and interventions are labeled do(Sprinkler)=1 in the readable summary.

How it works, and why not CausalInference

The obvious implementation is pgmpy's CausalInference.query, but its adjustment-based estimator gives wrong answers on two cases this feature needs, both verified against hand-derived CPT values on pgmpy 1.1.2 and on current pgmpy dev (reported as pgmpy/pgmpy#3543):

query exact CausalInference.query
P(W=1 | do(S=1), R=1) — evidence outside the adjustment set 0.99 0.9612
P(C=1 | do(A=1), do(B=1)) — interventions on causally related nodes 1.0 0.7

So do instead runs VariableElimination over model.do(...)'s mutilated network with the intervened variables fixed as evidence — in a network whose incoming edges are cut, conditioning is intervening. That is exact in both cases above, and it keeps elimination_order and joint working with do. If pgmpy fixes #3543, nothing here needs to change.

Notes

  • do is the last parameter of fit, so existing positional calls are unaffected.
  • Validation: unknown node names in do, and a variable given in both do and evidence, raise instead of silently misbehaving.
  • Also fixes a pre-existing crash: evidence=None raised AttributeError on the evidence.keys() validation line.
  • 4 new tests (locked values hand-derived from the CPDs, incl. both cases in the table); full suite 94/94 green locally under pgmpy 1.1.2, and green on all 9 CI jobs (Python 3.10/3.12/3.13 × ubuntu/macos/windows) on the fork.

🤖 Generated with Claude Code

dbolser and others added 2 commits August 31, 2026 21:19
…t#79)

Expose Pearl's do-operator via pgmpy's CausalInference, which became
available with the pgmpy 1.x migration (erdogant#135). do={} simulates setting a
variable by intervention (incoming edges cut, comparable to mutilated()
in the R version of bnlearn), and combines with ordinary evidence.
Interventions are labeled do(X) in the readable summary.

Locked test values on the sprinkler network, derived by hand from the
CPDs: P(W=1|S=1)=0.927 (observational) vs P(W=1|do(S=1))=0.945.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017dh1KPAuSYmLpERF2T3Drm
…ard compatible

Codex confirmed two real correctness bugs in the adjustment-based
CausalInference.query path (verified locally): evidence outside the
adjustment set was mishandled (P(W=1|do(S=1),R=1) gave 0.9612, exact is
0.99) and interventions on causally related nodes collapsed
(P(C=1|do(A=1,B=1)) gave 0.7, exact is 1.0). Do-queries now run plain
variable elimination on model.do()'s mutilated network with the
interventions fixed as evidence, which is exact in both cases and lets
elimination_order/joint apply to causal queries too. Regression tests
lock both values.

Also: move do to the end of the signature so existing positional calls
keep their meaning; return the factor dict directly for joint=False
(attaching .df to it raised AttributeError before, on master too); fix
the do(X=x) docstring notation; replace an assigned lambda (E731).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017dh1KPAuSYmLpERF2T3Drm
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Intervention using do-calculas?

1 participant