Add pymc_forecast model-provider adapter for InterruptedTimeSeries#1014
Conversation
Implements the ITS-only first pass of #1013: a thin adapter that lets a pymc_forecast forecasting model act as a model provider behind CausalPy's existing experiment API, in the same spirit as the sklearn wrapper. - causalpy/pymc_forecast_models.py: PyMCForecastModel wraps a pymc_forecast model_fn/ForecastingModel + forecaster (NUTS by default). fit() trains on the pre-period; predict() maps predict_in_sample() and forecast(future_covariates=/future_index=) onto CausalPy's draw-level posterior-predictive contract, renaming the documented schema dims (time/time_future -> obs_ind, series -> treated_units). - PyMCForecastAdapter backend in experiments/model_adapter.py, gated by a supports_pymc_forecast flag; enabled for InterruptedTimeSeries only. - pymc-forecast ships as an optional extra (causalpy[forecast]) pinned to the 0.1.x minor; also added to the test extra so CI exercises it. - Round-trip tests: fit pre / forecast post-as-untreated / calculate_impact on draw-level samples, checked against the native PyMC LinearRegression path, plus covariate-free future_index and three-period designs. Closes #1013 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Automated triageRecommendation: Why:
Review focus:
Confidence: high |
…eferred fit The four upstream feature requests this adapter was scoped around have shipped (pymc-labs/pymc-forecast#44, releasing as 0.2.0): - mu is now the genuine noise-free latent predictor (upstream mu / mu_future, issue pymc-labs/pymc-forecast#36) instead of aliasing the posterior predictive of the observed variable, so causal impact excludes observation-level noise exactly like the native PyMCModel backends. y_hat keeps the draw-level posterior predictive. The documented caveat is gone; StatespaceForecaster results (no upstream latent) fall back to the posterior predictive. - One posterior subsample is drawn at fit time and passed to both predictive calls via posterior= (pymc-labs/pymc-forecast#37), so draw i of the pre-period fit and draw i of the counterfactual come from the same parameter draw. - The forecaster is constructed unfitted at PyMCForecastModel construction and fit via fit(data, covariates) (deferred fit, pymc-labs/pymc-forecast#39), replacing the carried (class, kwargs) recipe; invalid options now fail at construction. progressbar= is accepted uniformly through forecaster_kwargs. The default forecaster resolves to StatespaceForecaster for StatespaceModel definitions. - Pins bumped to pymc-forecast>=0.2,<0.3. New tests: mu is strictly narrower than y_hat and impact spread equals mu spread (noise-free contract), and pre/post mu reproduce X @ beta from the shared posterior draw-for-draw (draw coherence), with repeated predict() calls bit-identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The upstream changes this adapter requested are now in: pymc-labs/pymc-forecast#44 implemented all four feature requests (pymc-labs/pymc-forecast#36 noise-free This PR is updated accordingly (latest commit):
Note: CI will stay red on the dependency resolve until |
pymc-forecast is PyPI-only (not on conda-forge), so listing it as a conda dependency made the micromamba env solve fail in CI. Mark it pip=true via [tool.pyproject2conda.dependencies] and regenerate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The y ~ 1 + t design leaves intercept and slope strongly correlated (unscaled t), and tune=200 produced a badly adapted mass matrix and a biased posterior on CI's platform: in-sample R2 ~0.5 and a counterfactual off by ~9 units, while the draw-coherence contract tests still passed (the predictions faithfully reproduced the bad posterior). Bump to draws=500 / tune=1000. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The conftest registers PyMC's mock_sample (prior sampling instead of MCMC) through a session-scoped fixture, so once any earlier test module requests mock_pymc_sample, pm.sample stays mocked for the rest of the run — including modules that never asked for it. In the full CI suite the adapter round-trip tests therefore fit nothing: the posterior was the prior, giving the deterministic Bayesian R2 ~= 0.5 and a counterfactual off by ~10 with sign flipping between runs, while every structural/contract assertion still passed. Running the file alone (as done locally) never triggered the mock, which is why it was green. Add a module-scoped autouse fixture restoring pymc.sampling.mcmc.sample for this module only, since effect-recovery assertions are meaningless under prior sampling. Verified locally by running a mock-requesting test first and then this module: 15 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1014 +/- ##
==========================================
+ Coverage 95.51% 95.58% +0.06%
==========================================
Files 98 102 +4
Lines 15870 16324 +454
Branches 931 955 +24
==========================================
+ Hits 15159 15604 +445
- Misses 504 509 +5
- Partials 207 211 +4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
drbenvincent
left a comment
There was a problem hiding this comment.
Thanks for this — the core direction looks right. pymc-forecast should remain a distinct, experiment-gated backend rather than inherit from PyMCModel: it owns a forecaster with a different fit/forecast lifecycle, coefficient contract, and inference-result shape. Once its outputs are normalized, treating it as Bayesian through is_bayesian is appropriate. supports_pymc_forecast is a reasonable incremental gate for the ITS-only first pass; before adding further providers, we should replace the parallel booleans with an explicit set of supported backend kinds.
Three areas need addressing before merge:
1. Make the existing CausalPy placebo-in-time path work
The PR/issue says that placebo methods remain CausalPy's responsibility, but PlaceboInTime.validate() currently rejects PyMCForecastModel because it checks isinstance(model, PyMCModel). Refitting also fails because PyMCForecastModel has no _clone() and its fallback deepcopy() raises TypeError: cannot pickle 'module' object.
This is a small CausalPy-only fix, not a new upstream feature: add an _clone() that reconstructs an unfitted wrapper from its stored configuration, validate against the backend's Bayesian capability, and add coverage that a forecast-backed ITS can be cloned and accepted/refitted by PlaceboInTime. Track broader support for other sensitivity checks separately.
2. Do not alias noisy statespace predictions to mu
For StatespaceForecaster, the adapter currently falls back from a missing noise-free expected observation to y_hat, then exposes that as mu. ITS consequently computes causal impact with observation noise included, contrary to CausalPy's mu-based impact contract.
This requires a new upstream pymc-forecast capability: statespace prediction currently exposes latent-state draws and noisy observed draws, but not the expected observation in observation space with measurement error removed. Please open and link an upstream issue requesting draw-level in-sample and future expected-observation outputs that:
- apply the state-space observation equation rather than returning the latent state vector;
- exclude measurement error;
- remain conditioned on the same supplied
posterior=draws; - preserve matching chain/draw/time/series coordinates; and
- align with the core forecaster's
mu/mu_futureschema where possible.
Until that capability exists, please reject or stop advertising the statespace path here rather than silently substituting y_hat for mu. Core pymc-forecast models built with predict() already provide proper mu / mu_future and can remain supported.
3. Complete the new backend's central contracts
- Update
ARCHITECTURE.md; it still describes two backends and its maintenance rule explicitly requires updates when backend dispatch changes. - Update the central
BaseExperimentandModelAdapter.modelannotations/docstrings, which still excludePyMCForecastModel. - Align the optional dependency with the documented API. Upstream places statespace/Pathfinder support behind
pymc-forecast[extras]withpymc-extras>=0.10, while this PR installs barepymc-forecastand CausalPy permitspymc-extras>=0.3.0. - Make inference diagnostics discoverable. The full HMC/Pathfinder fit remains available at
model.forecaster.idatawhileresult.idatacontains the draw-coherent prediction subset; document that distinction or expose a directfit_idata/fit_resultproperty. Noresult.idataredesign is required in this PR.
Maketables support can be a separate CausalPy issue. Forecasting parameters do not necessarily map to Patsy coefficients, so that needs an explicit table-semantics decision rather than just another dispatch branch. For this PR, documenting the limitation and raising an intentional error is enough.
The distribution/import naming (pymc-forecast / pymc_forecast), optional minor pin, draw-coherence work, output normalization, and ITS integration tests all look appropriate. CI is green. With the placebo/refit path, statespace semantic boundary, and central architecture contracts addressed, this will establish a sound direction for future forecast-backed experiments.
…t_idata Review feedback on #1014: - PlaceboInTime now accepts any Bayesian backend: validate() checks the experiment's _model_backend.is_bayesian instead of isinstance PyMCModel, and PyMCForecastModel gains _clone() so clone_model() can refit the same spec on placebo folds. Covered by a 2-fold clone-and-refit test. - Statespace backends are rejected at construction instead of silently substituting the noisy predictive for mu; the mu/mu_future lookups are now strict. Tracked upstream as pymc-labs/pymc-forecast#50. - fit_idata property exposes the full forecaster fit result (complete NUTS InferenceData with sample stats), distinct from idata's thinned draw-coherent posterior; documented in the module docstring. - ARCHITECTURE.md describes the third backend and supports_pymc_forecast; BaseExperiment / ModelAdapter.model annotations and docstrings include PyMCForecastModel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks for the review @drbenvincent — all four points addressed in 9f09035:
118 tests green locally across the adapter, placebo-in-time, model-adapter, panel-regression, and guard-script suites, plus doctests and the full hook set (mypy, numpydoc, ruff, architecture/exports). 🤖 Generated with Claude Code |
|
Thanks @twiecki — I rechecked the follow-up commit and the substantive review points are addressed: placebo refitting is covered, statespace now fails safely with pymc-forecast#50 tracking the missing capability, and the architecture, typing, diagnostics, tests, and current CI look good. The upstream statespace issue does not need to block this PR. The one remaining item is the optional dependency contract: the adapter advertises Pathfinder, but the extras still install bare |
The adapter advertises PathfinderForecaster, but the forecast/test extras installed bare pymc-forecast while CausalPy only floors pymc-extras at 0.3. Upstream declares the integration as pymc-forecast[extras] with pymc-extras>=0.10 — use that marker in both extras (and the install hint) so the optional-dependency contract matches what the module documents. Resolves cleanly against the existing pymc/pytensor pins (pymc-extras 0.10 with pymc 5.28 / pytensor 2.38). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@drbenvincent done. |
|
One final scope clarification before approval, following a broader look at CausalPy's prediction semantics. Current behavior: CausalPy's Boundary exposed by this PR: No adapter redesign or upstream release should block this PR. Please add a short note documenting the current requirement: models using a link function must pass the inverse-linked conditional expectation in observed outcome units to We have opened follow-up issues so the temporary convention and the intended long-term design are historically connected:
The existing statespace limitation remains tracked in pymc-labs/pymc-forecast#50. |
|
FYI: No action needed from @twiecki. I'm going to get an agent to blast through this and the issues it generated |
Co-authored-by: Cursor <cursoragent@cursor.com>
Closes #1013.
Implements the ITS-only first pass: a thin adapter that lets a
pymc_forecastforecasting model act as a model provider behind CausalPy's existing experiment API, in the same spirit as the sklearn wrapper. CausalPy keeps identification, counterfactual construction, and placebo methods;pymc_forecastprovides the fitted forecasting model.What's in here
causalpy/pymc_forecast_models.py—PyMCForecastModel, a thin wrapper (comparable in size toskl_models.py) around apymc_forecastmodel function /ForecastingModelplus a forecaster class (NUTSHMCForecasterby default, matching CausalPy's native backends; ADVI/Pathfinder are explicit opt-ins). Maps the experiment protocol onto the upstream 0.1.0 API exactly as scoped in the issue:fit(X, y)→ construct + fit the forecaster on the pre-period; the patsy design matrix rides along as covariates (obs_ind → time,coeffs → covariate).predict(X)(pre) →predict_in_sample(),posterior_predictivegroup.predict(X, out_of_sample=True)(post, "as if untreated") →forecast(future_covariates=post_X)with exogenous regressors, orforecast(future_index=post_index)for a covariate-free trend model.prediction_samples(result)and the documented schema dims are renamed onto CausalPy coords (time_future → obs_ind,series → treated_units).Prior), keeping the transparent-prior ethos.PyMCForecastAdapterinexperiments/model_adapter.py, gated by a newsupports_pymc_forecastflag — enabled forInterruptedTimeSeriesonly (SyntheticControlis a tracked follow-up). No change to the public API/UX: users just passmodel=PyMCForecastModel(...)tocp.InterruptedTimeSeries.pip install causalpy[forecast]→pymc-forecast>=0.1.0,<0.2(minor pinned while upstream is 0.x; the output schema is the surface upstream contract-tests). Also added to thetestextra so CI exercises the adapter; the test moduleimportorskips it.test_pymc_forecast_adapter.py, 12 tests): fit pre / forecast post-as-untreated /calculate_impacton draw-level samples, with the output contract and recovered effect checked against the existing PyMCLinearRegressionpath on the same synthetic data; plus the covariate-freefuture_indexpath, three-period designs, plot/summary()/effect_summary()/get_plot_data_bayesian()smoke, single-unit validation, and adapter resolution/gating.PyMCModelclasses" guidance (referenced from the ITS docstring), and the module is added to the API reference index.Out of scope (per the issue)
No outsourcing of placebo-in-time, power analysis, or power curves — the boundary stays at model-provider + forecasting metrics.
Caveats
Coefficient printing reports posterior summaries of the model's scalar parameters instead of design-matrix coefficients.
Resolved with pymc-forecast 0.2:pymc_forecastexposes the draw-level posterior predictive of the observed variable rather than a separate noise-freemu...munow carries the upstream noise-free latent predictor (mu/mu_future), so causal impact excludes observation noise exactly like the native backends, one posterior subsample is shared by the pre-period fit and the counterfactual (draw coherence viaposterior=), and the forecaster uses the deferred-fit lifecycle. Pinnedpymc-forecast>=0.2,<0.3.🤖 Generated with Claude Code