- name
- pathmc
- description
- 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.
- license
- MIT
- compatibility
- Requires Python >=3.12, PyMC >=6.0.
- metadata
- {"author":"drbenvincent","version":"0.1","homepage":"https://github.com/pymc-labs/pathmc","tags":["causal-inference","bayesian","sem","path-analysis","pymc"]}
# 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
```bash
pip install pathmc
# Optional faster samplers (nutpie, numpyro, jax):
pip install "pathmc[samplers]"
```
## Quick start
```python
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
1. **`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.
2. **`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()`.
3. **`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")`).
4. **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.
5. **`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`.
6. **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.
7. **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()`).
8. **`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.
9. **`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.
10. **`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)
```python
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
```python
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
```python
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
```python
import pathmc
m = pathmc.model(
"sales ~ b*price + a*lag(sales) + trend",
data=df,
panel={"unit": "region", "time": "week"},
pooling="partial",
)
m.fit()
View on GitHub