Skip to main content

pathmc

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.

Source facts

Repository
pymc-labs/pathmc
Last source activity
November 8, 2026 at 16:10
Detected SKILL.md language
English
Stars
139
Forks
17

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
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() # 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 m.optimal_adjustment_set("X", "Y") # variance-optimal graphical O-set ``` 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=...)` - Categorical predictors: string columns are treatment-coded automatically; `C(region, reference='north')` picks the reference and `C(store, prior='hierarchical')` pools one coefficient per level (no reference; the term takes over the intercept's role) ## 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()` | | 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, wrt=...)` | | 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})` | | Intervene on a categorical level | `m.do(set={"region": "south"})`, `m.ate("Y", "region", values=("north", "south"))` | | Declare an integer-coded column as labels | `Y ~ C(store_type)` | | Counterfactual / time-forward (panel) | `m.do(set={...}, simulate_over="time")` | | History-dependent panel treatment policy | `m.do(policy=TreatmentPolicy(...), simulate_over="time", horizon=n_times)` | | Adjustment sets for identification | `m.adjustment_sets(treatment, outcome)` | | Variance-optimal graphical adjustment set | `m.optimal_adjustment_set(treatment, outcome)` | | Yes/no identification check | `m.is_identifiable(treatment, outcome)` | | Front-door identification | `m.frontdoor_identifiable(treatment, mediator, outcome)` | | Flag colliders on any path (heuristic; validity is `is_valid_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(set=...)` returns a `DoResult` describing the whole system, where accessors still take a variable name (`r.mean("Y")`). `m.do(policy=...)` returns a `PolicyRun`, not a `DoResult`. 4. **The DSL is lavaan-*inspired*, not a 1:1 reimplementation.** `~`, `~~`, `:=`, and labeled coefficients all work. A `:=` name must not match a coefficient label (`a := a * 2` is rejected; use a distinct name such as `twice := a * 2`). 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()`, `optimal_adjustment_set()`, `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 an explicit set or opt into `adjustment_set="optimal"`; the default does not pick among minimal sets. The O-set can include precision variables that are unnecessary for identification, and its efficiency guarantee is asymptotic for adjusted OLS in causal linear models, not a general Bayesian GLM posterior-variance guarantee. A required latent O-set member and a query with no directed causal path raise explicitly. Pass `data=` when the parent structural model is data-free. Panel models are not supported on the adjustment path. A shared prior key is inherited when its shape does not follow the predictor set or treatment coding: outcome dispersion (`sigma_Y`, `nu_Y`, `alpha_disp_Y`), transform parameters named in the reduced formula, and scalar hierarchical hyperpriors (`mu_beta_*`, `sigma_beta_*`). Otherwise a custom parent value must be passed in `priors=` or construction raises. That includes coefficient priors and non-scalar hyperpriors. Unknown shared keys fail closed the same way. An explicit `priors=` value still wins. 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*. 11. **String columns become treatment-coded categoricals; integers stay continuous.** `Y ~ region` on a string column creates `beta_Y_region` indexed by level name (coordinate `Y_region_levels`), with the first sorted level as reference. Wrap integer codes in `C(...)` to treat them as labels. The level set is frozen at `model()` time: `do()`, `ate()`, `prob()`, and `predict(data=...)` accept labels, and an unseen label raises rather than re-inferring the coding. Categorical interactions, transforms, outcomes, and panel models are rejected with an explanatory error. 12. **`policy=` is a typed rule, not a Python callback.** `TreatmentPolicy` expressions (`current`, `previous`, `where`, `TimeSchedule`) are evaluated inside a Gaussian lag-panel scan. Pass `simulate_over="time"`, `horizon=` equal to the fitted time count, and an explicit pre-sample baseline. `horizon` is not inferred. `kind="predictive"` is rejected. Latent variables, non-Gaussian families, transforms, categoricals, and residual covariance are rejected rather than run on a weaker executor. 13. **Residual covariance works with Gaussian panel dynamics.** Complete observed Gaussian panels support `~~` together with `lag()` and geometric `adstock()`, including nested transforms. For example, `online ~ adstock(tv, decay=theta_tv) + lag(online); store ~ adstock(tv, decay=theta_tv) + lag(store); online ~~ store` uses `beta_online=[1, 0.8, 0.4]`, `beta_store=[2, 0.5, 0.2]`, `theta_tv=0.6`, and packed `chol_online_store=[1, 0.6, 0.8]` in `simulate()`. Members sort lexicographically, with row-major lower-triangle packing and positive diagonals. Covariance is global; innovations are independent across unit/time/block before temporal propagation. The template omits member `sigma`s; configure their marginal scale priors through `sigma_{member}`. Descendants consume realized states. One-step predictions condition on observed parents/history, while prior prediction, free-running prediction, and predictive hard `do(set=...)` generate correlated trajectories. Hard holds retain unheld members' marginal noise; NaN releases resume their equations. Mean `do()` uses zero process noise. Scaling converts covariance by `D @ Sigma @ D`. Latent nodes, any non-Gaussian equation, missing fitting outcomes, within-block directed dependencies (including cross-member lags), singleton/exogenous endpoints, and `policy=` are excluded. `~~` alone does not identify causal effects. ## Capabilities and boundaries **Agents using pathmc can:** - Write spec strings in the DSL (regressions, residual covariances, defined parameters, labeled coefficients, transforms, categorical predictors via inference or `C(...)`). - 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.
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub