pathmc
pathmc lets you specify a system of structural equations as a string,
compile it to a generative PyMC model, fit with MCMC, and reason about
causal effects using the do-operator.
Installation
pip install pathmc
# Optional faster samplers (nutpie, numpyro, jax):
pip install "pathmc[samplers]"
Quick start
import pathmc
spec = """
M ~ a*X
Y ~ b*M + c*X
indirect := a*b
"""
m = pathmc.model(spec, data=df) # returns a PathModel (NOT a fitted result)
m.fit(draws=1000, chains=2) # MCMC happens here
m.effects_summary() # labeled coefficients + defined params
m.ate("Y", "X", values=(0, 1)) # average treatment effect via do()
m.adjustment_sets("X", "Y") # valid backdoor adjustment sets
The DSL is lavaan-inspired:
Y ~ X — regression
Y ~~ X — residual covariance
indirect := a*b — defined parameter
a*X — labeled coefficient
- Transforms:
adstock(x, decay=...), logistic_saturation(x, lam=...)
Decision table
| Need |
Use |
| Build a model from a spec + data |
m = pathmc.model(spec, data=df) |
| Explore the DAG without data |
m = pathmc.model(spec) (data-free mode) |
| Inspect causal DAG |
m.graph() |
| Inspect structural equations + priors |
m.equations() |
| Inspect priors only |
m.priors() |
| Refine priors |
m.set_priors({"beta_Y": Prior(...)}) |
| Prior predictive check |
m.sample_prior_predictive() |
| Run MCMC |
m.fit(draws=1000, chains=2) |
| Summarize posteriors |
m.summary() or m.effects_summary() |
| Standardized (stdyx) coefficients |
m.standardized() |
Path-specific effect (e.g. X -> M -> Y) |
m.effect("X -> M -> Y") |
| Posterior predictions |
m.predict(...) |
| Average treatment effect |
m.ate(outcome, treatment, values=(0, 1)) |
| Conditional ATE (effect modification) |
m.cate(outcome, treatment, condition={"Z": z0}) |
| ATE on the treated / untreated |
m.att(...) / m.atu(...) |
| Backdoor-adjusted outcome regression |
adj = m.adjustment_model("X -> Y") then adj.fit() |
| Inspect adjustment set / formula before fit |
adj.adjustment_set, adj.formula (before adj.fit()) |
| Interventional / associational predictions |
m.predictions(outcome, set={...}) |
| Interventional contrasts (structural model) |
m.comparisons(outcome, variable, contrast=(0, 1)) |
| Marginal slopes under intervention |
m.slopes(outcome, variable) |
| Same interpret API on adjustment model |
adj.comparisons(...), adj.slopes(...), etc. |
| Probability under intervention |
m.prob("Y > 0", set={"X": 1}) |
| Manual intervention |
m.do(set={"X": 1}) |
| Counterfactual / time-forward (panel) |
m.do(set={...}, kind="time-forward") |
| Adjustment sets for identification |
m.adjustment_sets(treatment, outcome) |
| Yes/no identification check |
m.is_identifiable(treatment, outcome) |
| Front-door identification |
m.frontdoor_identifiable(treatment, outcome) |
| Warn about colliders in an adjustment set |
m.collider_warnings(adjust, treatment, outcome) |
| Enumerate implied conditional independences |
m.implied_independences() |
| Test DAG implications against data |
m.test_implications() |
| Falsify the whole DAG (permutation test) |
m.falsify() |
| Sensitivity analysis (unmeasured confounding) |
m.sensitivity(outcome, treatment) |
| Placebo refutation of an estimated effect |
m.refute_placebo(outcome, treatment) |
| Simulate from a fully-specified model |
pathmc.simulate(spec, data, params=...) |
Gotchas
pathmc.model(...) returns a PathModel, not a fitted result.
You must call .fit() separately. model() only parses, builds the
DAG, and compiles the PyMC graph — it does not sample.
m.do(...) is a structural intervention, not conditioning.
It applies pm.do() graph surgery and forward-simulates from the
intervened model, propagating posterior uncertainty through the
causal chain (g-computation; Robins, 1986). It is not the same as
conditioning on observed values. For typical user-facing queries,
prefer the wrappers m.ate(), m.cate(), m.att(), m.atu(),
m.prob().
ate()/cate()/att()/atu() return an EstimandResult, not a DoResult.
It knows the outcome, so r.mean(), r.hdi(), and r.prob("> 0") need
no variable argument, float(r) gives the posterior mean, and printing it
shows a tidy summary. m.do(...) returns a DoResult describing the whole
system, where accessors still take a variable name (r.mean("Y")).
- The DSL is lavaan-inspired, not a 1:1 reimplementation.
~, ~~, :=, and labeled coefficients all work. Latent-variable
measurement models (=~) are out of scope in v0.1 — see the user
guide for the full operator list.
Prior is re-exported from pymc_extras for convenience.
from pathmc import Prior is a shortcut for
from pymc_extras.prior import Prior. The canonical reference and
list of supported distributions live in pymc_extras.
- Panel lag terms are declared in the model spec.
Use
lag(sales) directly in the DSL and pass
panel={"unit": "region", "time": "week"} to pathmc.model(...).
pathmc builds the lagged design internally.
- Data-free models have a partial method surface.
When
data=None, graph(), equations(), priors(),
adjustment_sets(), is_identifiable(), collider_warnings(),
implied_independences() all work. fit(), do(), ate(),
cate(), design(), sample_prior_predictive(),
test_implications(), falsify(), sensitivity(),
refute_placebo() raise RuntimeError until the model is rebuilt
with data (and refute_placebo() also needs a prior .fit()).
PathModel is not in pathmc.__all__ — it's the class returned
by model(). You don't import it directly; you receive it. Type
annotations can use pathmc.PathModel (it is reachable as an
attribute) but the public entrypoint is the model() function.
adjustment_model() returns an AdjustmentModel facade.
Inspect adj.adjustment_set and adj.formula before calling
adj.fit(). An empty set {} is valid when no covariates are
needed to block backdoors; if no valid set exists, construction
raises (effect not identifiable via backdoor). When several minimal
sets exist, pass adjustment_set= explicitly; pathmc does not pick
among them. Pass data= when the parent structural model is
data-free. Panel models are not supported on the adjustment path.
predictions() / comparisons() / slopes() share one API on
PathModel and AdjustmentModel. On the structural model they
use truncated-factorization g-computation; on
adjustment_model() they delegate to the reduced outcome equation
with estimator="regression_adjustment". Read result.causal:
only queries on the designated treatment support a causal reading;
slopes or contrasts on adjustment covariates are interventional on
the fitted surface, not causal effects of those covariates. See the
user guide page Predictions, Comparisons, and Slopes.
Capabilities and boundaries
Agents using pathmc can:
- Write spec strings in the DSL (regressions, residual covariances,
defined parameters, labeled coefficients, transforms).
- Configure custom priors via
Prior objects from pymc_extras.
- Run
fit() with PyMC's NUTS sampler (or nutpie / numpyro via
the samplers extra).
- Query
ate/cate/att/atu/prob/effect with full posterior
uncertainty.
- Fit a DAG-derived backdoor adjustment model via
adjustment_model()
when a single treatment-outcome query suffices.
- Run
predictions() / comparisons() / slopes() on structural
PathModel or fitted AdjustmentModel objects for interpret-style
queries (check result.causal on adjustment models).
- Check identification (
adjustment_sets, is_identifiable,
frontdoor_identifiable, collider_warnings).
- Test the DAG's conditional-independence implications against data
(
test_implications).
- Falsify the whole DAG with a permutation-based test (
falsify),
which grades the graph against randomly-rewired competitors (a port of
dowhy's gcm.falsify_graph).
- Build hierarchical panel models with random intercepts/slopes and
use
lag() terms.
- Run sensitivity analysis to quantify robustness to unmeasured
confounding.
- Refute an estimated effect with a Bayesian placebo treatment
(
refute_placebo): permute the treatment, re-fit, and pool the
per-permutation ATE posteriors through a hierarchical normal-normal
null model whose null predictive should straddle zero. Upgrades
dowhy's placebo_treatment_refuter with a calibrated z_cal/p_tail
for the real effect.
Out of scope (do not attempt):
- Latent variables / SEM measurement models (the
=~ operator).
Out of scope in v0.1; on the post-v1 roadmap.
- Categorical mediators or treatments with >2 levels in
ate()/cate() without manual do() calls. Use m.do(set={...})
with explicit values for non-binary interventions.
- Editing the compiled
pm.Model object directly. pathmc owns the
graph; mutating it bypasses the introspection layer and breaks
do() propagation. To customize, change the spec or pass priors=
/ families= to model().
Patterns
Inspect before sampling (data-free DAG exploration)
m = pathmc.model("""
M ~ a*X
Y ~ b*M + c*X
indirect := a*b
""")
m.graph() # DAG plot
m.equations() # structural equations + priors
m.adjustment_sets("X", "Y") # what to adjust for
m.is_identifiable("X", "Y") # can we estimate the effect at all?
Standard fit-and-query workflow
m = pathmc.model(spec, data=df)
m.fit(draws=1000, chains=2)
m.effects_summary() # labeled coefs
m.ate("Y", "X", values=(0, 1)) # ATE
m.cate("Y", "X", condition={"Z": 1}) # CATE | Z=1
m.test_implications() # DAG vs data check
Backdoor adjustment model
m = pathmc.model(spec, data=df) # structural DAG + data
adj = m.adjustment_model("X -> Y") # inspect before fit
adj.adjustment_set # validated backdoor set
adj.formula # reduced outcome equation
adj.fit(draws=1000, chains=2)
adj.ate(values=(0, 1)) # outcome-regression standardization
adj.comparisons(comparison="lift") # same API as PathModel
adj.slopes(wrt="X") # defaults to designated treatment
When several minimal adjustment sets exist, pass adjustment_set= explicitly.
When the structural model has no data, pass data= to adjustment_model().
Panel model
import pathmc
m = pathmc.model(
"sales ~ b*price + a*lag(sales) + trend",
data=df,
panel={"unit": "region", "time": "week"},
pooling="partial",
)
m.fit()
m.do(set={"price": 1.5}, kind="time-forward")
Custom priors
from pathmc import Prior # re-export of pymc_extras.prior.Prior
m = pathmc.model(
spec,
data=df,
priors={
"beta_Y": Prior("Normal", mu=0, sigma=2),
"sigma_Y": Prior("HalfNormal", sigma=1),
},
)
m.priors() # confirm overrides applied
m.sample_prior_predictive() # check the priors imply plausible data
Resources
1---2name: pathmc3description: Bayesian path analysis (observed-variable SEM) in PyMC. Compiles a lavaan-inspired formula DSL into a generative PyMC model, then layers introspection, identification diagnostics, the `do()` operator, and causal estimands (ATE/CATE/ATT/ATU/prob) on top. Use when the user asks to specify, fit, or query a Bayesian structural causal model; estimate average treatment effects via g-computation; check identification with adjustment sets or the front-door criterion; or simulate panel/longitudinal counterfactuals.4license: MIT5---67# pathmc89`pathmc` lets you specify a system of structural equations as a string,10compile it to a generative PyMC model, fit with MCMC, and reason about11causal effects using the do-operator.1213## Installation1415```bash16pip install pathmc17# Optional faster samplers (nutpie, numpyro, jax):18pip install "pathmc[samplers]"19```2021## Quick start2223```python24import pathmc2526spec = """27M ~ a*X28Y ~ b*M + c*X29indirect := a*b30"""3132m = pathmc.model(spec, data=df) # returns a PathModel (NOT a fitted result)33m.fit(draws=1000, chains=2) # MCMC happens here3435m.effects_summary() # labeled coefficients + defined params36m.ate("Y", "X", values=(0, 1)) # average treatment effect via do()37m.adjustment_sets("X", "Y") # valid backdoor adjustment sets38```3940The DSL is lavaan-inspired:4142- `Y ~ X` — regression43- `Y ~~ X` — residual covariance44- `indirect := a*b` — defined parameter45- `a*X` — labeled coefficient46- Transforms: `adstock(x, decay=...)`, `logistic_saturation(x, lam=...)`4748## Decision table4950| Need | Use |51| ------------------------------------------------- | ------------------------------------------------------ |52| Build a model from a spec + data | `m = pathmc.model(spec, data=df)` |53| Explore the DAG without data | `m = pathmc.model(spec)` (data-free mode) |54| Inspect causal DAG | `m.graph()` |55| Inspect structural equations + priors | `m.equations()` |56| Inspect priors only | `m.priors()` |57| Refine priors | `m.set_priors({"beta_Y": Prior(...)})` |58| Prior predictive check | `m.sample_prior_predictive()` |59| Run MCMC | `m.fit(draws=1000, chains=2)` |60| Summarize posteriors | `m.summary()` or `m.effects_summary()` |61| Standardized (stdyx) coefficients | `m.standardized()` |62| Path-specific effect (e.g. `X -> M -> Y`) | `m.effect("X -> M -> Y")` |63| Posterior predictions | `m.predict(...)` |64| **Average treatment effect** | `m.ate(outcome, treatment, values=(0, 1))` |65| **Conditional ATE** (effect modification) | `m.cate(outcome, treatment, condition={"Z": z0})` |66| ATE on the treated / untreated | `m.att(...)` / `m.atu(...)` |67| **Backdoor-adjusted outcome regression** | `adj = m.adjustment_model("X -> Y")` then `adj.fit()` |68| Inspect adjustment set / formula before fit | `adj.adjustment_set`, `adj.formula` (before `adj.fit()`) |69| Interventional / associational predictions | `m.predictions(outcome, set={...})` |70| Interventional contrasts (structural model) | `m.comparisons(outcome, variable, contrast=(0, 1))` |71| Marginal slopes under intervention | `m.slopes(outcome, variable)` |72| Same interpret API on adjustment model | `adj.comparisons(...)`, `adj.slopes(...)`, etc. |73| Probability under intervention | `m.prob("Y > 0", set={"X": 1})` |74| Manual intervention | `m.do(set={"X": 1})` |75| Counterfactual / time-forward (panel) | `m.do(set={...}, kind="time-forward")` |76| Adjustment sets for identification | `m.adjustment_sets(treatment, outcome)` |77| Yes/no identification check | `m.is_identifiable(treatment, outcome)` |78| Front-door identification | `m.frontdoor_identifiable(treatment, outcome)` |79| Warn about colliders in an adjustment set | `m.collider_warnings(adjust, treatment, outcome)` |80| Enumerate implied conditional independences | `m.implied_independences()` |81| Test DAG implications against data | `m.test_implications()` |82| Falsify the whole DAG (permutation test) | `m.falsify()` |83| Sensitivity analysis (unmeasured confounding) | `m.sensitivity(outcome, treatment)` |84| Placebo refutation of an estimated effect | `m.refute_placebo(outcome, treatment)` |85| Simulate from a fully-specified model | `pathmc.simulate(spec, data, params=...)` |8687## Gotchas88891. **`pathmc.model(...)` returns a `PathModel`, not a fitted result.**90 You must call `.fit()` separately. `model()` only parses, builds the91 DAG, and compiles the PyMC graph — it does not sample.922. **`m.do(...)` is a structural intervention, not conditioning.**93 It applies `pm.do()` graph surgery and forward-simulates from the94 intervened model, propagating posterior uncertainty through the95 causal chain (g-computation; Robins, 1986). It is **not** the same as96 conditioning on observed values. For typical user-facing queries,97 prefer the wrappers `m.ate()`, `m.cate()`, `m.att()`, `m.atu()`,98 `m.prob()`.993. **`ate()`/`cate()`/`att()`/`atu()` return an `EstimandResult`, not a `DoResult`.**100 It knows the outcome, so `r.mean()`, `r.hdi()`, and `r.prob("> 0")` need101 no variable argument, `float(r)` gives the posterior mean, and printing it102 shows a tidy summary. `m.do(...)` returns a `DoResult` describing the whole103 system, where accessors still take a variable name (`r.mean("Y")`).1044. **The DSL is lavaan-*inspired*, not a 1:1 reimplementation.**105 `~`, `~~`, `:=`, and labeled coefficients all work. Latent-variable106 measurement models (`=~`) are out of scope in v0.1 — see the user107 guide for the full operator list.1085. **`Prior` is re-exported from `pymc_extras` for convenience.**109 `from pathmc import Prior` is a shortcut for110 `from pymc_extras.prior import Prior`. The canonical reference and111 list of supported distributions live in `pymc_extras`.1126. **Panel lag terms are declared in the model spec.**113 Use `lag(sales)` directly in the DSL and pass114 `panel={"unit": "region", "time": "week"}` to `pathmc.model(...)`.115 pathmc builds the lagged design internally.1167. **Data-free models have a partial method surface.**117 When `data=None`, `graph()`, `equations()`, `priors()`,118 `adjustment_sets()`, `is_identifiable()`, `collider_warnings()`,119 `implied_independences()` all work. `fit()`, `do()`, `ate()`,120 `cate()`, `design()`, `sample_prior_predictive()`,121 `test_implications()`, `falsify()`, `sensitivity()`,122 `refute_placebo()` raise `RuntimeError` until the model is rebuilt123 with data (and `refute_placebo()` also needs a prior `.fit()`).1248. **`PathModel` is not in `pathmc.__all__`** — it's the class returned125 by `model()`. You don't import it directly; you receive it. Type126 annotations can use `pathmc.PathModel` (it is reachable as an127 attribute) but the public entrypoint is the `model()` function.1289. **`adjustment_model()` returns an `AdjustmentModel` facade.**129 Inspect `adj.adjustment_set` and `adj.formula` before calling130 `adj.fit()`. An empty set `{}` is valid when no covariates are131 needed to block backdoors; if no valid set exists, construction132 raises (effect not identifiable via backdoor). When several minimal133 sets exist, pass `adjustment_set=` explicitly; pathmc does not pick134 among them. Pass `data=` when the parent structural model is135 data-free. Panel models are not supported on the adjustment path.13610. **`predictions()` / `comparisons()` / `slopes()` share one API on137 `PathModel` and `AdjustmentModel`.** On the structural model they138 use truncated-factorization g-computation; on139 `adjustment_model()` they delegate to the reduced outcome equation140 with `estimator="regression_adjustment"`. Read `result.causal`:141 only queries on the designated treatment support a causal reading;142 slopes or contrasts on adjustment covariates are interventional on143 the fitted surface, not causal effects of those covariates. See the144 user guide page *Predictions, Comparisons, and Slopes*.145146## Capabilities and boundaries147148**Agents using pathmc can:**149150- Write spec strings in the DSL (regressions, residual covariances,151 defined parameters, labeled coefficients, transforms).152- Configure custom priors via `Prior` objects from `pymc_extras`.153- Run `fit()` with PyMC's NUTS sampler (or `nutpie` / `numpyro` via154 the `samplers` extra).155- Query `ate`/`cate`/`att`/`atu`/`prob`/`effect` with full posterior156 uncertainty.157- Fit a DAG-derived backdoor adjustment model via `adjustment_model()`158 when a single treatment-outcome query suffices.159- Run `predictions()` / `comparisons()` / `slopes()` on structural160 `PathModel` or fitted `AdjustmentModel` objects for interpret-style161 queries (check `result.causal` on adjustment models).162- Check identification (`adjustment_sets`, `is_identifiable`,163 `frontdoor_identifiable`, `collider_warnings`).164- Test the DAG's conditional-independence implications against data165 (`test_implications`).166- Falsify the whole DAG with a permutation-based test (`falsify`),167 which grades the graph against randomly-rewired competitors (a port of168 dowhy's `gcm.falsify_graph`).169- Build hierarchical panel models with random intercepts/slopes and170 use `lag()` terms.171- Run sensitivity analysis to quantify robustness to unmeasured172 confounding.173- Refute an estimated effect with a Bayesian placebo treatment174 (`refute_placebo`): permute the treatment, re-fit, and pool the175 per-permutation ATE posteriors through a hierarchical normal-normal176 null model whose null predictive should straddle zero. Upgrades177 dowhy's `placebo_treatment_refuter` with a calibrated `z_cal`/`p_tail`178 for the real effect.179180**Out of scope (do not attempt):**181182- **Latent variables / SEM measurement models** (the `=~` operator).183 Out of scope in v0.1; on the post-v1 roadmap.184- **Categorical mediators or treatments with >2 levels in185 `ate()`/`cate()`** without manual `do()` calls. Use `m.do(set={...})`186 with explicit values for non-binary interventions.187- **Editing the compiled `pm.Model` object directly.** pathmc owns the188 graph; mutating it bypasses the introspection layer and breaks189 `do()` propagation. To customize, change the spec or pass `priors=`190 / `families=` to `model()`.191192## Patterns193194### Inspect before sampling (data-free DAG exploration)195196```python197m = pathmc.model("""198 M ~ a*X199 Y ~ b*M + c*X200 indirect := a*b201""")202m.graph() # DAG plot203m.equations() # structural equations + priors204m.adjustment_sets("X", "Y") # what to adjust for205m.is_identifiable("X", "Y") # can we estimate the effect at all?206```207208### Standard fit-and-query workflow209210```python211m = pathmc.model(spec, data=df)212m.fit(draws=1000, chains=2)213m.effects_summary() # labeled coefs214m.ate("Y", "X", values=(0, 1)) # ATE215m.cate("Y", "X", condition={"Z": 1}) # CATE | Z=1216m.test_implications() # DAG vs data check217```218219### Backdoor adjustment model220221```python222m = pathmc.model(spec, data=df) # structural DAG + data223adj = m.adjustment_model("X -> Y") # inspect before fit224adj.adjustment_set # validated backdoor set225adj.formula # reduced outcome equation226adj.fit(draws=1000, chains=2)227adj.ate(values=(0, 1)) # outcome-regression standardization228adj.comparisons(comparison="lift") # same API as PathModel229adj.slopes(wrt="X") # defaults to designated treatment230```231232When several minimal adjustment sets exist, pass `adjustment_set=` explicitly.233When the structural model has no data, pass `data=` to `adjustment_model()`.234235### Panel model236237```python238import pathmc239m = pathmc.model(240 "sales ~ b*price + a*lag(sales) + trend",241 data=df,242 panel={"unit": "region", "time": "week"},243 pooling="partial",244)245m.fit()246m.do(set={"price": 1.5}, kind="time-forward")247```248249### Custom priors250251```python252from pathmc import Prior # re-export of pymc_extras.prior.Prior253254m = pathmc.model(255 spec,256 data=df,257 priors={258 "beta_Y": Prior("Normal", mu=0, sigma=2),259 "sigma_Y": Prior("HalfNormal", sigma=1),260 },261)262m.priors() # confirm overrides applied263m.sample_prior_predictive() # check the priors imply plausible data264```265266## Resources267268- Docs site: <https://pathmc.pymc-labs.com/>269- Interpret gallery: [Conditional Predictions](https://pathmc.pymc-labs.com/docs/examples/interpret/predictions.html), [Interventional Contrasts](https://pathmc.pymc-labs.com/docs/examples/interpret/comparisons.html), [Local Slopes](https://pathmc.pymc-labs.com/docs/examples/interpret/slopes.html)270- `llms.txt` — indexed API reference for LLMs271- `llms-full.txt` — comprehensive API documentation for LLMs272- GitHub: <https://github.com/pymc-labs/pathmc>