pathmc
SkillAI & modelsBayesian 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.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the pathmc skill
What this skill tells your AI
The instructions your AI receives, as published by pymc-labs/pathmc in pathmc/skills/pathmc/SKILL.md and read by ahel’s review.
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— regressionY ~~ X— residual covarianceindirect := a*b— defined parametera*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 aPathModel, 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 appliespm.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 wrappersm.ate(),m.cate(),m.att(),m.atu(),m.prob().ate()/cate()/att()/atu()return anEstimandResult, not aDoResult. It knows the outcome, sor.mean(),r.hdi(), andr.prob("> 0")need no variable argument,float(r)gives the posterior mean, and printing it shows a tidy summary.m.do(...)returns aDoResultdescribing 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. Prioris re-exported frompymc_extrasfor convenience.from pathmc import Prioris a shortcut forfrom pymc_extras.prior import Prior. The canonical reference and list of supported distributions live inpymc_extras.- Panel lag terms are declared in the model spec.
Use
lag(sales)directly in the DSL and passpanel={"unit": "region", "time": "week"}topathmc.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()raiseRuntimeErroruntil the model is rebuilt with data (andrefute_placebo()also needs a prior.fit()). PathModelis not inpathmc.__all__— it's the class returned bymodel(). You don't import it directly; you receive it. Type annotations can usepathmc.PathModel(it is reachable as an attribute) but the public entrypoint is themodel()function.adjustment_model()returns anAdjustmentModelfacade. Inspectadj.adjustment_setandadj.formulabefore callingadj.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, passadjustment_set=explicitly; pathmc does not pick among them. Passdata=when the parent structural model is data-free. Panel models are not supported on the adjustment path.predictions()/comparisons()/slopes()share one API onPathModelandAdjustmentModel. On the structural model they use truncated-factorization g-computation; onadjustment_model()they delegate to the reduced outcome equation withestimator="regression_adjustment". Readresult.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
Priorobjects frompymc_extras. - Run
fit()with PyMC's NUTS sampler (ornutpie/numpyrovia thesamplersextra). - Query
ate/cate/att/atu/prob/effectwith 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 structuralPathModelor fittedAdjustmentModelobjects for interpret-style queries (checkresult.causalon 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'sgcm.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'splacebo_treatment_refuterwith a calibratedz_cal/p_tailfor 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 manualdo()calls. Usem.do(set={...})with explicit values for non-binary interventions. - Editing the compiled
pm.Modelobject directly. pathmc owns the graph; mutating it bypasses the introspection layer and breaksdo()propagation. To customize, change the spec or passpriors=/families=tomodel().
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
- Docs site: https://pathmc.pymc-labs.com/
- Interpret gallery: Conditional Predictions, Interventional Contrasts, Local Slopes
llms.txt— indexed API reference for LLMsllms-full.txt— comprehensive API documentation for LLMs- GitHub: https://github.com/pymc-labs/pathmc
Signals
- GitHub stars
- 128
- Forks
- 12
- Last commit
- Sep 2026
ahel review
K1binfo
installs-packages
Automated review, not a security audit. Ruleset v1+k2.
Advanced
- Catalog kind
- skill
- Gateway key
pathmc- Source
- github.com/pymc-labs/pathmc