Skip to content

Add built-in GeneralizedLinearRegression for count and binary outcomes (#1022) - #1024

Open
drbenvincent wants to merge 8 commits into
mainfrom
feature/generalized-linear-regression
Open

drbenvincent wants to merge 8 commits into
mainfrom
feature/generalized-linear-regression

Conversation

@drbenvincent

@drbenvincent drbenvincent commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #1022

  • Adds GeneralizedLinearRegression with curated families (Gaussian, Poisson, Negative Binomial, Bernoulli) and canonical links, exposing response-scale mu for the prediction contract (Define outcome-scale prediction semantics across model backends #1016).
  • Refactors LinearRegression as a Gaussian/identity specialization of the new GLM backend.
  • Routes non-identity DiD and PrePostNEGD ATT through g-computation on treated units; RD/RK plots skip R² when score() is unavailable.
  • Documents the GLM API in the prediction contract and estimands knowledgebase pages.

Test plan

  • prek run --all-files
  • make test-patch-cov (1252 passed, 96% patch coverage)
  • CI green on this PR

Made with Cursor

#1022)

Introduce a curated GLM backend with canonical families/links, response-scale mu for the prediction contract, and g-computation ATT for non-identity DiD and PrePostNEGD estimands.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions github-actions Bot added enhancement New feature or request major OSS_PRODUCT OSS_PRODUCT project priorities. Labs members should get approval before logging hours. review:high High-impact change requiring thorough human review labels Jul 18, 2026
@read-the-docs-community

read-the-docs-community Bot commented Jul 18, 2026

Copy link
Copy Markdown

Documentation build overview

📚 causalpy | 🛠️ Build #33709025 | 📁 Comparing 87aa49f against latest (62f7bea)

  🔍 Preview build  

185 files changed · + 38 added · ± 97 modified · - 50 deleted

+ Added

± Modified

- Deleted

@codecov

codecov Bot commented Jul 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.27834% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.75%. Comparing base (62f7bea) to head (36eb87e).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
causalpy/tests/test_glm.py 98.83% 3 Missing and 2 partials ⚠️
causalpy/pymc_models.py 98.03% 2 Missing and 1 partial ⚠️
causalpy/experiments/synthetic_control.py 0.00% 1 Missing and 1 partial ⚠️
causalpy/tests/test_glm_recovery.py 95.00% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1024      +/-   ##
==========================================
+ Coverage   95.61%   95.75%   +0.13%     
==========================================
  Files         103      105       +2     
  Lines       16433    17098     +665     
  Branches      956     1012      +56     
==========================================
+ Hits        15713    16372     +659     
- Misses        509      513       +4     
- Partials      211      213       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@drbenvincent drbenvincent left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer: GPT-5.6 Sol High

The core eta → inverse link → response-scale mu → y_hat design is sound, and keeping LinearRegression as a thin Gaussian/identity specialization is the right compatibility choice. However, I recommend changes before merge because the estimator dispatch and test evidence do not yet support the breadth claimed by this major feature.

Must-fix

1. Use g-computation consistently for Bayesian causal estimands

In causalpy/pymc_models.py::model_uses_identity_link, models without an explicit capability flag default to identity-link behavior. DifferenceInDifferences.algorithm and PrePostNEGD.algorithm therefore send contract-compliant custom linked models down the coefficient path, producing a link-scale parameter rather than a response-scale causal effect. The identity branch in PrePostNEGD is also not generally an ATT when group–covariate interactions are present.

The simpler and safer rule is estimand-based: all Bayesian DiD and PrePostNEGD models already promise response-scale mu, so always contrast factual and counterfactual mu draws. Keep coefficient extraction only for OLS. This also removes model_uses_identity_link and makes Gaussian additive models an equivalent special case.

2. Add tests that establish causal correctness, not only execution

causalpy/tests/test_glm.py::test_poisson_its_recovers_simulated_effect uses mocked sampling and only checks that cumulative impact is finite; it never compares the estimate with the known effect. The DiD and PrePostNEGD tests assert dimensions but not direction or magnitude. RD, RK, PiecewiseITS, and Staggered DiD are primarily smoke tests.

There is also a concrete false-positive in test_poisson_piecewise_its_smoke: t is normalized to [0, 1], while the formula keeps step(t, 40) and ramp(t, 40), so the interruption terms are always zero.

Please add deterministic numerical assertions for the g-computation contrasts and one lightweight unmocked ITS recovery test whose posterior interval contains the known daily/cumulative truth. The PiecewiseITS interruption must lie inside the modeled time range.

3. Preserve the existing default_priors extension point

GeneralizedLinearRegression.__init__ calls PyMCModel.__init__, which assembles priors from self.default_priors, and then immediately replaces self.priors with _default_priors_for_family(...). Existing LinearRegression subclasses that override default_priors are therefore silently ignored. Please retain one authoritative prior source while preserving this released subclass contract.

Should-fix before the public API freezes

  • PrePostNEGD._glr_att_from_g_computation performs two posterior-predictive calls per treated row and also samples unused y_hat. Build treated/control design matrices in bulk and make two prediction calls total.
  • ARCHITECTURE.md says GLR can be used in every LinearRegression experiment, including PanelRegression. Non-Gaussian outcomes are invalid with fe_method="demeaned", because demeaning creates negative/non-integral responses. Reject that combination or narrow the support claim to dummy fixed effects.
  • The public link argument currently adds no capability because every non-canonical pairing is rejected. For v1, family alone with a read-only resolved link is a smaller API.
  • A custom priors["y_hat"] can contradict the declared family without validation. Either validate the distribution or reserve this override for the Gaussian compatibility path.
  • Document the identification scale for nonlinear DiD: Poisson/log imposes untreated parallel trends on the log-mean scale, while Bernoulli/logit imposes it on the log-odds scale, even though the reported ATT is transformed to an additive response-scale contrast.
  • Clarify that the current DiD ATT weights treated-post rows, not treated units, and reconcile the detailed estimand text with the quick-reference table, which still says DiD is coefficient-based.
  • Remove or qualify “rate” support until there is an exposure/offset API. The current implementation supports counts conditional on covariates, not a fixed-exposure rate model.

What worked well

  • mu is inverse-linked draw by draw before effects are computed.
  • The family likelihood heads and Negative Binomial alpha customization are compact and understandable.
  • _clone() preserves family, link, priors, and sampling configuration.
  • Returning None rather than a misleading non-Gaussian Bayesian R² is conservative.
  • Keeping LinearRegression preserves imports, default experiment wiring, type identity, and the common Gaussian path.

If these issues are resolved, this genuinely unlocks count and binary outcomes across ITS, Piecewise ITS, DiD, PrePostNEGD, RD, RKD, Staggered DiD, and appropriately configured panel regressions, with effects reported in counts or probability points rather than link-scale coefficients.

Merge readiness: changes recommended. The branch is current and mergeable, and remote CI is green; the blockers are statistical-contract and public-API issues rather than mechanical failures.

@drbenvincent
drbenvincent marked this pull request as draft July 18, 2026 22:59
…d correctness tests

Route all Bayesian DiD and PrePostNEGD effects through response-scale mu contrasts, simplify GeneralizedLinearRegression to family-only construction with preserved default_priors subclassing, reject demeaned panel FE for non-Gaussian GLRs, and add deterministic estimand checks plus one real-posterior ITS recovery test.

Co-authored-by: Cursor <cursoragent@cursor.com>
@review-notebook-app

Copy link
Copy Markdown

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

@drbenvincent

Copy link
Copy Markdown
Collaborator Author

Thanks @drbenvincent — pushed 31c7d4c0 addressing the review. Summary of what changed, how, and why, plus what I deliberately left for later.

Must-fix

1. Uniform Bayesian g-computation for DiD / PrePostNEGD

What: Removed model_uses_identity_link and the identity/coefficient dispatch entirely. Bayesian DifferenceInDifferences and PrePostNEGD now always compute ATT from factual minus counterfactual response-scale mu draws. OLS DiD still uses the interaction coefficient.
How: _att_from_g_computation() in both experiments calls predict(..., var_names=["mu"]) so we do not sample unused y_hat on the estimand path. PrePost builds treated/control design matrices in bulk (two prediction calls total, not per-row).
Why: Every compliant PyMCModel already exposes outcome-scale mu; estimand logic should not guess link behaviour from model type. This also fixes custom linked models and PrePost cases with group×covariate interactions where a main-effect coefficient is not the ATT.

2. Correctness tests (not just smoke)

What:

  • Deterministic FixedMuRegression test double with fixed log-link mu; exact DiD and PrePost ATT assertions, including a group×pre interaction where coefficient extraction would be wrong. PrePost test records that exactly two bulk mu-only predictions are used for the effect.
  • Fixed test_poisson_piecewise_its_smoke: interruption threshold now matches normalized t (step/ramp columns asserted nonzero).
  • New test_glm_recovery.py::test_poisson_its_posterior_recovers_level_shift uses real MCMC via a function-scoped real_pymc_sample fixture (session prior mock preserved elsewhere). Deterministic count DGP on log-mean scale; HDI checks for daily and cumulative impact.
    Why: Prior-sampling mocks are fine for plumbing but cannot validate estimands. One isolated real-posterior test gives genuine coverage without slowing the whole suite.

3. default_priors subclass contract

What: GeneralizedLinearRegression.default_priors is now a property merging _default_priors_for_family(family) with subclass default_priors overrides in the MRO. PyMCModel.__init__ remains the single merge point (defaults → user priors); we no longer overwrite after super().__init__.
Why: Restores the released extension point for LinearRegression subclasses without a second prior assembly path.

Should-fix (also done in this push)

Item Change
PrePost vectorization Two bulk mu predictions over all treated rows
Panel + demeaned + non-Gaussian GLR Construction-time ValueError; docs/ARCHITECTURE qualified
Public link arg Removed; family only, read-only link property
Custom priors["y_hat"] on non-Gaussian families Rejected at construction with a clear error
Nonlinear identification scale Documented in estimands.md (log-mean / log-odds parallel trends; reported effects remain response-scale contrasts)
DiD weighting + quick-reference Documented row-weighted treated-post averaging; table distinguishes OLS vs Bayesian
“Rate” claims Removed from prediction-contract.md; count means only until offset/exposure API exists
custom_pymc_models.ipynb Likelihood override narrowed to Gaussian LinearRegression; count/binary → GeneralizedLinearRegression

Validation

  • Targeted GLM / recovery / panel / input-validation tests pass (mocked + real sampler in one run).
  • make doctest, make html, make test-patch-cov (97% patch coverage), prek run --all-files all green locally.

Not fixed yet (intentionally deferred)

  • Deeper correctness tests for RD, RK, Staggered DiD — still smoke/plumbing coverage; same pattern as the new DiD/PrePost deterministic tests could be added in a follow-up if useful.
  • Offset / exposure (“rate”) models — out of scope for this PR; docs no longer claim rate support.
  • Stale API autodoc stub for removed model_uses_identity_link may linger until docs are regenerated on RTD (harmless warning locally).

Happy to iterate on any of the deferred items or tighten the recovery test budget if CI runtime is a concern.

@drbenvincent
drbenvincent marked this pull request as ready for review July 19, 2026 06:40
drbenvincent and others added 2 commits July 19, 2026 08:57
Exclude test files from diff-cover, raise the local fail-under to 96%, pin Codecov patch to 95% on causalpy/, and add targeted tests for previously uncovered production paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
Clarify that make test-patch-cov stays out of prek because it runs the full suite (~90s+) and should run at PR-ready or large-task handoff points instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

@drbenvincent drbenvincent left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer: GPT-5.6 Sol High, reviewing on behalf of @drbenvincent.

I support the core direction here. A curated GLR with canonical families, draw-wise inverse linking, response-scale mu, and LinearRegression retained as the Gaussian specialization is the right basic design. The second commit also materially improved the DiD/PrePost estimands.

I still think this needs changes before merge, primarily to make the model/experiment boundary principled and to fix two concrete public-API bugs.

Must-fix

1. Subclass prior overrides still drop required family priors

In causalpy/pymc_models.py:382,784-794, the new default_priors property is shadowed whenever a subclass defines the existing class-level default_priors extension point. For _CustomPoissonGLR, PyMCModel.__init__ therefore sees only the subclass’s beta; the family’s y_hat is absent and fitting fails with KeyError: "y_hat".

The tests at causalpy/tests/test_glm.py:252-274 do not catch this: they inspect the override or invoke the base descriptor manually, but never assert that model.priors retains y_hat and never fit the subclass.

Please use one non-shadowable prior-merging path and add a subclass fit test. The implementation should not require callers to invoke a base descriptor manually.

2. predict() breaks for natural integer count/binary outcomes

PyMCModel._data_setter() (causalpy/pymc_models.py:456-458) replaces y with a float64 zero array. If a Poisson, Negative Binomial, or Bernoulli model was fit using an integer y, PyMC registered integer mutable data and prediction then fails with a dtype error.

Please preserve the fitted data node’s dtype when creating the prediction placeholder and add an integer-outcome fit()predict(var_names=["mu"]) regression test.

The GLR should also validate response support at the fit boundary: Bernoulli values must be 0/1; Poisson/NB values must be finite, non-negative, and integer-like. Otherwise users receive opaque PyMC sampling errors.

3. State the estimand boundary independently of backend type

The current implementation is moving in the right direction, but “Bayesian versus OLS” is being used as a proxy for estimand computation in DifferenceInDifferences.algorithm() (diff_in_diff.py:223-235). That is not the durable boundary: sklearn also supplies outcome-scale expected predictions, while some Bayesian models do not satisfy the response-scale mu contract.

Please make the ownership rule explicit:

  • The model/backend owns E[Y | X, θ] on the response scale and the uncertainty representation.
  • The experiment owns the intervention/counterfactual construction, evaluation population, and aggregation.
  • “Counterfactual prediction/contrast” is the broad computational primitive. G-computation is the treatment-standardization subset, not a synonym for every prediction-based estimator.

For DiD, retaining the OLS interaction coefficient is fine as an algebraic shortcut under the currently validated additive design, but document and test its equivalence to the experiment’s ATT rather than defining the estimand by backend.

A compact matrix in ARCHITECTURE.md (probably not estimands.md because #1033 is moving specifics out of docs into docstrings, to avoid drift between code and docs) should identify, for each experiment, its counterfactual operation and required model capability:

  • observed minus counterfactual expectation: ITS, Synthetic Control, Staggered DiD;
  • factual minus counterfactual expected-outcome contrast: DiD, PrePostNEGD;
  • local expected-outcome geometry: RD and RK;
  • specialized/non-portable estimators: IV, IPW, SDiD;
  • PanelRegression: model fitting and link-scale coefficient inference only until it has an explicit treatment contrast and target-population API.

I would not add a generic g_computation.py abstraction in this PR: the interventions are genuinely experiment-specific. Establish the semantic contract first; extract a helper later only if repeated code has the same semantics.

4. Correct the claimed model/experiment compatibility

The committed custom_pymc_models.ipynb says implementing one build_model() makes a custom model work with all CausalPy experiment classes, plots, summaries, and diagnostics. That is false for experiments with specialized model signatures or estimators (IV, IPW, SDiD), and optional methods such as scoring/printing are not uniformly supported.

Please narrow this to the standard regression experiments that actually satisfy the contract and connect it to the support/estimand matrix above. Similarly, describe Panel + non-Gaussian GLR as coefficient-level support, not a completed response-scale causal estimand.

Required behavioral evidence

Please keep the existing deterministic intervention tests, but add the smallest tests that pin the actual contracts:

  1. A GLR subclass with partial default priors can fit and retains the family likelihood.
  2. Integer Poisson/Bernoulli data can fit and predict.
  3. Standard Gaussian DiD and PrePostNEGD remain equivalent to their former coefficient estimands where that equivalence is expected.
  4. At least one built-in GLR experiment test asserts a numerical expected-outcome contrast, rather than only type/dimension checks.
  5. The Poisson panel test uses actual non-negative integer count data; it currently reuses a continuous Gaussian fixture containing invalid Poisson outcomes under mocked sampling.

Should-fix

  • DifferenceInDifferences plots collapse treated rows with groupby(time).first() while the reported ATT averages all treated-post rows (diff_in_diff.py:197-209 versus 310-336). Either align the populations or label the plot as one representative trajectory.
  • Decide whether score-less models are unsupported in Synthetic Control and reject them early, or handle score is None in synthetic_control.py:919-932. Patching individual plots after they fail is evidence that backend kind is too coarse a capability boundary.
  • Document that Regression Kink with a nonlinear link reports a change in the slope of expected counts/probabilities, not a link-scale coefficient.
  • Split commits 16eb78bb and 03e25b80 (Codecov/Makefile/maintainer workflow policy) into a separate PR. They are repo-wide process changes unrelated to the GLR feature and complicate review and revert.

What is already strong

  • eta → inverse link → mu → y_hat is the correct prediction contract.
  • The family-only public API is appropriately small; this does not need a Bambi dependency.
  • Bayesian DiD and PrePostNEGD now contrast response-scale expectations, including interaction-aware PrePost effects.
  • The new ATT calculations are vectorized and request only mu.
  • The docs correctly distinguish link-scale identification assumptions from response-scale reported effects.

Merge readiness: changes requested. The branch is current, mergeable, and CI is green; the remaining blockers are API correctness and the conceptual support contract, not mechanical CI failures.

drbenvincent and others added 2 commits July 21, 2026 23:38
Revert the Makefile/codecov/docs coverage-gate changes to main; they are repo-wide process changes unrelated to the GLR feature and now live in their own PR, per review.

Co-authored-by: Cursor <cursoragent@cursor.com>
…d estimand ownership.

Merge family priors non-shadowably under subclass default_priors so partial overrides keep the family y_hat; preserve the fitted y node dtype in _data_setter and validate outcomes against the family support at the fit boundary; document the experiment-owned estimand rule with a capability matrix in ARCHITECTURE.md and equivalence comments in DiD; narrow custom-model compatibility claims in the knowledgebase notebook; handle score-less models in Synthetic Control titles and document RK/DiD reporting semantics. Adds subclass-fit, integer-outcome, Gaussian-equivalence, OLS-shortcut, and GLR numeric-contrast tests, and fixes the Poisson panel test to use real count data.

Co-authored-by: Cursor <cursoragent@cursor.com>
@drbenvincent

Copy link
Copy Markdown
Collaborator Author

Pushed cca16be and 36eb87e addressing the second review.

Must-fix

  1. Subclass prior overrides: removed the shadowable default_priors property. GeneralizedLinearRegression.__init__ now merges the family's required priors underneath whatever PyMCModel.__init__ assembled, so a partial class-level default_priors override can never drop the family y_hat. No base-descriptor invocation needed. New tests: test_glr_subclass_partial_default_priors_retain_family_priors and test_glr_subclass_partial_default_priors_can_fit (the subclass actually fits and keeps the Poisson likelihood).
  2. Integer outcomes: _data_setter now creates the prediction placeholder with the fitted y node's dtype. GeneralizedLinearRegression.fit validates response support at the boundary (Bernoulli 0/1; Poisson/NB finite, non-negative, integer-valued) with clear ValueErrors. New tests: test_glr_integer_outcome_fit_and_predict (int64 fit → predict(var_names=["mu"])) and test_glr_fit_rejects_out_of_support_outcomes.
  3. Estimand boundary: added an estimand/capability matrix to ARCHITECTURE.md stating the ownership rule (experiment owns intervention, population, aggregation; model owns response-scale E[Y|X,θ] and uncertainty) and classifying each experiment's counterfactual operation, including PanelRegression as coefficient-inference-only. DifferenceInDifferences.algorithm now documents the OLS interaction coefficient as a tested algebraic shortcut rather than a backend-defined estimand, pinned by test_ols_did_coefficient_equals_prediction_contrast. No generic g_computation.py abstraction added, per the review.
  4. Compatibility claims: custom_pymc_models.ipynb now scopes custom-model support to the standard regression experiments, points at the ARCHITECTURE matrix, names IV/IPW/SDiD as requiring dedicated model classes, and describes Panel + non-Gaussian GLR as coefficient-level support.

Behavioral evidence

  1. Subclass with partial default priors fits and retains the family likelihood ✔ (see must-fix 1)
  2. Integer Poisson/Bernoulli fit and predict ✔ (see must-fix 2)
  3. test_gaussian_did_att_matches_interaction_coefficient and test_gaussian_prepostnegd_att_matches_group_coefficient assert draw-wise equality of the g-computation ATT with the former coefficient estimands under identity link.
  4. test_poisson_glr_did_att_matches_posterior_beta_contrast reconstructs mean(exp(X_f·β) − exp(X_c·β)) from the built-in GLR's own posterior draws and asserts numerical equality with causal_impact.
  5. test_panel_regression_poisson_dummies now generates genuine Poisson counts; the old continuous fixture is rejected by the new fit-boundary validation.

Should-fix

  • DiD plot vs ATT population: documented in the plot() docstring that trajectories use one representative covariate row per time point while the reported ATT averages all treated-post rows.
  • Synthetic Control now handles score is None gracefully in _get_score_title instead of crashing, consistent with the RD/RK handling.
  • Regression Kink docstring now states that gradient_change with a non-identity link is a change in the slope of expected counts/probabilities, not a link-scale coefficient.
  • Commits 16eb78bb/03e25b80 process changes split out to Align local patch coverage gate with Codecov (split from #1024) #1034 and reverted here (the GLR tests from 16eb78bb stay in this PR).

prek run --all-files passes and make test-patch-cov reports 98% patch coverage (1273 passed).

Made with Cursor

@drbenvincent

Copy link
Copy Markdown
Collaborator Author

Status summary

For clarity on where this PR stands after commits cca16beb and 36eb87e6 (details in the comment above):

Resolved — all blockers from the second review:

Review item Status How
Must-fix 1: subclass default_priors drops family y_hat ✅ Fixed Shadowable property removed; family priors merged non-shadowably underneath in __init__; subclass now fitted in tests
Must-fix 2: predict() breaks on integer outcomes ✅ Fixed _data_setter preserves the fitted y dtype; fit boundary validates family support (0/1, non-negative integer counts)
Must-fix 3: estimand defined by backend type ✅ Fixed Ownership rule + estimand/capability matrix in ARCHITECTURE.md; OLS coefficient documented and tested as an algebraic shortcut; no generic g-computation abstraction added
Must-fix 4: overstated custom-model compatibility ✅ Fixed custom_pymc_models.ipynb scoped to standard regression experiments; IV/IPW/SDiD and Panel caveats stated
Behavioral evidence 1–5 ✅ Added Subclass fit, integer fit/predict, Gaussian DiD/PrePostNEGD draw-wise equivalence, built-in GLR numeric contrast, Poisson panel with real counts
Should-fixes (DiD plot label, SC score=None, RK link docs, commit split) ✅ Done Process changes extracted to #1034 and reverted here

Verification: prek run --all-files clean, make test-patch-cov at 98% patch coverage (1273 passed), all CI checks green.

Remaining, not blocking merge of this PR:

No known open issues from either review round remain unaddressed in code.

Made with Cursor

drbenvincent added a commit that referenced this pull request Jul 22, 2026
… milestone check. (#1034)

Exclude test files from diff-cover, raise the local fail-under to 96%, pin the Codecov patch check to 95% on causalpy/, and clarify in AGENTS.md/CONTRIBUTING.md that make test-patch-cov runs at PR-ready or handoff milestones rather than on every commit. Split out of #1024 because these are repo-wide process changes unrelated to the GLR feature.

Co-authored-by: Cursor <cursoragent@cursor.com>
@drbenvincent

Copy link
Copy Markdown
Collaborator Author

MERGE ORDER

@drbenvincent

Copy link
Copy Markdown
Collaborator Author

Heads-up for whoever updates this branch from main: #1036 is about to merge and it reshapes the prediction boundary this PR builds on. Guidance for the rebase:

  • ModelAdapter.predict() is now the single prediction method. It returns response-scale mu as an xr.DataArray with canonical dims (chain, draw, obs_ind, treated_units) on every backend (sklearn returns singleton chain/draw). predict_mu() no longer exists — if a conflict resolution reintroduces it, that's a wrong merge.
  • calculate_impact() / calculate_cumulative_impact() were deleted from PyMCModel, ScikitLearnAdaptor, and PyMCForecastModel. Experiments now compute impact as plain xarray subtraction y - predict(X) and .cumsum(dim="obs_ind"). Don't resurrect the methods when resolving conflicts in the model classes.
  • Nothing about GLM semantics needs to change here. This PR already registers mu = inverse_link(eta) on the response scale, which is exactly what the adapter's predict() extracts (via _extract_mu in model_adapter.py), so impact/effect summaries stay in outcome units automatically. The g-computation ATT paths should consume self._model_backend.predict(...) and get mu draws directly — no posterior_predictive["mu"] digging needed.
  • reporting.py now assumes canonical containers. _extract_window / _extract_counterfactual no longer duck-type across InferenceData/dict/ndarray; the duplicated Bayesian/OLS effect_summary bodies in ITS/SC/PiecewiseITS collapsed onto _effect_summary_timeseries. If this branch touched those, prefer the main side and re-apply the GLM-specific bits on top.
  • If this PR ends up needing link-scale eta or noise-inclusive y_hat downstream of the adapter, the agreed design is a small var_name="mu" parameter on predict() at the _extract_mu seam — not a new family of predict_* methods.

The contract is pinned by causalpy/tests/test_prediction_contract.py (now asserted through the adapter), so a correct merge should leave those tests green.

drbenvincent added a commit that referenced this pull request Jul 22, 2026
…1055)

* Add validation to _data_setter for non-standard data node names (#847)

* Rebase onto main and add happy-path validation test

- Resolved merge conflicts in all three files against origin/main
- Restored upstream class docstring rubric (Parameters + Examples) in
  PyMCModel
- Restored actionable error message pointing to
  BayesianBasisExpansionTimeSeries
- Added test_standard_data_nodes_predict exercising the validation
  happy path to satisfy codecov/patch

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Rebase onto main preserving all upstream classes and PR validation

Previous stash-based resolution accidentally dropped ~330 lines from
pymc_models.py, removing SyntheticDifferenceInDifferencesWeightFitter and
other classes added to main after the branch diverged.

This commit takes origin/main's full pymc_models.py (2652 lines) and
re-applies only the PR's changes: class docstring note, _data_setter
validation, method docstring update, and the codecov happy-path test.

Also adds test_standard_data_nodes_predict to cover the validation
loop's non-error path, resolving the codecov/patch gap.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Use origin/main panel_regression.py (no PR changes needed here)

* Remove unused import (ruff fix)

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Align agent skill names with directory and file conventions. (#1015)

Rename review-pr skill identifier from maintainer-pr-review and rename pr-workflows pre-commit reference to prek.md so frontmatter names match their paths.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Treat datetime formula predictors as continuous (#1007)

Encode bare datetime factors as elapsed days while preserving explicit categorical factors and out-of-sample design compatibility.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add pymc_forecast model-provider adapter for InterruptedTimeSeries (#1014)

* Add pymc_forecast model-provider adapter for InterruptedTimeSeries

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>

* Update adapter to pymc-forecast 0.2: noise-free mu, draw coherence, deferred 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>

* Route pymc-forecast to the pip section of environment.yml

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>

* Give the adapter test fixtures a real NUTS warmup budget

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>

* Restore real pm.sample for the adapter round-trip tests

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>

* Address review: PlaceboInTime support, statespace rejection, docs, fit_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>

* Install pymc-forecast[extras] so the advertised Pathfinder path resolves

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>

* Clarify the outcome-scale contract for linked forecast models

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Benjamin T. Vincent <inferencelab@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* Document and test the outcome-scale prediction contract (#1020)

Co-authored-by: Cursor <cursoragent@cursor.com>

* Reject unsorted or duplicate ITS time indexes before fitting (#1019)

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add a g-computation tutorial for link-function outcome effects (#1021)

Co-authored-by: Cursor <cursoragent@cursor.com>

* Revert g-computation link-functions tutorial notebook (#1021) (#1023)

* Revert "Add a g-computation tutorial for link-function outcome effects (#1021)"

This reverts commit 2ccd098.

* Only require rediraffe redirects for renames, not deletes

A deleted notebook has no meaningful target to redirect to. The
previous behavior forced a redirect to notebooks/index which is
pointless for a notebook that never had external links. Only
renames need redirects so bookmarks follow the moved content.

* [pre-commit.ci] pre-commit autoupdate (#1031)

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.15.21 → v0.15.22](astral-sh/ruff-pre-commit@v0.15.21...v0.15.22)
- [github.com/codespell-project/codespell: v2.4.2 → v2.4.3](codespell-project/codespell@v2.4.2...v2.4.3)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Update UML Diagrams (#1030)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* chore(deps): bump github/codeql-action/upload-sarif in the actions group (#1029)

Bumps the actions group with 1 update: [github/codeql-action/upload-sarif](https://github.com/github/codeql-action).


Updates `github/codeql-action/upload-sarif` from 4.36.3 to 4.37.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@54f647b...99df26d)

---
updated-dependencies:
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Fix residual-shape broadcasting bug in OLS DiD effect_summary (#1026)

_compute_statistics_did_ols computed residuals as y_da - y_pred, where
y_da is an xarray DataArray of shape (n, 1) with dims
(obs_ind, treated_units) and y_pred is a bare (n,) numpy array with no
dimension names. xarray aligned y_pred positionally against the last
axis (treated_units, size 1) instead of obs_ind, producing an (n, n)
array of every observation's y minus every other observation's
prediction instead of n proper residuals.

On the built-in did dataset this inflated the reported SE by roughly
70x (SE ~4.16 instead of ~0.06), making the reported CI
[-7.98, 8.89] instead of the correct [0.34, 0.58].

Fixes it by flattening both operands to 1-D before differencing so
they align by position on obs_ind. Adds a regression test asserting
the residuals have shape (n,) and that the reported SE now matches
the expected value (up to the separately-tracked mean/(n-p)
denominator convention, out of scope here).

* Align the local patch coverage gate with Codecov and document it as a milestone check. (#1034)

Exclude test files from diff-cover, raise the local fail-under to 96%, pin the Codecov patch check to 95% on causalpy/, and clarify in AGENTS.md/CONTRIBUTING.md that make test-patch-cov runs at PR-ready or handoff milestones rather than on every commit. Split out of #1024 because these are repo-wide process changes unrelated to the GLR feature.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Unify predict() to a canonical prediction container and delete the parallel legacy paths (#1036)

* Normalize expected prediction containers across backends

Move response-scale prediction normalization into model adapters so estimand code can share one xarray path across PyMC, sklearn, and forecast models.

Closes #1035

Co-authored-by: Cursor <cursoragent@cursor.com>

* Unify predict() to a canonical DataArray and delete the parallel legacy paths

predict_mu is absorbed into predict(): every backend now returns response-scale expected outcomes as an xr.DataArray with dims (chain, draw, obs_ind, treated_units), with sklearn backends returning singleton chain/draw. This lets experiments compute impact as plain xarray subtraction, removes the fake-InferenceData shim in SDiD, deletes the now-dead calculate_impact/calculate_cumulative_impact model methods, strips the container-sniffing from reporting.py, and collapses the duplicated Bayesian/OLS effect_summary bodies in ITS, SC, and PiecewiseITS onto a shared _effect_summary_timeseries helper.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Update lift-test notebook to the canonical prediction container

post_pred is now a DataArray with (chain, draw, obs_ind, treated_units) dims, so indexing into posterior_predictive no longer applies.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Assert obs_ind alignment before impact subtraction in ITS and SC

Hardens the implicit contract flagged in review: a bare-ndarray X would get arange coords from the adapter and silently corrupt the xarray subtraction against a datetime-indexed y.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop stray non-dim coords in _extract_mu so all backends emit the canonical container

The state-space backend leaked an `observed_state` scalar coord through its posterior predictive, which the new obs_ind alignment asserts correctly caught in CI (state-space ITS test and placebo-in-time notebook).

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* Unify `_bayesian_plot` / `_ols_plot` into a single backend-agnostic `_plot` (#1053)

* Unify ITS plotting into a single backend-agnostic _plot (#1037)

Add has_posterior_draws / extract_r2_score helpers to plot_utils and replace InterruptedTimeSeries._bayesian_plot / _ols_plot and the get_plot_data_* pair with single implementations keyed on data properties of the canonical prediction container. Base _render_plot now calls _plot(), with a transitional dispatch for experiments not yet migrated. Output verified pixel-identical for both backends against seeded baselines.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Unify SyntheticControl plotting into a single backend-agnostic _plot (#1037)

Replace the _bayesian_plot / _ols_plot and get_plot_data_* pairs with single implementations keyed on has_posterior_draws. Output verified pixel-identical for both backends against seeded baselines. As a side effect, plot_predictors and per-unit get_plot_data(treated_unit=...) now also work with the sklearn backend.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Unify DifferenceInDifferences plotting into a single backend-agnostic _plot (#1037)

Replace _bayesian_plot / _ols_plot with one implementation branching on has_posterior_draws; the causal-impact arrow annotation is now shared. Output verified pixel-identical for both backends against seeded baselines.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Unify PiecewiseITS plotting into single _plot / get_plot_data

Replace paired _bayesian_plot/_ols_plot and get_plot_data_bayesian/get_plot_data_ols with backend-agnostic methods keyed on has_posterior_draws. Net -52 lines.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Unify RegressionDiscontinuity plotting into single _plot

Replace paired _bayesian_plot/_ols_plot with one backend-agnostic method keyed on has_posterior_draws; shared scatter layers, threshold and donut lines are no longer duplicated. Net -51 lines.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Unify PanelRegression plotting into single _plot / get_plot_data

Replace _bayesian_plot/_ols_plot wrappers and paired get_plot_data_* with backend-agnostic methods; plot_residuals now uses the unified get_plot_data. The coefficient forest-vs-bar branch stays (genuinely backend-specific: az.plot_forest needs idata), as does the fitted-values branch (no canonical in-sample prediction container stored). Drop tests asserting the removed wrong-backend guards.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Unify StaggeredDiD plotting into single _plot / get_plot_data

Merge the paired event-study plots, group-time plots, and segment renderers into backend-agnostic methods. Uncertainty rendering keys on has_posterior_draws and on which columns the aggregation produced (att_lower/att_upper vs att_std/n_obs) rather than on backend identity. The placebo-data helpers stay split (HDI bounds need draws, dispersion needs only residuals) behind a data-property dispatch.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Rename _bayesian_plot to _plot in Bayesian-only experiments

SDiD, RegressionKink, and PrePostNEGD support a single backend, so their plot methods need no dispatch. Drop SDiD's dead _ols_plot stub (OLS is already rejected at fit time).

Co-authored-by: Cursor <cursoragent@cursor.com>

* Remove backend plot dispatch from BaseExperiment

All experiments now implement a single backend-agnostic _plot / get_plot_data, so the transitional is_bayesian/is_ols dispatch and the _bayesian_plot/_ols_plot/get_plot_data_bayesian/get_plot_data_ols stubs are gone. Update ARCHITECTURE.md and the review-pr skill to describe the new contract.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Key R2 title rendering on score dispersion, not container type

Review follow-up on #1053: ITS and PiecewiseITS titled the plot via isinstance(score, pd.Series) while the bands keyed on has_posterior_draws, so a future backend breaking that pairing could disagree between title and bands. Use the same data property (r2_std_val is not None) that SyntheticControl._get_score_title already uses. Output verified pixel-identical on both backends.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add seeded batch plot-generation script for visual regression review (#1054)

Renders every experiment x backend figure via the public plot() API with fixed seeds (data, MCMC, matplotlib), saving tagged PNGs to .scratch/plot_validation/. Re-runs in the same environment are byte-identical, so plot refactors can be gated with cmp (as done for #1053, 19/19 identical) and deliberate visual migrations like #990 can be reviewed by eyeballing tagged before/after pairs.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Re-apply declarative plotting foundation onto unified _plot architecture

base.py keeps main's single backend-agnostic _plot dispatch but _plot may now return PlotSpec | ggplot | (fig, ax); _finalize_plot renders declarative results and normalizes everything to the stable (Figure, Axes) contract, with plotnine-aware legend mutation. plot_utils.py gains the #990 declarative stack (PlotSpec, CausalPanelData, build_causal_panel_plot, dataarray_draws, summarize_draws, spaghetti_draws, posterior_kind_layers, validation helpers) alongside main's has_posterior_draws/extract_r2_score. The InferenceData-specific prediction_draws is dropped: the canonical prediction container from #1036 is a DataArray, so dataarray_draws is the single extraction seam.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Port InterruptedTimeSeries to unified declarative _plot

The single backend-agnostic _plot now builds CausalPanelData from the canonical prediction container via dataarray_draws and renders through build_causal_panel_plot; point-estimate backends flow through the same pipeline with uncertainty ribbons collapsing to mean lines. The R2 title keys on score dispersion via extract_r2_score. The matplotlib singleton-HDI marker is superseded by the panel builder's post_index point-range path. build_causal_panel_plot now categorizes posterior layer frames so facet order is deterministic under plotnine's default table ordering.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Port SyntheticControl to unified declarative _plot

Single _plot builds CausalPanelData from the canonical container via dataarray_draws (per treated unit) and renders through build_causal_panel_plot; donor trajectories become a geom_line overlay and the treatment line a geom_vline, so the axis-unit conversion helper is no longer needed.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Port SyntheticDiD and PiecewiseITS to unified declarative _plot

Both build CausalPanelData from the canonical container via dataarray_draws and render through build_causal_panel_plot. PiecewiseITS joins a time lookup so all panels share the t axis, keys its R2 title on score dispersion, and keeps axis labels as a narrow PlotSpec overlay. SDiD gains the figsize plot kwarg and drops the matplotlib axis-unit conversion helper.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Port RegressionDiscontinuity and RegressionKink to declarative _plot

Both build tidy point/draw tables from the canonical container (dataarray_draws joined to the x_pred grid) and render via posterior_kind_layers, with thresholds and donut boundaries as legend-mapped geom_vline layers. RD keys its title and fit-line label on has_posterior_draws so point-estimate backends get the plain R2/model-fit rendering through the same pipeline; seaborn is no longer needed.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Port DifferenceInDifferences to unified declarative _plot

Co-authored-by: Cursor <cursoragent@cursor.com>

* Port PrePostNEGD to unified declarative _plot

Co-authored-by: Cursor <cursoragent@cursor.com>

* Port StaggeredDiD to unified declarative _plot

Co-authored-by: Cursor <cursoragent@cursor.com>

* Port PanelRegression to declarative plotnine rendering

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop dead matplotlib posterior helpers from plot_utils

Co-authored-by: Cursor <cursoragent@cursor.com>

* Reconcile tests with unified declarative plotting

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Thomas Wiecki <thomas.wiecki@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mariappan Subramanian <mariappanmts@gmail.com>
drbenvincent and others added 2 commits July 22, 2026 15:33
…inear-regression

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	causalpy/experiments/regression_discontinuity.py
#	causalpy/experiments/synthetic_control.py
#	causalpy/tests/test_prediction_contract.py
#	docs/source/knowledgebase/prediction-contract.md
Co-authored-by: Cursor <cursoragent@cursor.com>
@drbenvincent

drbenvincent commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

TODO (note to self)

  • Confirm the slow CI jobs from the main-sync push went green: test (3.11/3.14), pymc + mixed notebook jobs, Read the Docs — all green as of 2026-07-22 15:55 UTC+1
  • Human read-through of the policy-stating docs: estimand/capability matrix in ARCHITECTURE.md, narrowed claims in custom_pymc_models.ipynb, estimands.md wording, and the updated authoring checklist in prediction-contract.md
  • Decide on merge order vs Clarify causal estimand extraction approaches #1033: either merge Clarify causal estimand extraction approaches #1033 first and align docstrings here, or drop that ordering and merge this directly
  • Approve + merge (no unresolved review threads; no known code work left)

Context: branch was synced with main on 2026-07-22 — resolved conflicts against the unified _plot refactor (#1053) and canonical prediction container (#1036), non-Gaussian GLM plots now fall back gracefully when score() is skipped. Full suite green locally, 99% patch coverage.

@ErikRingen

Copy link
Copy Markdown
Contributor

Am I right in my reading that one has to set a single prior on all coefficients, so same for intercept and slopes? This PR made me think about the trickiness of setting good default priors for GLMs. Whether N(0,1) is a good intercept prior seems entirely dependent on the magnitude of the count, with the current prior assuming a range of only [0.14, 7.4]. If you had a larger count you would want something bigger, but then you wouldn't also want to scale up the predictor betas because it is multiplicative.

So two suggestions:

(1) allow different spec of intercept and slope priors. PyMC marketing does this for loglink models: https://github.com/pymc-labs/pymc-marketing/blob/07b1a2046bdb7e0397cd8e488145553eed32ee28/pymc_marketing/mmm/link.py#L240

(2) add the sample mean as an offset for count models, so that a generic prior makes sense regardless of count magnitude, i.e., log(mu) = log(y_bar) + b0 + b)

b0 ~ N(0,1)

@ErikRingen

Copy link
Copy Markdown
Contributor

For the _plot() method, we might want to add a parameter to plot on the response or link scale. For example, given that log-link diff-in-diff makes the parallel assumptions trend on the log scale, that would be the appropriate scale to visually interrogate that assumption.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request major needs:maintainer-decision Maintainer direction needed before review can conclude OSS_PRODUCT OSS_PRODUCT project priorities. Labs members should get approval before logging hours. review:high High-impact change requiring thorough human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add GeneralizedLinearRegression with curated family and link support

3 participants