Skip to content

Commit 9602e91

Browse files
authored
Deprecate and disable budget optimization method & Updating notebook (#1832)
* Deprecate and disable budget optimization method Marked the budget optimization method as deprecated and replaced its implementation with a NotImplementedError. Users are advised to migrate to the `Multidimensal.MMM` class. * Using deprecation numpy style * Adjusting notebook * Solving test issues * Update multidimensional_model.nc size optimization * Updating notebooks * Notebook update * Update model nc * Changing to raise warning * Revert docs/source/notebooks/mmm/multidimensional_model.nc to version from main * Following Juan idea
1 parent 20c2ca0 commit 9602e91

10 files changed

+2614
-1704
lines changed

data/config_files/multi_dimensional_example_model.yml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,11 @@ effects:
101101
# ----------------------------------------------------------------------
102102
# (optional) sampler options you plan to forward to pm.sample():
103103
sampler_config:
104-
tune: 1000
105-
draws: 200
106-
chains: 8
104+
tune: 800
105+
draws: 400
106+
chains: 2
107107
random_seed: 42
108-
target_accept: 0.90
109-
nuts_sampler: "nutpie"
108+
target_accept: 0.80
110109

111110
# ----------------------------------------------------------------------
112111
# (optional) idata from a previous sample

data/multidimensional_mock_data.csv

Lines changed: 359 additions & 0 deletions
Large diffs are not rendered by default.

docs/source/notebooks/mmm/mmm_allocation_assessment.ipynb

Lines changed: 657 additions & 340 deletions
Large diffs are not rendered by default.

docs/source/notebooks/mmm/mmm_budget_allocation_example.ipynb

Lines changed: 309 additions & 248 deletions
Large diffs are not rendered by default.

docs/source/notebooks/mmm/mmm_multidimensional_example.ipynb

Lines changed: 1173 additions & 1083 deletions
Large diffs are not rendered by default.

pymc_marketing/mmm/mmm.py

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2647,19 +2647,21 @@ def optimize_budget(
26472647
):
26482648
"""Optimize the given budget based on the specified utility function over a specified time period.
26492649
2650-
This function optimizes the allocation of a given budget across different channels
2651-
to maximize the response, considering adstock and saturation effects. It scales the
2652-
budget and budget bounds, performs the optimization, and generates a synthetic dataset
2653-
for posterior predictive sampling.
2654-
2655-
The function first scales the budget and budget bounds using the maximum scale
2656-
of the channel transformer. It then uses the `BudgetOptimizer` to allocate the
2657-
budget, and creates a synthetic dataset based on the optimal allocation. Finally,
2658-
it performs posterior predictive sampling on the synthetic dataset.
2659-
2660-
**Important**: When generating the posterior predicive distribution for the target with the optimized budget,
2661-
we are setting the control variables to zero! This is done because in many situations we do not have all the
2662-
control variables in the future (e.g. outlier control, special events).
2650+
.. deprecated:: 0.0.3
2651+
This function optimizes the allocation of a given budget across different channels
2652+
to maximize the response, considering adstock and saturation effects. It scales the
2653+
budget and budget bounds, performs the optimization, and generates a synthetic dataset
2654+
for posterior predictive sampling.
2655+
2656+
The function first scales the budget and budget bounds using the maximum scale
2657+
of the channel transformer. It then uses the `BudgetOptimizer` to allocate the
2658+
budget, and creates a synthetic dataset based on the optimal allocation. Finally,
2659+
it performs posterior predictive sampling on the synthetic dataset.
2660+
2661+
**Important**: When generating the posterior predicive distribution for the target with the
2662+
optimized budget, we are setting the control variables to zero! This is done because in many
2663+
situations we do not have all the control variables in the future (e.g. outlier control,
2664+
special events).
26632665
26642666
Parameters
26652667
----------
@@ -2699,6 +2701,13 @@ def optimize_budget(
26992701
ValueError
27002702
If the noise level is not a float.
27012703
"""
2704+
warnings.warn(
2705+
"This method is deprecated and will be removed in a future version. "
2706+
"Please migrate to the `Multidimensal.MMM` class.",
2707+
DeprecationWarning,
2708+
stacklevel=2,
2709+
)
2710+
27022711
from pymc_marketing.mmm.budget_optimizer import BudgetOptimizer
27032712

27042713
allocator = BudgetOptimizer(

pymc_marketing/mmm/utility.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ def mean_tightness_score(
169169
It is calculated as:
170170
171171
.. math::
172-
Mean\ Tightness\ Score = \mu - \alpha \cdot Tail\ Distance
172+
Mean\ Tightness\ Score = \mu - \alpha \cdot Tail\ Distance / \mu
173173
174174
where:
175175
- :math:`\mu` is the mean of the sample returns.
@@ -202,7 +202,7 @@ def _mean_tightness_score(
202202
samples = _check_samples_dimensionality(samples)
203203
mean = pt.mean(samples)
204204
tail_metric = tail_distance(confidence_level)
205-
return mean - alpha * tail_metric(samples, budgets)
205+
return (mean - alpha * tail_metric(samples, budgets)) / mean
206206

207207
return _mean_tightness_score
208208

tests/mmm/test_budget_optimizer.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,7 @@ def test_callback_functionality_parametrized(
576576
],
577577
)
578578
def test_mmm_optimize_budget_callback_parametrized(dummy_df, dummy_idata, callback):
579-
"""Test callback functionality through MMM.optimize_budget interface."""
579+
"""Test that MMM.optimize_budget properly raises deprecation error."""
580580
df_kwargs, X_dummy, y_dummy = dummy_df
581581

582582
mmm = MMM(
@@ -588,12 +588,15 @@ def test_mmm_optimize_budget_callback_parametrized(dummy_df, dummy_idata, callba
588588
mmm.build_model(X=X_dummy, y=y_dummy)
589589
mmm.idata = dummy_idata
590590

591-
# Test the MMM interface
592-
result = mmm.optimize_budget(
593-
budget=100,
594-
num_periods=10,
595-
callback=callback,
596-
)
591+
with pytest.warns(
592+
DeprecationWarning,
593+
match="This method is deprecated and will be removed in a future version",
594+
):
595+
result = mmm.optimize_budget(
596+
budget=100,
597+
num_periods=10,
598+
callback=callback,
599+
)
597600

598601
# Check return value count
599602
if callback:

tests/mmm/test_budget_optimizer_multidimensional.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,3 +1004,72 @@ def test_budget_distribution_carryover_interaction_issue(dummy_df, fitted_mmm):
10041004
np.abs(channel_1_spend_with_carryover - channel_1_allocation * num_periods)
10051005
< 0.1
10061006
), "With carryover: total spend should still equal allocation * num_periods"
1007+
1008+
1009+
@pytest.mark.parametrize(
1010+
"callback",
1011+
[
1012+
False, # Default no callback
1013+
True, # With callback
1014+
],
1015+
ids=[
1016+
"no_callback",
1017+
"with_callback",
1018+
],
1019+
)
1020+
def test_multidimensional_optimize_budget_callback_parametrized(
1021+
dummy_df, fitted_mmm, callback
1022+
):
1023+
"""Test callback functionality through MultiDimensionalBudgetOptimizerWrapper.optimize_budget interface."""
1024+
df_kwargs, X_dummy, y_dummy = dummy_df
1025+
1026+
optimizable_model = MultiDimensionalBudgetOptimizerWrapper(
1027+
model=fitted_mmm,
1028+
start_date=X_dummy["date_week"].max() + pd.Timedelta(weeks=1),
1029+
end_date=X_dummy["date_week"].max() + pd.Timedelta(weeks=10),
1030+
)
1031+
1032+
# Test the MultiDimensionalBudgetOptimizerWrapper interface
1033+
result = optimizable_model.optimize_budget(
1034+
budget=100,
1035+
callback=callback,
1036+
)
1037+
1038+
# Check return value count
1039+
if callback:
1040+
assert len(result) == 3
1041+
optimal_budgets, opt_result, callback_info = result
1042+
1043+
# Validate callback info
1044+
assert isinstance(callback_info, list)
1045+
assert len(callback_info) > 0
1046+
1047+
# Each iteration should have required keys
1048+
for iter_info in callback_info:
1049+
assert "x" in iter_info
1050+
assert "fun" in iter_info
1051+
assert "jac" in iter_info
1052+
1053+
# Check that objective values are finite
1054+
objectives = [iter_info["fun"] for iter_info in callback_info]
1055+
assert all(np.isfinite(obj) for obj in objectives)
1056+
1057+
else:
1058+
assert len(result) == 2
1059+
optimal_budgets, opt_result = result
1060+
1061+
# Common validations
1062+
assert isinstance(optimal_budgets, xr.DataArray)
1063+
assert optimal_budgets.dims == (
1064+
"geo",
1065+
"channel",
1066+
) # Multidimensional has geo dimension
1067+
assert len(optimal_budgets.coords["channel"]) == len(fitted_mmm.channel_columns)
1068+
1069+
# Budget should sum to total (within tolerance)
1070+
assert np.abs(optimal_budgets.sum().item() - 100) < 1e-6
1071+
1072+
# Check optimization result
1073+
assert hasattr(opt_result, "success")
1074+
assert hasattr(opt_result, "x")
1075+
assert hasattr(opt_result, "fun")

tests/mmm/test_utility.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
EXPECTED_RESULTS = {
3838
"avg_response": 5.5,
3939
"tail_dist": 4.5,
40-
"mean_tight_score": 3.25,
40+
"mean_tight_score": 0.591,
4141
"var_95": 1.45,
4242
"cvar_95": 1.0,
4343
"sharpe": 1.81327,
@@ -196,7 +196,7 @@ def test_tail_distance(mean1, std1, mean2, std2, expected_order):
196196
60,
197197
0.1,
198198
"higher_mean",
199-
), # With low alpha, higher mean should dominate
199+
), # With low alpha, lower std still dominates due to normalization
200200
],
201201
)
202202
def test_compare_mean_tightness_score(
@@ -215,14 +215,17 @@ def test_compare_mean_tightness_score(
215215
score1 = mean_tightness_score_func(samples1, None).eval()
216216
score2 = mean_tightness_score_func(samples2, None).eval()
217217

218-
# Assertions based on observed behavior: higher mean should dominate in both cases
218+
# Assertions based on actual behavior of the normalized formula
219+
# With the normalized mean tightness score, lower std tends to dominate
220+
# because the score gets closer to 1 with less tail distance
219221
if expected_relation == "higher_mean":
220-
assert score2 > score1, (
221-
f"Expected score for mean={mean2} to be higher, but got {score2} <= {score1}"
222+
# Even with low alpha, lower std distribution scores higher due to normalization
223+
assert score1 > score2, (
224+
f"Expected score for std={std1} to be higher due to normalization, but got {score1} <= {score2}"
222225
)
223226
elif expected_relation == "lower_std":
224227
assert score1 > score2, (
225-
f"Expected score for std={std1} to be lower, but got {score1} <= {score2}"
228+
f"Expected score for std={std1} to be higher, but got {score1} <= {score2}"
226229
)
227230

228231

0 commit comments

Comments
 (0)