Skip to content

feat(mmm): add ControlEffect MuEffect to handle control variables via protocol#2568

Draft
williambdean wants to merge 2 commits into
mainfrom
feature/control-effect
Draft

feat(mmm): add ControlEffect MuEffect to handle control variables via protocol#2568
williambdean wants to merge 2 commits into
mainfrom
feature/control-effect

Conversation

@williambdean
Copy link
Copy Markdown
Contributor

@williambdean williambdean commented May 14, 2026

Context

This is exploratory work on how to allow MuEffects to consume additional external data (e.g. control columns from the training X DataFrame). Previously, control-variable logic lived as inline code inside build_model and _set_xarray_data. This PR routes it through the MuEffect protocol instead, using ControlEffect as the first concrete example.

The key design question being explored: how should MuEffects that need external data (beyond model coordinates) receive it? The solution here is to pass an optional X: xr.Dataset | None to create_data, which effects that don't need external data simply ignore.

Changes

MuEffect ABC

  • create_data(mmm, X=None) — optional X so effects that need external data can consume it; existing effects ignore it safely
  • set_data(mmm, model, X: xr.Dataset | None) — type annotation corrected

ControlEffect(MuEffect)

  • Registers control_data as a pmd.Data shared variable
  • Creates gamma_control from a configurable Prior
  • Returns control_contribution Deterministic with "control" dim summed out
  • field_validator mirrors Transformation.with_default_prior_dims: dims=None defaults to "control"; explicit dims are untouched
  • Fully serializable; registered in the serialization registry

MMM

  • __init__: auto-inserts ControlEffect(prior=model_config["gamma_control"]) at position 0 of mu_effects when control_columns are present
  • build_model: inline control block removed; delegated to mu_effects
  • _set_xarray_data: hardcoded control pm.set_data block removed
  • build_from_idata: backward-compat guard re-injects ControlEffect for models saved before this change

What's not in scope

  • Other MuEffects receiving X (only ControlEffect uses it today)
  • Public API changes (control_columns param unchanged)
  • Panel/multidimensional dims are handled transparently via mmm.dims

Testing

  • 17 TestControlEffect unit tests covering create/effect/set_data, idempotency, serialization round-trip, prior validator, and scalar + panel-geo multidimensional cases
  • All existing tests pass

📚 Documentation preview 📚: https://pymc-marketing--2568.org.readthedocs.build/en/2568/

… protocol

Introduces ControlEffect, a new MuEffect subclass that routes control-variable
logic through the MuEffect protocol (create_data / create_effect / set_data)
instead of inline build_model code.

- MuEffect ABC: create_data now accepts optional X: xr.Dataset | None so
  effects that need external data (e.g. control columns) can consume it while
  existing effects safely ignore it
- ControlEffect: registers control_data as a pmd.Data shared variable,
  creates gamma_control from a configurable Prior, and exposes a
  control_contribution Deterministic; supports panel dims transparently
- field_validator mirrors Transformation.with_default_prior_dims: dims=None
  on the supplied Prior is defaulted to 'control'; explicit dims are untouched
- MMM.__init__: auto-inserts ControlEffect(prior=model_config['gamma_control'])
  at position 0 of mu_effects when control_columns are present
- MMM.build_model: removed inline control block; now delegates to mu_effects
- MMM._set_xarray_data: removed hardcoded control pm.set_data block
- MMM.build_from_idata: backward-compat guard re-injects ControlEffect for
  models saved before this change
- Serialization: ControlEffect registered in the serialization registry;
  save/load round-trip preserves prior and date_dim_name
Covers the full ControlEffect surface:
- create_data registers shared variable and coord; is idempotent
- create_data is a no-op when X is None or lacks '_control' key
- create_effect produces a 'control_contribution' Deterministic with
  'control' summed out; raises RuntimeError if create_data was skipped
- set_data updates shared variable; is a no-op for None or missing key
- to_dict / from_dict round-trip preserves prior and date_dim_name
- field_validator: Prior(dims=None) defaults to dims='control';
  explicitly supplied dims are left untouched
- Parametrized scalar and panel-geo multidimensional cases
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant