Release notes#
This page records user-visible changes, with an emphasis on anything that can break existing code. The auto-generated, commit-level history for each tagged release lives on the GitHub releases page; this page is the curated, human-written companion to it.
1.0.0 (unreleased) — PyMC 6 migration#
This is a major, breaking release. It migrates CausalPy off the PyMC 5
stack and onto PyMC 6 / PyTensor 3 / ArviZ 1.x (compatible with
pymc-marketing v1.0.0). Read the breaking-change section below before
upgrading; several public signatures and default behaviours changed.
Supported versions and dependencies#
Python
>=3.12(raised from 3.11). The dependency stack currently resolves on Python 3.12–3.14 (PyTensor caps Python<3.15). CI exercises 3.12 and 3.14.pymc
>=6.0.1,<7(previously the PyMC 5 series). The exact patch the resolver selects depends on what else is installed — for examplepymc-marketingv1.0.0 capspymc<6.1, so environments that include it resolve to an older 6.x. Do not rely on a specific patch version.pytensor
>=3,<4.arviz
>=1.1,<2— the ArviZ 0.x → 1.x jump.arviz.InferenceDatais no longer available as a usable class — accessing it emits aMigrationWarning("arviz.InferenceData is no longer available on the arviz package") — and ArviZ now uses xarray’sDataTreefor the same role.pandas
>=2.3,<4— pandas 2.3 through the 3.x line are supported and are exercised as separate CI legs.pymc-extras
>=0.3.0.numba is now effectively a hard (transitive) dependency. It is PyTensor 3’s default compilation backend and is pulled in through the PyTensor/PyMC dependency chain. See Runtime characteristics.
These floors are declared in pyproject.toml.
Breaking changes#
Inference results are now xarray.DataTree, not arviz.InferenceData#
Under ArviZ 1.x, pm.sample() — and therefore the .idata attribute on
CausalPy’s PyMC models and experiments — is an xarray.DataTree rather than an
arviz.InferenceData. CausalPy’s own type annotations reflect this (for
example causalpy/pymc_forecast_models.py annotates self.idata: xr.DataTree | None).
If you wrote code against the old InferenceData object you may need to update
it. In particular:
InferenceData.extend()no longer exists; groups are combined withDataTreeoperations instead.Constructing results via
az.InferenceData(group=ds)no longer works.Accessing a group returns a
DataTreenode, which does not expose the group-level.assign_coords/.renameshortcuts thatInferenceDatagroups had.
These are ArviZ/PyMC-level changes; they surface through any CausalPy object
that exposes an idata.
Public method signatures were narrowed (no more silent **kwargs)#
Public methods across the experiment classes had their *args / **kwargs
catch-alls removed so that the supported keyword arguments are explicit
(PR #1107). A call that passes a keyword the method does not declare now raises
TypeError, where previously the argument was silently accepted and ignored.
This narrowing is enforced in CI (scripts/audit_public_signatures.py and
causalpy/tests/test_public_signatures.py). The two most user-visible
consequences are get_plot_data and effect_summary, below.
get_plot_data is now keyword-only, and absent on several experiments#
get_plot_data used to be defined generically on BaseExperiment (as
get_plot_data(self, *args, **kwargs), which raised NotImplementedError for
experiments that did not override it). That generic method was removed. As a
result:
On the four experiments that implement it with an
hdi_probargument,get_plot_datais now keyword-only:InterruptedTimeSeries(causalpy/experiments/interrupted_time_series.py)PiecewiseITS(causalpy/experiments/piecewise_its.py)StaggeredDifferenceInDifferences(causalpy/experiments/staggered_did.py)SyntheticControl(causalpy/experiments/synthetic_control.py)
These previously accepted
hdi_probpositionally, so a call such asexperiment.get_plot_data(0.9)now raisesTypeError. Use the keyword form,experiment.get_plot_data(hdi_prob=0.9).On
PanelRegression(causalpy/experiments/panel_regression.py),get_plot_datais now argument-less. It previously accepted**kwargs(which were ignored), so passing any argument now raisesTypeError. A bareexperiment.get_plot_data()call still works.Because there is no longer a
BaseExperiment.get_plot_data, callingget_plot_dataon an experiment that does not define it now raisesAttributeError(previously it raisedNotImplementedError). The seven experiments in this situation are:DifferenceInDifferencesInstrumentalVariableInversePropensityWeightingPrePostNEGDRegressionKinkRegressionDiscontinuitySyntheticDifferenceInDifferences
(This split — five experiments implement
get_plot_data, seven do not — is pinned bycausalpy/tests/test_public_signatures.py.)
effect_summary(alpha=...) raises TypeError instead of NotImplementedError#
On the three experiments that do not implement a unified effect summary —
PanelRegression (causalpy/experiments/panel_regression.py),
InstrumentalVariable (causalpy/experiments/instrumental_variable.py), and
InversePropensityWeighting
(causalpy/experiments/inverse_propensity_weighting.py) — effect_summary is
now defined as def effect_summary(self) -> NoReturn with no parameters. A
bare experiment.effect_summary() call still raises NotImplementedError as
before. But because the method no longer declares an alpha (or any other)
parameter, a call such as experiment.effect_summary(alpha=0.05) now raises
TypeError before the body runs, whereas previously the same call reached
the body and raised NotImplementedError. The operation remains unsupported on
these three experiments; only the exception type for the alpha= form changed.
SyntheticControl default prior changed#
SyntheticControl now derives its observation-noise sigma prior from each
treated unit’s pre-treatment spread (a data-scaled prior), controlled by the
new auto_scale_sigma argument, which defaults to True
(causalpy/experiments/synthetic_control.py; PR #1106). This changes the
default posterior relative to earlier releases. To restore the previous fixed
HalfNormal(1) prior — for example when reproducing an analysis run against an
older release — pass SyntheticControl(..., auto_scale_sigma=False).
PlaceboInTime abstains when its null cannot be identified#
PlaceboInTime now returns INCONCLUSIVE (passed=None) rather than a SUPPORTED or NOT-SUPPORTED verdict when fewer than MIN_USABLE_FOLDS (2) usable placebo folds complete or the completed folds have a non-positive/non-finite between-fold spread. Because it does not build a hierarchical null in those cases, the result metadata has no null_samples or p_effect_outside_null. The guard catches only exact degeneracy: a tiny-but-positive between-fold spread on a large-scale series remains eligible for a verdict. Choosing a relative threshold is an unresolved modelling decision.
InstrumentalVariable samples with cores=1#
The instrumental-variable model (an MvNormal with an LKJCholeskyCov prior)
can crash under multiprocess (“fork”) sampling on an affected platform/version
combination (observed on macOS arm64 with Python 3.14 and PyMC 6.0.1–6.2.0) —
the worker dies natively rather than raising a Python exception. To keep the
model usable, CausalPy forces cores=1 for it when cores is not otherwise
specified (causalpy/pymc_models.py:1330-1332). IV sampling therefore does not
parallelise across cores by default. This is a temporary mitigation: the
upstream bug is tracked at
pymc-devs/pymc#8377, and
removal of the workaround is tracked in CausalPy issue #1067.
Behaviour that intentionally did not change#
The ArviZ default-interval change is a no-op for CausalPy#
ArviZ 1.x changed its own default summary interval from a 0.94 highest-density
interval (HDI) to a 0.89 equal-tailed interval (ETI). This does not change
any CausalPy output. CausalPy always passes an explicit interval probability
when it calls ArviZ — its HDI helper calls az.hdi(..., prob=HDI_PROB), with
the project-wide HDI_PROB = 0.94 defined in causalpy/constants.py — so
nothing falls through to ArviZ’s new default. Interval widths in effect
summaries, plots, and get_plot_data are identical to the previous release.
(This is separate from the fact that different reports use different nominal
levels — some effect summaries report a 95% interval, and
PanelRegression.get_plot_data reports 95% quantile bounds — none of which
changed in this release.)
Runtime characteristics#
See also the runtime notes in the README.
numba first-compile latency. Because numba is PyTensor 3’s default backend, the first sample from a freshly defined model incurs a noticeable compilation delay before sampling begins.
Native worker crashes replace Python tracebacks. When a numba-compiled sampler worker fails, the failure can present as a native crash (or a stalled worker) rather than a Python traceback. A wedged worker can therefore look like a silent hang.
nutpie changes sampling defaults if installed. If a recent enough
nutpieis installed, PyMC selects it as the default NUTS sampler unless you pass an explicitnuts_sampler— so installing nutpie is itself enough to change howpm.sample()behaves. nutpie also applies its own tuning-step default rather than PyMC’s built-in NUTS default of 1000 tune steps (withtune=None, PyMC forwards the request straight to nutpie rather than resolving it to 1000). The migration investigation reported nutpie’s default as 400 tune steps; that exact number was not re-verified for this release (nutpie is optional and was not installed in the release environment), so treat 400 as indicative and confirm against your installed nutpie version if it matters for your run.
Version#
This release is 1.0.0: the migration is backwards-incompatible, so the major version increments. The version is single-sourced from causalpy/version.py, and pyproject.toml reads it dynamically, so causalpy.__version__ and the installed distribution metadata cannot drift apart.