Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MMM Case Study from PyData Global #1044

Merged
merged 25 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/source/notebooks/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ mmm/mmm_time_varying_media_example
mmm/mmm_components
mmm/mmm_roas
mmm/mmm_time_slice_cross_validation
mmm/mmm_case_study
:::

:::{toctree}
Expand Down
7,174 changes: 7,174 additions & 0 deletions docs/source/notebooks/mmm/mmm_case_study.ipynb

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions pymc_marketing/mmm/mmm.py
Original file line number Diff line number Diff line change
Expand Up @@ -2037,6 +2037,10 @@ def _create_synth_dataset(
) -> pd.DataFrame:
"""Create a synthetic dataset based on the given allocation strategy (Budget) and time granularity.

**Important**: When generating the posterior predicive distribution for the target with the optimized budget,
we are setting the control variables to zero! This is done because in many situations we do not have all the
control variables in the future (e.g. outlier control, special events).

Parameters
----------
df : pd.DataFrame
Expand Down Expand Up @@ -2131,6 +2135,7 @@ def allocate_budget_to_maximize_response(
custom_constraints: dict[str, float] | None = None,
quantile: float = 0.5,
noise_level: float = 0.01,
**minimize_kwargs,
) -> az.InferenceData:
"""Allocate the given budget to maximize the response over a specified time period.

Expand All @@ -2144,6 +2149,10 @@ def allocate_budget_to_maximize_response(
budget, and creates a synthetic dataset based on the optimal allocation. Finally,
it performs posterior predictive sampling on the synthetic dataset.

**Important**: When generating the posterior predicive distribution for the target with the optimized budget,
we are setting the control variables to zero! This is done because in many situations we do not have all the
control variables in the future (e.g. outlier control, special events).

Parameters
----------
budget : float or int
Expand All @@ -2159,6 +2168,10 @@ def allocate_budget_to_maximize_response(
Custom constraints for the optimization. If None, no custom constraints are applied.
quantile : float, optional
The quantile to use for recovering transformation parameters. Default is 0.5.
noise_level : float, optional
The level of noise added to the allocation strategy (by default 1%).
**minimize_kwargs
Additional arguments to pass to the `BudgetOptimizer`.

Returns
-------
Expand All @@ -2170,7 +2183,12 @@ def allocate_budget_to_maximize_response(
ValueError
If the time granularity is not supported.

ValueError
If the noise level is not a float.
"""
if not isinstance(noise_level, float):
raise ValueError("noise_level must be a float")

parameters_mid = self.format_recovered_transformation_parameters(
quantile=quantile
)
Expand All @@ -2188,6 +2206,7 @@ def allocate_budget_to_maximize_response(
total_budget=budget,
budget_bounds=budget_bounds,
custom_constraints=custom_constraints,
**minimize_kwargs,
)

synth_dataset = self._create_synth_dataset(
Expand Down
18 changes: 18 additions & 0 deletions tests/mmm/test_mmm.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,24 @@ def test_allocate_budget_to_maximize_response(self, mmm_fitted: MMM) -> None:
inference_periods == num_periods
), f"Number of periods in the data {inference_periods} does not match the expected {num_periods}"

def test_allocate_budget_to_maximize_response_bad_noise_level(
self, mmm_fitted: MMM
) -> None:
budget = 2.0
num_periods = 8
time_granularity = "weekly"
budget_bounds = {"channel_1": [0.5, 1.2], "channel_2": [0.5, 1.5]}
noise_level = "bad_noise_level"

with pytest.raises(ValueError, match="noise_level must be a float"):
mmm_fitted.allocate_budget_to_maximize_response(
budget=budget,
time_granularity=time_granularity,
num_periods=num_periods,
budget_bounds=budget_bounds,
noise_level=noise_level,
)

@pytest.mark.parametrize(
argnames="original_scale",
argvalues=[False, True],
Expand Down
Loading