google-meridian 1.0.3__py3-none-any.whl → 1.0.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {google_meridian-1.0.3.dist-info → google_meridian-1.0.5.dist-info}/METADATA +26 -21
- {google_meridian-1.0.3.dist-info → google_meridian-1.0.5.dist-info}/RECORD +20 -16
- {google_meridian-1.0.3.dist-info → google_meridian-1.0.5.dist-info}/WHEEL +1 -1
- meridian/__init__.py +1 -1
- meridian/analysis/analyzer.py +347 -512
- meridian/analysis/formatter.py +18 -0
- meridian/analysis/optimizer.py +259 -145
- meridian/analysis/summarizer.py +2 -2
- meridian/analysis/visualizer.py +21 -2
- meridian/data/__init__.py +1 -0
- meridian/data/arg_builder.py +107 -0
- meridian/data/input_data.py +23 -0
- meridian/data/test_utils.py +6 -4
- meridian/model/__init__.py +2 -0
- meridian/model/model.py +42 -984
- meridian/model/model_test_data.py +351 -0
- meridian/model/posterior_sampler.py +566 -0
- meridian/model/prior_sampler.py +633 -0
- {google_meridian-1.0.3.dist-info → google_meridian-1.0.5.dist-info}/LICENSE +0 -0
- {google_meridian-1.0.3.dist-info → google_meridian-1.0.5.dist-info}/top_level.txt +0 -0
meridian/analysis/formatter.py
CHANGED
|
@@ -110,6 +110,24 @@ def bar_chart_width(num_bars: int) -> int:
|
|
|
110
110
|
return (c.BAR_SIZE + c.PADDING_20) * num_bars
|
|
111
111
|
|
|
112
112
|
|
|
113
|
+
def format_percent(percent: float) -> str:
|
|
114
|
+
"""Formats a percentage value into a string format.
|
|
115
|
+
|
|
116
|
+
Percentage values between 0 and 1 are formatted with 1 decimal place.
|
|
117
|
+
Percentage values greater than 1 are formatted with 0 decimal places.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
percent: The percentage value to format.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
A formatted string.
|
|
124
|
+
"""
|
|
125
|
+
if percent >= 0.01:
|
|
126
|
+
return '{:.0%}'.format(percent)
|
|
127
|
+
else:
|
|
128
|
+
return '{:.1g}%'.format(percent * 100)
|
|
129
|
+
|
|
130
|
+
|
|
113
131
|
def compact_number(n: float, precision: int = 0, currency: str = '') -> str:
|
|
114
132
|
"""Formats a number into a compact notation to the specified precision.
|
|
115
133
|
|
meridian/analysis/optimizer.py
CHANGED
|
@@ -45,6 +45,64 @@ alt.data_transformers.disable_max_rows()
|
|
|
45
45
|
_SpendConstraint: TypeAlias = float | Sequence[float]
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
@dataclasses.dataclass(frozen=True)
|
|
49
|
+
class OptimizationGrid:
|
|
50
|
+
"""Optimization grid information.
|
|
51
|
+
|
|
52
|
+
Attributes:
|
|
53
|
+
spend: ndarray of shape `(n_paid_channels,)` containing the spend allocation
|
|
54
|
+
for spend for all media and RF channels. The order matches
|
|
55
|
+
`InputData.get_all_paid_channels`.
|
|
56
|
+
use_kpi: Whether using generic KPI or revenue.
|
|
57
|
+
use_posterior: Whether posterior distributions were used, or prior.
|
|
58
|
+
use_optimal_frequency: Whether optimal frequency was used.
|
|
59
|
+
round_factor: The round factor used for the optimization grid.
|
|
60
|
+
optimal_frequency: Optional ndarray of shape `(n_paid_channels,)`,
|
|
61
|
+
containing the optimal frequency per channel. Value is `None` if the model
|
|
62
|
+
does not contain reach and frequency data, or if the model does contain
|
|
63
|
+
reach and frequency data, but historical frequency is used for the
|
|
64
|
+
optimization scenario.
|
|
65
|
+
selected_times: The time coordinates from the model used in this grid.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
_grid_dataset: xr.Dataset
|
|
69
|
+
|
|
70
|
+
spend: np.ndarray
|
|
71
|
+
use_kpi: bool
|
|
72
|
+
use_posterior: bool
|
|
73
|
+
use_optimal_frequency: bool
|
|
74
|
+
round_factor: int
|
|
75
|
+
optimal_frequency: np.ndarray | None
|
|
76
|
+
selected_times: list[str] | None
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def grid_dataset(self) -> xr.Dataset:
|
|
80
|
+
"""Dataset holding the grid information used for optimization.
|
|
81
|
+
|
|
82
|
+
The dataset contains the following:
|
|
83
|
+
|
|
84
|
+
- Coordinates: `grid_spend_index`, `channel`
|
|
85
|
+
- Data variables: `spend_grid`, `incremental_outcome_grid`
|
|
86
|
+
- Attributes: `spend_step_size`
|
|
87
|
+
"""
|
|
88
|
+
return self._grid_dataset
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def spend_grid(self) -> np.ndarray:
|
|
92
|
+
"""The spend grid."""
|
|
93
|
+
return self.grid_dataset.spend_grid
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def incremental_outcome_grid(self) -> np.ndarray:
|
|
97
|
+
"""The incremental outcome grid."""
|
|
98
|
+
return self.grid_dataset.incremental_outcome_grid
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def spend_step_size(self) -> float:
|
|
102
|
+
"""The spend step size."""
|
|
103
|
+
return self.grid_dataset.attrs[c.SPEND_STEP_SIZE]
|
|
104
|
+
|
|
105
|
+
|
|
48
106
|
@dataclasses.dataclass(frozen=True)
|
|
49
107
|
class OptimizationResults:
|
|
50
108
|
"""The optimized budget allocation.
|
|
@@ -69,10 +127,6 @@ class OptimizationResults:
|
|
|
69
127
|
meridian: The fitted Meridian model that was used to create this budget
|
|
70
128
|
allocation.
|
|
71
129
|
analyzer: The analyzer bound to the model above.
|
|
72
|
-
use_posterior: Whether the posterior distribution was used to optimize the
|
|
73
|
-
budget. If `False`, the prior distribution was used.
|
|
74
|
-
use_optimal_frequency: Whether optimal frequency was used to optimize the
|
|
75
|
-
budget.
|
|
76
130
|
spend_ratio: The spend ratio used to scale the non-optimized budget metrics
|
|
77
131
|
to the optimized budget metrics.
|
|
78
132
|
spend_bounds: The spend bounds used to scale the non-optimized budget
|
|
@@ -88,10 +142,6 @@ class OptimizationResults:
|
|
|
88
142
|
meridian: model.Meridian
|
|
89
143
|
# The analyzer bound to the model above.
|
|
90
144
|
analyzer: analyzer.Analyzer
|
|
91
|
-
|
|
92
|
-
# The intermediate values used to derive the optimized budget allocation.
|
|
93
|
-
use_posterior: bool
|
|
94
|
-
use_optimal_frequency: bool
|
|
95
145
|
spend_ratio: np.ndarray # spend / historical spend
|
|
96
146
|
spend_bounds: tuple[np.ndarray, np.ndarray]
|
|
97
147
|
|
|
@@ -99,7 +149,7 @@ class OptimizationResults:
|
|
|
99
149
|
_nonoptimized_data: xr.Dataset
|
|
100
150
|
_nonoptimized_data_with_optimal_freq: xr.Dataset
|
|
101
151
|
_optimized_data: xr.Dataset
|
|
102
|
-
_optimization_grid:
|
|
152
|
+
_optimization_grid: OptimizationGrid
|
|
103
153
|
|
|
104
154
|
# TODO: Move this, and the plotting methods, to a summarizer.
|
|
105
155
|
@functools.cached_property
|
|
@@ -174,23 +224,15 @@ class OptimizationResults:
|
|
|
174
224
|
return self._optimized_data
|
|
175
225
|
|
|
176
226
|
@property
|
|
177
|
-
def optimization_grid(self) ->
|
|
178
|
-
"""
|
|
179
|
-
|
|
180
|
-
The dataset contains the following:
|
|
181
|
-
|
|
182
|
-
- Coordinates: `grid_spend_index`, `channel`
|
|
183
|
-
- Data variables: `spend_grid`, `incremental_outcome_grid`
|
|
184
|
-
- Attributes: `spend_step_size`
|
|
185
|
-
"""
|
|
227
|
+
def optimization_grid(self) -> OptimizationGrid:
|
|
228
|
+
"""The grid information used for optimization."""
|
|
186
229
|
return self._optimization_grid
|
|
187
230
|
|
|
188
231
|
def output_optimization_summary(self, filename: str, filepath: str):
|
|
189
232
|
"""Generates and saves the HTML optimization summary output."""
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
f.write(self._gen_optimization_summary())
|
|
233
|
+
os.makedirs(filepath, exist_ok=True)
|
|
234
|
+
with open(os.path.join(filepath, filename), 'w') as f:
|
|
235
|
+
f.write(self._gen_optimization_summary())
|
|
194
236
|
|
|
195
237
|
def plot_incremental_outcome_delta(self) -> alt.Chart:
|
|
196
238
|
"""Plots a waterfall chart showing the change in incremental outcome."""
|
|
@@ -540,10 +582,10 @@ class OptimizationResults:
|
|
|
540
582
|
# response curve computation might take a significant amount of time.
|
|
541
583
|
return self.analyzer.response_curves(
|
|
542
584
|
spend_multipliers=spend_multiplier,
|
|
543
|
-
use_posterior=self.use_posterior,
|
|
585
|
+
use_posterior=self.optimization_grid.use_posterior,
|
|
544
586
|
selected_times=selected_times,
|
|
545
587
|
by_reach=True,
|
|
546
|
-
use_optimal_frequency=self.use_optimal_frequency,
|
|
588
|
+
use_optimal_frequency=self.optimization_grid.use_optimal_frequency,
|
|
547
589
|
)
|
|
548
590
|
|
|
549
591
|
def _get_plottable_response_curves_df(
|
|
@@ -675,7 +717,6 @@ class OptimizationResults:
|
|
|
675
717
|
id=summary_text.SCENARIO_PLAN_CARD_ID,
|
|
676
718
|
title=summary_text.SCENARIO_PLAN_CARD_TITLE,
|
|
677
719
|
)
|
|
678
|
-
|
|
679
720
|
scenario_type = (
|
|
680
721
|
summary_text.FIXED_BUDGET_LABEL.lower()
|
|
681
722
|
if self.optimized_data.fixed_budget
|
|
@@ -892,6 +933,14 @@ class BudgetOptimizer:
|
|
|
892
933
|
self._meridian = meridian
|
|
893
934
|
self._analyzer = analyzer.Analyzer(self._meridian)
|
|
894
935
|
|
|
936
|
+
def _validate_model_fit(self, use_posterior: bool):
|
|
937
|
+
"""Validates that the model is fit."""
|
|
938
|
+
dist_type = c.POSTERIOR if use_posterior else c.PRIOR
|
|
939
|
+
if dist_type not in self._meridian.inference_data.groups():
|
|
940
|
+
raise model.NotFittedModelError(
|
|
941
|
+
'Running budget optimization scenarios requires fitting the model.'
|
|
942
|
+
)
|
|
943
|
+
|
|
895
944
|
def optimize(
|
|
896
945
|
self,
|
|
897
946
|
use_posterior: bool = True,
|
|
@@ -930,24 +979,33 @@ class BudgetOptimizer:
|
|
|
930
979
|
specify either `target_roi` or `target_mroi`.
|
|
931
980
|
budget: Number indicating the total budget for the fixed budget scenario.
|
|
932
981
|
Defaults to the historical budget.
|
|
933
|
-
pct_of_spend: Numeric list of size `
|
|
982
|
+
pct_of_spend: Numeric list of size `n_paid_channels` containing the
|
|
934
983
|
percentage allocation for spend for all media and RF channels. The order
|
|
935
|
-
must match `InputData.media` with values between
|
|
936
|
-
default, the historical allocation is used. Budget
|
|
937
|
-
used in conjunction to determine the non-optimized
|
|
938
|
-
which is used to calculate the non-optimized
|
|
939
|
-
example, ROI) and construct the feasible range
|
|
940
|
-
the spend constraints.
|
|
941
|
-
|
|
984
|
+
must match `(InputData.media + InputData.reach)` with values between
|
|
985
|
+
0-1, summing to 1. By default, the historical allocation is used. Budget
|
|
986
|
+
and allocation are used in conjunction to determine the non-optimized
|
|
987
|
+
media-level spend, which is used to calculate the non-optimized
|
|
988
|
+
performance metrics (for example, ROI) and construct the feasible range
|
|
989
|
+
of media-level spend with the spend constraints. Consider using
|
|
990
|
+
`InputData.get_paid_channels_argument_builder()` to construct this
|
|
991
|
+
argument.
|
|
992
|
+
spend_constraint_lower: Numeric list of size `n_paid_channels` or float
|
|
942
993
|
(same constraint for all channels) indicating the lower bound of
|
|
943
|
-
media-level spend.
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
994
|
+
media-level spend. If given as a channel-indexed array, the order must
|
|
995
|
+
match `(InputData.media + InputData.reach)`. The lower bound of
|
|
996
|
+
media-level spend is `(1 - spend_constraint_lower) * budget *
|
|
997
|
+
allocation)`. The value must be between 0-1. Defaults to `0.3` for fixed
|
|
998
|
+
budget and `1` for flexible. Consider using
|
|
999
|
+
`InputData.get_paid_channels_argument_builder()` to construct this
|
|
1000
|
+
argument.
|
|
1001
|
+
spend_constraint_upper: Numeric list of size `n_paid_channels` or float
|
|
947
1002
|
(same constraint for all channels) indicating the upper bound of
|
|
948
|
-
media-level spend.
|
|
949
|
-
|
|
950
|
-
|
|
1003
|
+
media-level spend. If given as a channel-indexed array, the order must
|
|
1004
|
+
match `(InputData.media + InputData.reach)`. The upper bound of
|
|
1005
|
+
media-level spend is `(1 + spend_constraint_upper) * budget *
|
|
1006
|
+
allocation)`. Defaults to `0.3` for fixed budget and `1` for flexible.
|
|
1007
|
+
Consider using `InputData.get_paid_channels_argument_builder()` to
|
|
1008
|
+
construct this argument.
|
|
951
1009
|
target_roi: Float indicating the target ROI constraint. Only used for
|
|
952
1010
|
flexible budget scenarios. The budget is constrained to when the ROI of
|
|
953
1011
|
the total spend hits `target_roi`.
|
|
@@ -972,12 +1030,13 @@ class BudgetOptimizer:
|
|
|
972
1030
|
An `OptimizationResults` object containing optimized budget allocation
|
|
973
1031
|
datasets, along with some of the intermediate values used to derive them.
|
|
974
1032
|
"""
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1033
|
+
_validate_budget(
|
|
1034
|
+
fixed_budget=fixed_budget,
|
|
1035
|
+
budget=budget,
|
|
1036
|
+
target_roi=target_roi,
|
|
1037
|
+
target_mroi=target_mroi,
|
|
1038
|
+
)
|
|
1039
|
+
|
|
981
1040
|
if selected_times is not None:
|
|
982
1041
|
start_date, end_date = selected_times
|
|
983
1042
|
selected_time_dims = self._meridian.expand_selected_time_dims(
|
|
@@ -986,23 +1045,17 @@ class BudgetOptimizer:
|
|
|
986
1045
|
)
|
|
987
1046
|
else:
|
|
988
1047
|
selected_time_dims = None
|
|
989
|
-
|
|
990
1048
|
hist_spend = self._analyzer.get_historical_spend(
|
|
991
1049
|
selected_time_dims,
|
|
992
1050
|
include_media=self._meridian.n_media_channels > 0,
|
|
993
1051
|
include_rf=self._meridian.n_rf_channels > 0,
|
|
994
1052
|
).data
|
|
995
1053
|
|
|
996
|
-
use_historical_budget = budget is None or round(budget) == round(
|
|
997
|
-
np.sum(hist_spend)
|
|
998
|
-
)
|
|
999
1054
|
budget = budget or np.sum(hist_spend)
|
|
1000
1055
|
pct_of_spend = self._validate_pct_of_spend(hist_spend, pct_of_spend)
|
|
1001
1056
|
spend = budget * pct_of_spend
|
|
1002
1057
|
round_factor = _get_round_factor(budget, gtol)
|
|
1003
|
-
step_size = 10 ** (-round_factor)
|
|
1004
1058
|
rounded_spend = np.round(spend, round_factor).astype(int)
|
|
1005
|
-
spend_ratio = spend / hist_spend
|
|
1006
1059
|
if self._meridian.n_rf_channels > 0 and use_optimal_frequency:
|
|
1007
1060
|
optimal_frequency = tf.convert_to_tensor(
|
|
1008
1061
|
self._analyzer.optimal_freq(
|
|
@@ -1024,34 +1077,30 @@ class BudgetOptimizer:
|
|
|
1024
1077
|
fixed_budget=fixed_budget,
|
|
1025
1078
|
)
|
|
1026
1079
|
)
|
|
1027
|
-
|
|
1080
|
+
optimization_grid = self.create_optimization_grid(
|
|
1028
1081
|
spend=hist_spend,
|
|
1029
1082
|
spend_bound_lower=optimization_lower_bound,
|
|
1030
1083
|
spend_bound_upper=optimization_upper_bound,
|
|
1031
|
-
step_size=step_size,
|
|
1032
1084
|
selected_times=selected_time_dims,
|
|
1085
|
+
round_factor=round_factor,
|
|
1033
1086
|
use_posterior=use_posterior,
|
|
1034
1087
|
use_kpi=use_kpi,
|
|
1088
|
+
use_optimal_frequency=use_optimal_frequency,
|
|
1035
1089
|
optimal_frequency=optimal_frequency,
|
|
1036
1090
|
batch_size=batch_size,
|
|
1037
1091
|
)
|
|
1092
|
+
# TODO: b/375644691) - Move grid search to a OptimizationGrid class.
|
|
1038
1093
|
optimal_spend = self._grid_search(
|
|
1039
|
-
spend_grid=spend_grid,
|
|
1040
|
-
incremental_outcome_grid=incremental_outcome_grid,
|
|
1094
|
+
spend_grid=optimization_grid.spend_grid,
|
|
1095
|
+
incremental_outcome_grid=optimization_grid.incremental_outcome_grid,
|
|
1041
1096
|
budget=np.sum(rounded_spend),
|
|
1042
1097
|
fixed_budget=fixed_budget,
|
|
1043
1098
|
target_mroi=target_mroi,
|
|
1044
1099
|
target_roi=target_roi,
|
|
1045
1100
|
)
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
}
|
|
1050
|
-
if target_roi:
|
|
1051
|
-
constraints[c.TARGET_ROI] = target_roi
|
|
1052
|
-
elif target_mroi:
|
|
1053
|
-
constraints[c.TARGET_MROI] = target_mroi
|
|
1054
|
-
|
|
1101
|
+
use_historical_budget = budget is None or round(budget) == round(
|
|
1102
|
+
np.sum(hist_spend)
|
|
1103
|
+
)
|
|
1055
1104
|
nonoptimized_data = self._create_budget_dataset(
|
|
1056
1105
|
use_posterior=use_posterior,
|
|
1057
1106
|
use_kpi=use_kpi,
|
|
@@ -1073,6 +1122,13 @@ class BudgetOptimizer:
|
|
|
1073
1122
|
batch_size=batch_size,
|
|
1074
1123
|
use_historical_budget=use_historical_budget,
|
|
1075
1124
|
)
|
|
1125
|
+
constraints = {
|
|
1126
|
+
c.FIXED_BUDGET: fixed_budget,
|
|
1127
|
+
}
|
|
1128
|
+
if target_roi:
|
|
1129
|
+
constraints[c.TARGET_ROI] = target_roi
|
|
1130
|
+
elif target_mroi:
|
|
1131
|
+
constraints[c.TARGET_MROI] = target_mroi
|
|
1076
1132
|
optimized_data = self._create_budget_dataset(
|
|
1077
1133
|
use_posterior=use_posterior,
|
|
1078
1134
|
use_kpi=use_kpi,
|
|
@@ -1085,18 +1141,16 @@ class BudgetOptimizer:
|
|
|
1085
1141
|
batch_size=batch_size,
|
|
1086
1142
|
use_historical_budget=use_historical_budget,
|
|
1087
1143
|
)
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1144
|
+
spend_ratio = np.divide(
|
|
1145
|
+
spend,
|
|
1146
|
+
hist_spend,
|
|
1147
|
+
out=np.zeros_like(hist_spend, dtype=float),
|
|
1148
|
+
where=hist_spend != 0,
|
|
1093
1149
|
)
|
|
1094
1150
|
|
|
1095
1151
|
return OptimizationResults(
|
|
1096
1152
|
meridian=self._meridian,
|
|
1097
1153
|
analyzer=self._analyzer,
|
|
1098
|
-
use_posterior=use_posterior,
|
|
1099
|
-
use_optimal_frequency=use_optimal_frequency,
|
|
1100
1154
|
spend_ratio=spend_ratio,
|
|
1101
1155
|
spend_bounds=spend_bounds,
|
|
1102
1156
|
_nonoptimized_data=nonoptimized_data,
|
|
@@ -1105,7 +1159,83 @@ class BudgetOptimizer:
|
|
|
1105
1159
|
_optimization_grid=optimization_grid,
|
|
1106
1160
|
)
|
|
1107
1161
|
|
|
1108
|
-
def
|
|
1162
|
+
def create_optimization_grid(
|
|
1163
|
+
self,
|
|
1164
|
+
spend: np.ndarray,
|
|
1165
|
+
spend_bound_lower: np.ndarray,
|
|
1166
|
+
spend_bound_upper: np.ndarray,
|
|
1167
|
+
selected_times: Sequence[str] | None,
|
|
1168
|
+
round_factor: int,
|
|
1169
|
+
use_posterior: bool = True,
|
|
1170
|
+
use_kpi: bool = False,
|
|
1171
|
+
use_optimal_frequency: bool = True,
|
|
1172
|
+
optimal_frequency: xr.DataArray | None = None,
|
|
1173
|
+
batch_size: int = c.DEFAULT_BATCH_SIZE,
|
|
1174
|
+
) -> OptimizationGrid:
|
|
1175
|
+
"""Creates a OptimizationGrid for optimization.
|
|
1176
|
+
|
|
1177
|
+
Args:
|
|
1178
|
+
spend: ndarray of shape `(n_paid_channels,)` with spend per paid channel.
|
|
1179
|
+
spend_bound_lower: ndarray of dimension `(n_total_channels,)` containing
|
|
1180
|
+
the lower constraint spend for each channel.
|
|
1181
|
+
spend_bound_upper: ndarray of dimension `(n_total_channels,)` containing
|
|
1182
|
+
the upper constraint spend for each channel.
|
|
1183
|
+
selected_times: Sequence of strings representing the time dimensions in
|
|
1184
|
+
`meridian.input_data.time` to use for optimization.
|
|
1185
|
+
round_factor: The round factor used for the optimization grid.
|
|
1186
|
+
use_posterior: Boolean. If `True`, then the incremental outcome is derived
|
|
1187
|
+
from the posterior distribution of the model. Otherwise, the prior
|
|
1188
|
+
distribution is used.
|
|
1189
|
+
use_kpi: Boolean. If `True`, then the incremental outcome is derived from
|
|
1190
|
+
the KPI impact. Otherwise, the incremental outcome is derived from the
|
|
1191
|
+
revenue impact.
|
|
1192
|
+
use_optimal_frequency: Boolean. Whether optimal frequency was used.
|
|
1193
|
+
optimal_frequency: `xr.DataArray` with dimension `n_rf_channels`,
|
|
1194
|
+
containing the optimal frequency per channel, that maximizes mean ROI
|
|
1195
|
+
over the corresponding prior/posterior distribution. Value is `None` if
|
|
1196
|
+
the model does not contain reach and frequency data, or if the model
|
|
1197
|
+
does contain reach and frequency data, but historical frequency is used
|
|
1198
|
+
for the optimization scenario.
|
|
1199
|
+
batch_size: Max draws per chain in each batch. The calculation is run in
|
|
1200
|
+
batches to avoid memory exhaustion. If a memory error occurs, try
|
|
1201
|
+
reducing `batch_size`. The calculation will generally be faster with
|
|
1202
|
+
larger `batch_size` values.
|
|
1203
|
+
|
|
1204
|
+
Returns:
|
|
1205
|
+
An OptimizationGrid object containing the grid data for optimization.
|
|
1206
|
+
"""
|
|
1207
|
+
self._validate_model_fit(use_posterior)
|
|
1208
|
+
|
|
1209
|
+
step_size = 10 ** (-round_factor)
|
|
1210
|
+
(spend_grid, incremental_outcome_grid) = self._create_grids(
|
|
1211
|
+
spend=spend,
|
|
1212
|
+
spend_bound_lower=spend_bound_lower,
|
|
1213
|
+
spend_bound_upper=spend_bound_upper,
|
|
1214
|
+
step_size=step_size,
|
|
1215
|
+
selected_times=selected_times,
|
|
1216
|
+
use_posterior=use_posterior,
|
|
1217
|
+
use_kpi=use_kpi,
|
|
1218
|
+
optimal_frequency=optimal_frequency,
|
|
1219
|
+
batch_size=batch_size,
|
|
1220
|
+
)
|
|
1221
|
+
grid_dataset = self._create_grid_dataset(
|
|
1222
|
+
spend_grid=spend_grid,
|
|
1223
|
+
spend_step_size=step_size,
|
|
1224
|
+
incremental_outcome_grid=incremental_outcome_grid,
|
|
1225
|
+
)
|
|
1226
|
+
|
|
1227
|
+
return OptimizationGrid(
|
|
1228
|
+
_grid_dataset=grid_dataset,
|
|
1229
|
+
spend=spend,
|
|
1230
|
+
use_kpi=use_kpi,
|
|
1231
|
+
use_posterior=use_posterior,
|
|
1232
|
+
use_optimal_frequency=use_optimal_frequency,
|
|
1233
|
+
round_factor=round_factor,
|
|
1234
|
+
optimal_frequency=optimal_frequency,
|
|
1235
|
+
selected_times=selected_times,
|
|
1236
|
+
)
|
|
1237
|
+
|
|
1238
|
+
def _create_grid_dataset(
|
|
1109
1239
|
self,
|
|
1110
1240
|
spend_grid: np.ndarray,
|
|
1111
1241
|
spend_step_size: float,
|
|
@@ -1151,39 +1281,6 @@ class BudgetOptimizer:
|
|
|
1151
1281
|
attrs={c.SPEND_STEP_SIZE: spend_step_size},
|
|
1152
1282
|
)
|
|
1153
1283
|
|
|
1154
|
-
def _validate_budget(
|
|
1155
|
-
self,
|
|
1156
|
-
fixed_budget: bool,
|
|
1157
|
-
budget: float | None,
|
|
1158
|
-
target_roi: float | None,
|
|
1159
|
-
target_mroi: float | None,
|
|
1160
|
-
):
|
|
1161
|
-
"""Validates the budget optimization arguments."""
|
|
1162
|
-
if fixed_budget:
|
|
1163
|
-
if target_roi is not None:
|
|
1164
|
-
raise ValueError(
|
|
1165
|
-
'`target_roi` is only used for flexible budget scenarios.'
|
|
1166
|
-
)
|
|
1167
|
-
if target_mroi is not None:
|
|
1168
|
-
raise ValueError(
|
|
1169
|
-
'`target_mroi` is only used for flexible budget scenarios.'
|
|
1170
|
-
)
|
|
1171
|
-
if budget is not None and budget <= 0:
|
|
1172
|
-
raise ValueError('`budget` must be greater than zero.')
|
|
1173
|
-
else:
|
|
1174
|
-
if budget is not None:
|
|
1175
|
-
raise ValueError('`budget` is only used for fixed budget scenarios.')
|
|
1176
|
-
if target_roi is None and target_mroi is None:
|
|
1177
|
-
raise ValueError(
|
|
1178
|
-
'Must specify either `target_roi` or `target_mroi` for flexible'
|
|
1179
|
-
' budget optimization.'
|
|
1180
|
-
)
|
|
1181
|
-
if target_roi is not None and target_mroi is not None:
|
|
1182
|
-
raise ValueError(
|
|
1183
|
-
'Must specify only one of `target_roi` or `target_mroi` for'
|
|
1184
|
-
'flexible budget optimization.'
|
|
1185
|
-
)
|
|
1186
|
-
|
|
1187
1284
|
def _validate_pct_of_spend(
|
|
1188
1285
|
self, hist_spend: np.ndarray, pct_of_spend: Sequence[float] | None
|
|
1189
1286
|
) -> np.ndarray:
|
|
@@ -1377,27 +1474,6 @@ class BudgetOptimizer:
|
|
|
1377
1474
|
incremental_outcome_with_mean_median_and_ci[:, 0]
|
|
1378
1475
|
)
|
|
1379
1476
|
|
|
1380
|
-
# expected_outcome here is a tensor with the shape (n_chains, n_draws)
|
|
1381
|
-
expected_outcome = self._analyzer.expected_outcome(
|
|
1382
|
-
use_posterior=use_posterior,
|
|
1383
|
-
new_data=analyzer.DataTensors(
|
|
1384
|
-
media=new_media,
|
|
1385
|
-
reach=new_reach,
|
|
1386
|
-
frequency=new_frequency,
|
|
1387
|
-
),
|
|
1388
|
-
selected_times=selected_times,
|
|
1389
|
-
use_kpi=use_kpi,
|
|
1390
|
-
batch_size=batch_size,
|
|
1391
|
-
)
|
|
1392
|
-
mean_expected_outcome = tf.reduce_mean(expected_outcome, (0, 1)) # a scalar
|
|
1393
|
-
|
|
1394
|
-
pct_contrib = incremental_outcome / mean_expected_outcome[..., None] * 100
|
|
1395
|
-
pct_contrib_with_mean_median_and_ci = analyzer.get_central_tendency_and_ci(
|
|
1396
|
-
data=pct_contrib,
|
|
1397
|
-
confidence_level=confidence_level,
|
|
1398
|
-
include_median=True,
|
|
1399
|
-
)
|
|
1400
|
-
|
|
1401
1477
|
aggregated_impressions = self._analyzer.get_aggregated_impressions(
|
|
1402
1478
|
selected_times=selected_times,
|
|
1403
1479
|
selected_geos=None,
|
|
@@ -1458,10 +1534,6 @@ class BudgetOptimizer:
|
|
|
1458
1534
|
[c.CHANNEL, c.METRIC],
|
|
1459
1535
|
incremental_outcome_with_mean_median_and_ci,
|
|
1460
1536
|
),
|
|
1461
|
-
c.PCT_OF_CONTRIBUTION: (
|
|
1462
|
-
[c.CHANNEL, c.METRIC],
|
|
1463
|
-
pct_contrib_with_mean_median_and_ci,
|
|
1464
|
-
),
|
|
1465
1537
|
c.EFFECTIVENESS: (
|
|
1466
1538
|
[c.CHANNEL, c.METRIC],
|
|
1467
1539
|
effectiveness_with_mean_median_and_ci,
|
|
@@ -1651,11 +1723,11 @@ class BudgetOptimizer:
|
|
|
1651
1723
|
"""Creates spend and incremental outcome grids for optimization algorithm.
|
|
1652
1724
|
|
|
1653
1725
|
Args:
|
|
1654
|
-
spend: np.ndarray with actual spend per media or RF channel.
|
|
1655
|
-
spend_bound_lower: np.ndarray of dimension (`n_total_channels`)
|
|
1656
|
-
the lower constraint spend for each channel.
|
|
1657
|
-
spend_bound_upper: np.ndarray of dimension (`n_total_channels`)
|
|
1658
|
-
the upper constraint spend for each channel.
|
|
1726
|
+
spend: `np.ndarray` with actual spend per media or RF channel.
|
|
1727
|
+
spend_bound_lower: `np.ndarray` of dimension (`n_total_channels`)
|
|
1728
|
+
containing the lower constraint spend for each channel.
|
|
1729
|
+
spend_bound_upper: `np.ndarray` of dimension (`n_total_channels`)
|
|
1730
|
+
containing the upper constraint spend for each channel.
|
|
1659
1731
|
step_size: Integer indicating the step size, or interval, between values
|
|
1660
1732
|
in the spend grid. All media channels have the same step size.
|
|
1661
1733
|
selected_times: Sequence of strings representing the time dimensions in
|
|
@@ -1666,11 +1738,12 @@ class BudgetOptimizer:
|
|
|
1666
1738
|
use_kpi: Boolean. If `True`, then the incremental outcome is derived from
|
|
1667
1739
|
the KPI impact. Otherwise, the incremental outcome is derived from the
|
|
1668
1740
|
revenue impact.
|
|
1669
|
-
optimal_frequency: xr.DataArray with dimension `n_rf_channels`,
|
|
1670
|
-
the optimal frequency per channel, that maximizes
|
|
1671
|
-
Value is `None` if
|
|
1672
|
-
|
|
1673
|
-
frequency
|
|
1741
|
+
optimal_frequency: `xr.DataArray` with dimension `n_rf_channels`,
|
|
1742
|
+
containing the optimal frequency per channel, that maximizes mean ROI
|
|
1743
|
+
over the corresponding prior/posterior distribution. Value is `None` if
|
|
1744
|
+
the model does not contain reach and frequency data, or if the model
|
|
1745
|
+
does contain reach and frequency data, but historical frequency is used
|
|
1746
|
+
for the optimization scenario.
|
|
1674
1747
|
batch_size: Max draws per chain in each batch. The calculation is run in
|
|
1675
1748
|
batches to avoid memory exhaustion. If a memory error occurs, try
|
|
1676
1749
|
reducing `batch_size`. The calculation will generally be faster with
|
|
@@ -1678,11 +1751,11 @@ class BudgetOptimizer:
|
|
|
1678
1751
|
|
|
1679
1752
|
Returns:
|
|
1680
1753
|
spend_grid: Discrete two-dimensional grid with the number of rows
|
|
1681
|
-
determined by the `
|
|
1754
|
+
determined by the `spend_bound_**` and `step_size`, and the number of
|
|
1682
1755
|
columns is equal to the number of total channels, containing spend by
|
|
1683
1756
|
channel.
|
|
1684
1757
|
incremental_outcome_grid: Discrete two-dimensional grid with the number of
|
|
1685
|
-
rows determined by the `
|
|
1758
|
+
rows determined by the `spend_bound_**` and `step_size`, and the
|
|
1686
1759
|
number of columns is equal to the number of total channels, containing
|
|
1687
1760
|
incremental outcome by channel.
|
|
1688
1761
|
"""
|
|
@@ -1700,9 +1773,12 @@ class BudgetOptimizer:
|
|
|
1700
1773
|
)
|
|
1701
1774
|
spend_grid[: len(spend_grid_m), i] = spend_grid_m
|
|
1702
1775
|
incremental_outcome_grid = np.full([n_grid_rows, n_grid_columns], np.nan)
|
|
1703
|
-
|
|
1776
|
+
multipliers_grid_base = tf.cast(
|
|
1704
1777
|
tf.math.divide_no_nan(spend_grid, spend), dtype=tf.float32
|
|
1705
1778
|
)
|
|
1779
|
+
multipliers_grid = np.where(
|
|
1780
|
+
np.isnan(spend_grid), np.nan, multipliers_grid_base
|
|
1781
|
+
)
|
|
1706
1782
|
for i in range(n_grid_rows):
|
|
1707
1783
|
self._update_incremental_outcome_grid(
|
|
1708
1784
|
i=i,
|
|
@@ -1824,6 +1900,39 @@ class BudgetOptimizer:
|
|
|
1824
1900
|
return spend_optimal
|
|
1825
1901
|
|
|
1826
1902
|
|
|
1903
|
+
def _validate_budget(
|
|
1904
|
+
fixed_budget: bool,
|
|
1905
|
+
budget: float | None,
|
|
1906
|
+
target_roi: float | None,
|
|
1907
|
+
target_mroi: float | None,
|
|
1908
|
+
):
|
|
1909
|
+
"""Validates the budget optimization arguments."""
|
|
1910
|
+
if fixed_budget:
|
|
1911
|
+
if target_roi is not None:
|
|
1912
|
+
raise ValueError(
|
|
1913
|
+
'`target_roi` is only used for flexible budget scenarios.'
|
|
1914
|
+
)
|
|
1915
|
+
if target_mroi is not None:
|
|
1916
|
+
raise ValueError(
|
|
1917
|
+
'`target_mroi` is only used for flexible budget scenarios.'
|
|
1918
|
+
)
|
|
1919
|
+
if budget is not None and budget <= 0:
|
|
1920
|
+
raise ValueError('`budget` must be greater than zero.')
|
|
1921
|
+
else:
|
|
1922
|
+
if budget is not None:
|
|
1923
|
+
raise ValueError('`budget` is only used for fixed budget scenarios.')
|
|
1924
|
+
if target_roi is None and target_mroi is None:
|
|
1925
|
+
raise ValueError(
|
|
1926
|
+
'Must specify either `target_roi` or `target_mroi` for flexible'
|
|
1927
|
+
' budget optimization.'
|
|
1928
|
+
)
|
|
1929
|
+
if target_roi is not None and target_mroi is not None:
|
|
1930
|
+
raise ValueError(
|
|
1931
|
+
'Must specify only one of `target_roi` or `target_mroi` for'
|
|
1932
|
+
'flexible budget optimization.'
|
|
1933
|
+
)
|
|
1934
|
+
|
|
1935
|
+
|
|
1827
1936
|
def _get_round_factor(budget: float, gtol: float) -> int:
|
|
1828
1937
|
"""Function for obtaining number of integer digits to round off of budget.
|
|
1829
1938
|
|
|
@@ -1888,6 +1997,11 @@ def _exceeds_optimization_constraints(
|
|
|
1888
1997
|
if fixed_budget:
|
|
1889
1998
|
return np.sum(spend) > budget
|
|
1890
1999
|
elif target_roi is not None:
|
|
1891
|
-
|
|
2000
|
+
cur_total_roi = np.sum(incremental_outcome) / np.sum(spend)
|
|
2001
|
+
# In addition to the total roi being less than the target roi, the roi of
|
|
2002
|
+
# the current optimization step should also be less than the total roi.
|
|
2003
|
+
# Without the second condition, the optimization algorithm may not have
|
|
2004
|
+
# found the roi point close to the target roi yet.
|
|
2005
|
+
return cur_total_roi < target_roi and roi_grid_point < cur_total_roi
|
|
1892
2006
|
else:
|
|
1893
2007
|
return roi_grid_point < target_mroi
|
meridian/analysis/summarizer.py
CHANGED
|
@@ -227,8 +227,8 @@ class Summarizer:
|
|
|
227
227
|
]
|
|
228
228
|
row_values = [
|
|
229
229
|
'{:.2f}'.format(sliced_table_by_eval_set[c.R_SQUARED].item()),
|
|
230
|
-
|
|
231
|
-
|
|
230
|
+
formatter.format_percent(sliced_table_by_eval_set[c.MAPE].item()),
|
|
231
|
+
formatter.format_percent(sliced_table_by_eval_set[c.WMAPE].item()),
|
|
232
232
|
]
|
|
233
233
|
return row_values
|
|
234
234
|
|