refactor(bass): migrate dim handling to pymc.dims - #2771
Conversation
BassModel.load left model_config as plain dicts after the JSON round-trip, so build_model crashed on any loaded model. Parse the config in __init__ like CLV does, and register Scaled in the TypeRegistry so the scaled market-potential prior round-trips too.
_data_setter filled y_obs with 1-D zeros, so out-of-sample prediction failed for multi-product models.
Rebuilt on the BassModel workflow and re-executed on the v1 stack (pymc 6, pytensor 3, arviz 1.2, arviz-plots 1.2). Migrates the arviz plotting calls: plot_trace -> azp.plot_trace_dist, plot_forest and plot_posterior -> arviz_plots PlotCollection idioms, az.hdi ci_bound coords, and the sample_posterior_predictive DataArray return. Drops the trailing empty cell so the watermark is last.
- plot_peak: pass col_wrap=3 so multi-product peaks wrap into a grid instead of one squished row (williambdean) - re-executed with ipywidgets installed, so the sampler progress bars render as widgets and the 'install ipywidgets' warning is gone; matches the other v1 re-run notebooks (williambdean) plot_decomposition legend now sits outside the axes via the fix in the stacked plotting PR.
|
Check out this pull request on See visual diffs & provide feedback on Jupyter Notebooks. Powered by ReviewNB |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2771 +/- ##
==========================================
+ Coverage 94.81% 94.83% +0.01%
==========================================
Files 111 111
Lines 17623 17665 +42
==========================================
+ Hits 16710 16752 +42
Misses 913 913 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
- p prior mu=0.02 so constrain(0.01, 0.03) is satisfiable, clearing the preliz mass warning and the leaked local path - draw observed cumulative as reference lines since plot_dist has no ref_val argument - forecast window np.arange(len(T) + 26), 26 points past the last observation - drop the %autoreload dev magics; save/load and mlflow write to temp dirs - re-executed top to bottom on the v1 stack (pymc 6.0.1, arviz 1.2)
a4a743b to
79fa653
Compare
- _data_setter builds the no-observed placeholder from y_obs's declared dims instead of assuming T is axis 0 - to_bass_dataset transposes an xr.Dataset so T leads, so a (product, T) dataset fits and forecasts - tests: transposed-dataset forecast, and assert model_config equality on the save/load round-trip
- sample_prior_bass_data returns the full draw so the true m, p, q are kept - the p/q forests and the m plot mark the true per-product values instead of prior means - show the restored model_config on load; note plot_peak's col_wrap/figure_kwargs API - re-executed on the v1 stack
Replace create_dim_handler (pymc_extras.prior) with pymc.dims, closing the last major create_dim_handler usage outside the Prior class (pymc-labs#2598). - m/p/q created with xdist=True; deterministics via pmd.Deterministic - inline xtensor _f_xt/_F_xt for the model graph; public F/f stay on PyTensor for the float/NumPy API and tests - keep t and y_obs as pm.Data wrapped with pmd.as_xtensor so the data-setter and out-of-sample forecasting keep working - transpose deterministics back to the historical (T, ...) dim order - DiracDelta is not yet in pymc.dims: fall back to a regular variable wrapped with pmd.as_xtensor for that distribution only Verified: 78 bass tests, autolog_bass, machine-precision adopters/peak vs the closed-form formula, and the bass notebook mock runner.
- consolidate F/f: a single _exp helper dispatches pmd.math.exp for xtensor and pt.exp otherwise, so F/f serve both the float/NumPy API and the model graph; drop the duplicate _f_xt/_F_xt (williambdean) - drop the _ordered transpose; let pymc.dims order the deterministic dims, consumers select by name (williambdean)
79fa653 to
3f639e1
Compare
|
Updating the base branch. Should work now with pymc version |
BassModel.load left model_config as plain dicts after the JSON round-trip, so build_model crashed on any loaded model. Parse the config in __init__ like CLV does, and register Scaled in the TypeRegistry so the scaled market-potential prior round-trips too.
_data_setter filled y_obs with 1-D zeros, so out-of-sample prediction failed for multi-product models.
Rebuilt on the BassModel workflow and re-executed on the v1 stack (pymc 6, pytensor 3, arviz 1.2, arviz-plots 1.2). Migrates the arviz plotting calls: plot_trace -> azp.plot_trace_dist, plot_forest and plot_posterior -> arviz_plots PlotCollection idioms, az.hdi ci_bound coords, and the sample_posterior_predictive DataArray return. Drops the trailing empty cell so the watermark is last.
- plot_peak: pass col_wrap=3 so multi-product peaks wrap into a grid instead of one squished row (williambdean) - re-executed with ipywidgets installed, so the sampler progress bars render as widgets and the 'install ipywidgets' warning is gone; matches the other v1 re-run notebooks (williambdean) plot_decomposition legend now sits outside the axes via the fix in the stacked plotting PR.
- p prior mu=0.02 so constrain(0.01, 0.03) is satisfiable, clearing the preliz mass warning and the leaked local path - draw observed cumulative as reference lines since plot_dist has no ref_val argument - forecast window np.arange(len(T) + 26), 26 points past the last observation - drop the %autoreload dev magics; save/load and mlflow write to temp dirs - re-executed top to bottom on the v1 stack (pymc 6.0.1, arviz 1.2)
- _data_setter builds the no-observed placeholder from y_obs's declared dims instead of assuming T is axis 0 - to_bass_dataset transposes an xr.Dataset so T leads, so a (product, T) dataset fits and forecasts - tests: transposed-dataset forecast, and assert model_config equality on the save/load round-trip
- sample_prior_bass_data returns the full draw so the true m, p, q are kept - the p/q forests and the m plot mark the true per-product values instead of prior means - show the restored model_config on load; note plot_peak's col_wrap/figure_kwargs API - re-executed on the v1 stack
|
Looking at the CI, the oldest-deps shard installs pymc==6.0.0 which lacks |
The pymc.dims migration uses pmd.Poisson/NegativeBinomial, which landed in pymc 6.0.1, so the oldest-deps CI shard must resolve 6.0.1 instead of 6.0.0.
- test_save_load_round_trip_scaled_priors: p prior mu=0.02, drops the preliz mass warning (3 of the suite's 4) - _from_xarray transposes before adding the T coord, so a (product, T) Dataset with no T coord gets the right T length; regression test added
# Conflicts: # pyproject.toml # uv.lock
|
Addressed in 1bc024c:
9f7af31 merges |
|
Checked 1bc024c and 9f7af31. All three notes are closed. The PR description now flags the The merge drops |
Address review findings: - The unobserved path now raises UnsupportedDistributionError for a mu-less likelihood, same as create_likelihood_variable, instead of a raw TypeError. - A dim declared on a prior but missing from the model coords raises a clear ValueError instead of a bare KeyError out of dim_lengths. - Docstring fixes: name the real positional labelling order (p, q, m, likelihood), drop the stale DiracDelta example, drop the dead hasattr(pmd, "Censored") check (always true at the pinned floor).
| # Censored keeps its parameters on the wrapped distribution. | ||
| inner = prior.distribution if isinstance(prior, Censored) else prior | ||
| if "mu" not in signature(inner.pymc_distribution.dist).parameters: | ||
| raise UnsupportedDistributionError( |
There was a problem hiding this comment.
create_likelihood_variable is only called with data: pymc-extras refuses observed=None there since pymc-devs/pymc-extras#732. The unobserved node goes through create_variable with mu attached instead, so the two guards that method applies (UnsupportedDistributionError for a mu-less distribution, MuAlreadyExistsError) are mirrored here to keep both paths raising the same typed errors. Covered by test_mu_less_likelihood_without_observed_raises and test_unobserved_skips_create_likelihood_variable.
…d dims - _supports_xdist now recurses into nested priors, so a distribution missing from pymc.dims anywhere in the tree takes the fallback path instead of raising UnsupportedDistributionError. - An observed dim the model does not know raises a clear ValueError instead of a KeyError from inside pymc.dims.
| def _get_random_variable_name(rv) -> str: | ||
| # Taken from new version of pymc/model_graph.py | ||
| symbol = rv.owner.op.__class__.__name__ | ||
| op = rv.owner.op |
There was a problem hiding this comment.
Removed the # Taken from new version of pymc/model_graph.py line here: it dated to a900f97 (2024) and pymc's random_variable_symbol has since diverged (it now prefers op.name with a class-name fallback, and has no core_op unwrap), so the reference no longer described this helper. The core_op line is local to this PR.
|
Thanks for all of the iteraetion on this, @anevolbap! I think we are over complicating a bit of this. We can leverage xtensor and the checks that come from pymc, xtensor, pymc_extras, etc. Here are some items: All 6 helper functions are redundant with existing APIs.
The MMM codebase uses
~6 tests in
|
|
Thanks for the detailed pass, it made me run every item against the released stack (pymc 6.2.0, pymc-extras 0.14.0). Good news first: the observed path does collapse to
So the full cleanup means narrowing the contract to pymc.dims-supported distributions and xdist-aware factories, same as MMM. Is that the intent? If yes, the fallback helpers and the ~6 tests go and Wald-style priors stop working; happy to push that version. |
|
We don't need to support everything. We want to leverage xtensor mainly like with the MMM implementation. The model block should be lean. |
|
Pushed the lean version (42039a8): Two things stayed, with the reason:
One tradeoff to flag: without |
|
Why the _exp 🥲 Just update the fixtures 😅 |
|
As Claude usually says, "You're absolutely right!" — On |
|
Review notes. Two of these I'd want closed before merge, two are cheap and belong in this PR rather than a follow-up. Everything else I found is listed at the bottom as non-blocking. 1.
|
Closes #2598
Migrates the Bass model's dimension handling from
create_dim_handler(pymc_extras.prior) topymc.dims, for consistency with the rest of the codebase.Reopens #2724, which GitHub auto-closed when the
v1.0.0branch was deleted and merged intomain. Same work, retargeted tomain.In
create_bass_model:m,p,qand the likelihood viacreate_variable(xdist=True); deterministics viapmd.Deterministic.Fandftake xtensor inputs, so there is no second copy of the formulas for the model graph.tandy_obsstaypm.Datawrapped withpmd.as_xtensor, so the data-setter and out-of-sample forecasting keep working.(T, ...). They are not broadcast up to the likelihood's dims: pooled parameters give pooled curves, and a per-product curve is the user's to build outside the model.mainthe deterministics carried the union of thep,qandmdims.create_variableandmuset, keeping theUnsupportedDistributionErrorandMuAlreadyExistsErrorguards, since upstream refusesobserved=None(Prior.create_likelihood_variable(observed=None) fails on the xdist path pymc-devs/pymc-extras#731).pymc.dimsimplements, and a customVariableFactorymust take thexdistkeyword.DiracDeltais native inpymc.dimson the pymc>=6.2 floor, so #2726 has nothing left to remove and can be closed.Verified: full bass test suite, mlflow autolog,
adopters/peakmatching the closed-form formula to machine precision, and the bass notebook mock runner.📚 Documentation preview 📚: https://pymc-marketing--2771.org.readthedocs.build/en/2771/