Add built-in GeneralizedLinearRegression for count and binary outcomes (#1022) - #1024
drbenvincent wants to merge 8 commits into
Conversation
#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>
Codecov Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|
drbenvincent
left a comment
There was a problem hiding this comment.
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_computationperforms two posterior-predictive calls per treated row and also samples unusedy_hat. Build treated/control design matrices in bulk and make two prediction calls total.ARCHITECTURE.mdsays GLR can be used in everyLinearRegressionexperiment, includingPanelRegression. Non-Gaussian outcomes are invalid withfe_method="demeaned", because demeaning creates negative/non-integral responses. Reject that combination or narrow the support claim to dummy fixed effects.- The public
linkargument currently adds no capability because every non-canonical pairing is rejected. For v1,familyalone 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
muis inverse-linked draw by draw before effects are computed.- The family likelihood heads and Negative Binomial
alphacustomization are compact and understandable. _clone()preserves family, link, priors, and sampling configuration.- Returning
Nonerather than a misleading non-Gaussian Bayesian R² is conservative. - Keeping
LinearRegressionpreserves 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.
…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>
|
Check out this pull request on See visual diffs & provide feedback on Jupyter Notebooks. Powered by ReviewNB |
|
Thanks @drbenvincent — pushed Must-fix1. Uniform Bayesian g-computation for DiD / PrePostNEGDWhat: Removed 2. Correctness tests (not just smoke)What:
3.
|
| 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-filesall 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_linkmay 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.
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>
There was a problem hiding this comment.
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:
- A GLR subclass with partial default priors can fit and retains the family likelihood.
- Integer Poisson/Bernoulli data can fit and predict.
- Standard Gaussian DiD and PrePostNEGD remain equivalent to their former coefficient estimands where that equivalence is expected.
- At least one built-in GLR experiment test asserts a numerical expected-outcome contrast, rather than only type/dimension checks.
- 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
DifferenceInDifferencesplots collapse treated rows withgroupby(time).first()while the reported ATT averages all treated-post rows (diff_in_diff.py:197-209versus310-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 Noneinsynthetic_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
16eb78bband03e25b80(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_hatis 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.
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>
|
Pushed cca16be and 36eb87e addressing the second review. Must-fix
Behavioral evidence
Should-fix
Made with Cursor |
Status summaryFor clarity on where this PR stands after commits Resolved — all blockers from the second review:
Verification: Remaining, not blocking merge of this PR:
No known open issues from either review round remain unaddressed in code. Made with Cursor |
… 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>
MERGE ORDER
|
|
Heads-up for whoever updates this branch from
The contract is pinned by |
…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>
…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>
TODO (note to self)
Context: branch was synced with main on 2026-07-22 — resolved conflicts against the unified |
|
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) |
|
For the |
Summary
Closes #1022
GeneralizedLinearRegressionwith curated families (Gaussian, Poisson, Negative Binomial, Bernoulli) and canonical links, exposing response-scalemufor the prediction contract (Define outcome-scale prediction semantics across model backends #1016).LinearRegressionas a Gaussian/identity specialization of the new GLM backend.score()is unavailable.Test plan
prek run --all-filesmake test-patch-cov(1252 passed, 96% patch coverage)Made with Cursor