google-meridian 1.3.1__py3-none-any.whl → 1.4.0__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.3.1.dist-info → google_meridian-1.4.0.dist-info}/METADATA +13 -9
- google_meridian-1.4.0.dist-info/RECORD +108 -0
- {google_meridian-1.3.1.dist-info → google_meridian-1.4.0.dist-info}/top_level.txt +1 -0
- meridian/analysis/__init__.py +1 -2
- meridian/analysis/analyzer.py +0 -1
- meridian/analysis/optimizer.py +5 -3
- meridian/analysis/review/checks.py +81 -30
- meridian/analysis/review/constants.py +4 -0
- meridian/analysis/review/results.py +40 -9
- meridian/analysis/summarizer.py +8 -3
- meridian/analysis/test_utils.py +934 -485
- meridian/analysis/visualizer.py +11 -7
- meridian/backend/__init__.py +53 -5
- meridian/backend/test_utils.py +72 -0
- meridian/constants.py +2 -0
- meridian/data/load.py +2 -0
- meridian/data/test_utils.py +82 -10
- meridian/model/__init__.py +2 -0
- meridian/model/context.py +925 -0
- meridian/model/eda/__init__.py +0 -1
- meridian/model/eda/constants.py +13 -2
- meridian/model/eda/eda_engine.py +299 -37
- meridian/model/eda/eda_outcome.py +21 -1
- meridian/model/equations.py +418 -0
- meridian/model/knots.py +75 -47
- meridian/model/model.py +93 -792
- meridian/{analysis/templates → templates}/card.html.jinja +1 -1
- meridian/{analysis/templates → templates}/chart.html.jinja +1 -1
- meridian/{analysis/templates → templates}/chips.html.jinja +1 -1
- meridian/{analysis → templates}/formatter.py +12 -1
- meridian/templates/formatter_test.py +216 -0
- meridian/{analysis/templates → templates}/insights.html.jinja +1 -1
- meridian/{analysis/templates → templates}/stats.html.jinja +1 -1
- meridian/{analysis/templates → templates}/style.css +1 -1
- meridian/{analysis/templates → templates}/style.scss +1 -1
- meridian/{analysis/templates → templates}/summary.html.jinja +4 -2
- meridian/{analysis/templates → templates}/table.html.jinja +1 -1
- meridian/version.py +1 -1
- scenarioplanner/__init__.py +42 -0
- scenarioplanner/converters/__init__.py +25 -0
- scenarioplanner/converters/dataframe/__init__.py +28 -0
- scenarioplanner/converters/dataframe/budget_opt_converters.py +383 -0
- scenarioplanner/converters/dataframe/common.py +71 -0
- scenarioplanner/converters/dataframe/constants.py +137 -0
- scenarioplanner/converters/dataframe/converter.py +42 -0
- scenarioplanner/converters/dataframe/dataframe_model_converter.py +70 -0
- scenarioplanner/converters/dataframe/marketing_analyses_converters.py +543 -0
- scenarioplanner/converters/dataframe/rf_opt_converters.py +314 -0
- scenarioplanner/converters/mmm.py +743 -0
- scenarioplanner/converters/mmm_converter.py +58 -0
- scenarioplanner/converters/sheets.py +156 -0
- scenarioplanner/converters/test_data.py +714 -0
- scenarioplanner/linkingapi/__init__.py +47 -0
- scenarioplanner/linkingapi/constants.py +27 -0
- scenarioplanner/linkingapi/url_generator.py +131 -0
- scenarioplanner/mmm_ui_proto_generator.py +354 -0
- schema/__init__.py +15 -0
- schema/mmm_proto_generator.py +71 -0
- schema/model_consumer.py +133 -0
- schema/processors/__init__.py +77 -0
- schema/processors/budget_optimization_processor.py +832 -0
- schema/processors/common.py +64 -0
- schema/processors/marketing_processor.py +1136 -0
- schema/processors/model_fit_processor.py +367 -0
- schema/processors/model_kernel_processor.py +117 -0
- schema/processors/model_processor.py +412 -0
- schema/processors/reach_frequency_optimization_processor.py +584 -0
- schema/test_data.py +380 -0
- schema/utils/__init__.py +1 -0
- schema/utils/date_range_bucketing.py +117 -0
- google_meridian-1.3.1.dist-info/RECORD +0 -76
- meridian/model/eda/meridian_eda.py +0 -220
- {google_meridian-1.3.1.dist-info → google_meridian-1.4.0.dist-info}/WHEEL +0 -0
- {google_meridian-1.3.1.dist-info → google_meridian-1.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
# Copyright 2025 The Meridian Authors.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Budget optimization converters.
|
|
16
|
+
|
|
17
|
+
This module defines various classes that convert `BudgetOptimizationResult`s
|
|
18
|
+
into flat dataframes.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import abc
|
|
22
|
+
from collections.abc import Iterator, Sequence
|
|
23
|
+
|
|
24
|
+
from meridian import constants as c
|
|
25
|
+
from mmm.v1.marketing.optimization import budget_optimization_pb2 as budget_pb
|
|
26
|
+
from mmm.v1.marketing.optimization import constraints_pb2 as constraints_pb
|
|
27
|
+
from scenarioplanner.converters import mmm
|
|
28
|
+
from scenarioplanner.converters.dataframe import common
|
|
29
|
+
from scenarioplanner.converters.dataframe import constants as dc
|
|
30
|
+
from scenarioplanner.converters.dataframe import converter
|
|
31
|
+
import pandas as pd
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"NamedOptimizationGridConverter",
|
|
36
|
+
"BudgetOptimizationSpecsConverter",
|
|
37
|
+
"BudgetOptimizationResultsConverter",
|
|
38
|
+
"BudgetOptimizationResponseCurvesConverter",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class _BudgetOptimizationConverter(converter.Converter, abc.ABC):
|
|
43
|
+
"""An abstract class for dealing with `BudgetOptimizationResult`s."""
|
|
44
|
+
|
|
45
|
+
def __call__(self) -> Iterator[tuple[str, pd.DataFrame]]:
|
|
46
|
+
results = self._mmm.budget_optimization_results
|
|
47
|
+
if not results:
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
# Validate group IDs.
|
|
51
|
+
group_ids = [result.group_id for result in results if result.group_id]
|
|
52
|
+
if len(set(group_ids)) != len(group_ids):
|
|
53
|
+
raise ValueError(
|
|
54
|
+
"Specified group_id must be unique or unset among the given group of"
|
|
55
|
+
" results."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
yield from self._handle_budget_optimization_results(
|
|
59
|
+
self._mmm.budget_optimization_results
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def _handle_budget_optimization_results(
|
|
63
|
+
self, results: Sequence[mmm.BudgetOptimizationResult]
|
|
64
|
+
) -> Iterator[tuple[str, pd.DataFrame]]:
|
|
65
|
+
raise NotImplementedError()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class NamedOptimizationGridConverter(_BudgetOptimizationConverter):
|
|
69
|
+
"""Outputs named tables for budget optimization grids.
|
|
70
|
+
|
|
71
|
+
When called, this converter returns a data frame with the columns:
|
|
72
|
+
|
|
73
|
+
* "Group ID"
|
|
74
|
+
A UUID generated for each named incremental outcome grid.
|
|
75
|
+
* "Channel"
|
|
76
|
+
* "Spend"
|
|
77
|
+
* "Incremental Outcome"
|
|
78
|
+
For each named budget optimization result in the MMM output proto.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def _handle_budget_optimization_results(
|
|
82
|
+
self, results: Sequence[mmm.BudgetOptimizationResult]
|
|
83
|
+
) -> Iterator[tuple[str, pd.DataFrame]]:
|
|
84
|
+
for budget_opt_result in results:
|
|
85
|
+
# There should be one unique ID for each result.
|
|
86
|
+
group_id = (
|
|
87
|
+
str(budget_opt_result.group_id) if budget_opt_result.group_id else ""
|
|
88
|
+
)
|
|
89
|
+
grid = budget_opt_result.incremental_outcome_grid
|
|
90
|
+
|
|
91
|
+
# Each grid yields its own data frame table.
|
|
92
|
+
optimization_grid_data = []
|
|
93
|
+
for channel, cells in grid.channel_spend_grids.items():
|
|
94
|
+
for spend, incremental_outcome in cells:
|
|
95
|
+
optimization_grid_data.append([
|
|
96
|
+
group_id,
|
|
97
|
+
channel,
|
|
98
|
+
spend,
|
|
99
|
+
incremental_outcome,
|
|
100
|
+
])
|
|
101
|
+
|
|
102
|
+
yield (
|
|
103
|
+
common.create_grid_sheet_name(
|
|
104
|
+
dc.OPTIMIZATION_GRID_NAME_PREFIX, grid.name
|
|
105
|
+
),
|
|
106
|
+
pd.DataFrame(
|
|
107
|
+
optimization_grid_data,
|
|
108
|
+
columns=[
|
|
109
|
+
dc.OPTIMIZATION_GROUP_ID_COLUMN,
|
|
110
|
+
dc.OPTIMIZATION_CHANNEL_COLUMN,
|
|
111
|
+
dc.OPTIMIZATION_GRID_SPEND_COLUMN,
|
|
112
|
+
dc.OPTIMIZATION_GRID_INCREMENTAL_OUTCOME_COLUMN,
|
|
113
|
+
],
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class BudgetOptimizationSpecsConverter(_BudgetOptimizationConverter):
|
|
119
|
+
"""Outputs a table of budget optimization specs.
|
|
120
|
+
|
|
121
|
+
When called, this converter returns a data frame with the columns:
|
|
122
|
+
|
|
123
|
+
* "Group ID"
|
|
124
|
+
A UUID generated for an incremental outcome grid present in the output.
|
|
125
|
+
* "Date Interval Start"
|
|
126
|
+
* "Date Interval End"
|
|
127
|
+
* "Analysis Period"
|
|
128
|
+
* "Objective"
|
|
129
|
+
* "Scenario Type"
|
|
130
|
+
* "Initial Channel Spend"
|
|
131
|
+
* "Target Metric Constraint"
|
|
132
|
+
None if scenario type is "Fixed"
|
|
133
|
+
* "Target Metric Value"
|
|
134
|
+
None if scenario type is "Fixed"
|
|
135
|
+
* "Channel"
|
|
136
|
+
* "Channel Spend Min"
|
|
137
|
+
* "Channel Spend Max"
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def _handle_budget_optimization_results(
|
|
141
|
+
self, results: Sequence[mmm.BudgetOptimizationResult]
|
|
142
|
+
) -> Iterator[tuple[str, pd.DataFrame]]:
|
|
143
|
+
spec_data = []
|
|
144
|
+
for budget_opt_result in results:
|
|
145
|
+
# There should be one unique ID for each result.
|
|
146
|
+
group_id = (
|
|
147
|
+
str(budget_opt_result.group_id) if budget_opt_result.group_id else ""
|
|
148
|
+
)
|
|
149
|
+
spec = budget_opt_result.spec
|
|
150
|
+
|
|
151
|
+
objective = common.map_target_metric_str(spec.objective)
|
|
152
|
+
# These are the start and end dates for the requested budget optimization
|
|
153
|
+
# in this spec.
|
|
154
|
+
date_interval_start, date_interval_end = (
|
|
155
|
+
d.strftime(c.DATE_FORMAT) for d in spec.date_interval.date_interval
|
|
156
|
+
)
|
|
157
|
+
budget_date_interval = (date_interval_start, date_interval_end)
|
|
158
|
+
|
|
159
|
+
# aka historical spend from marketing data in the model kernel
|
|
160
|
+
initial_channel_spends = self._mmm.marketing_data.all_channel_spends(
|
|
161
|
+
budget_date_interval
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
scenario = (
|
|
165
|
+
dc.OPTIMIZATION_SPEC_SCENARIO_FIXED
|
|
166
|
+
if spec.is_fixed_scenario
|
|
167
|
+
else dc.OPTIMIZATION_SPEC_SCENARIO_FLEXIBLE
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if spec.is_fixed_scenario:
|
|
171
|
+
target_metric_constraint = None
|
|
172
|
+
target_metric_value = None
|
|
173
|
+
else:
|
|
174
|
+
flexible_scenario = (
|
|
175
|
+
spec.budget_optimization_spec_proto.flexible_budget_scenario
|
|
176
|
+
)
|
|
177
|
+
# Meridian flexible budget spec only has one target metric constraint.
|
|
178
|
+
target_metric_constraint_pb = (
|
|
179
|
+
flexible_scenario.target_metric_constraints[0]
|
|
180
|
+
)
|
|
181
|
+
target_metric_constraint = common.map_target_metric_str(
|
|
182
|
+
target_metric_constraint_pb.target_metric
|
|
183
|
+
)
|
|
184
|
+
target_metric_value = target_metric_constraint_pb.target_value
|
|
185
|
+
|
|
186
|
+
# When the constraint of a channel is not specified, that channel will
|
|
187
|
+
# have a constraint of `[0, max_budget]` which is equivalent to no
|
|
188
|
+
# constraint.
|
|
189
|
+
#
|
|
190
|
+
# Here, `max_budget` is the total budget for a fixed scenario spec, or the
|
|
191
|
+
# max budget upper bound for a flexible scenario spec.
|
|
192
|
+
#
|
|
193
|
+
# NOTE: This assumption must be in line with what the budget optimization
|
|
194
|
+
# processor does with an empty channel constraints list.
|
|
195
|
+
channel_constraints = spec.channel_constraints
|
|
196
|
+
if not channel_constraints:
|
|
197
|
+
# Implicit channel constraints; synthesize them first before proceeding.
|
|
198
|
+
channel_constraints = [
|
|
199
|
+
budget_pb.ChannelConstraint(
|
|
200
|
+
channel_name=channel_name,
|
|
201
|
+
budget_constraint=constraints_pb.BudgetConstraint(
|
|
202
|
+
min_budget=0.0,
|
|
203
|
+
max_budget=spec.max_budget,
|
|
204
|
+
),
|
|
205
|
+
)
|
|
206
|
+
for channel_name in self._mmm.marketing_data.media_channels
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
for channel_constraint in channel_constraints:
|
|
210
|
+
spec_data.append([
|
|
211
|
+
group_id,
|
|
212
|
+
date_interval_start,
|
|
213
|
+
date_interval_end,
|
|
214
|
+
spec.date_interval_tag,
|
|
215
|
+
objective,
|
|
216
|
+
scenario,
|
|
217
|
+
initial_channel_spends.get(channel_constraint.channel_name, 0.0),
|
|
218
|
+
target_metric_constraint,
|
|
219
|
+
target_metric_value,
|
|
220
|
+
channel_constraint.channel_name,
|
|
221
|
+
channel_constraint.budget_constraint.min_budget,
|
|
222
|
+
channel_constraint.budget_constraint.max_budget,
|
|
223
|
+
])
|
|
224
|
+
|
|
225
|
+
yield (
|
|
226
|
+
dc.OPTIMIZATION_SPECS,
|
|
227
|
+
pd.DataFrame(
|
|
228
|
+
spec_data,
|
|
229
|
+
columns=[
|
|
230
|
+
dc.OPTIMIZATION_GROUP_ID_COLUMN,
|
|
231
|
+
dc.OPTIMIZATION_SPEC_DATE_INTERVAL_START_COLUMN,
|
|
232
|
+
dc.OPTIMIZATION_SPEC_DATE_INTERVAL_END_COLUMN,
|
|
233
|
+
dc.ANALYSIS_PERIOD_COLUMN,
|
|
234
|
+
dc.OPTIMIZATION_SPEC_OBJECTIVE_COLUMN,
|
|
235
|
+
dc.OPTIMIZATION_SPEC_SCENARIO_TYPE_COLUMN,
|
|
236
|
+
dc.OPTIMIZATION_SPEC_INITIAL_CHANNEL_SPEND_COLUMN,
|
|
237
|
+
dc.OPTIMIZATION_SPEC_TARGET_METRIC_CONSTRAINT_COLUMN,
|
|
238
|
+
dc.OPTIMIZATION_SPEC_TARGET_METRIC_VALUE_COLUMN,
|
|
239
|
+
dc.OPTIMIZATION_CHANNEL_COLUMN,
|
|
240
|
+
dc.OPTIMIZATION_SPEC_CHANNEL_SPEND_MIN_COLUMN,
|
|
241
|
+
dc.OPTIMIZATION_SPEC_CHANNEL_SPEND_MAX_COLUMN,
|
|
242
|
+
],
|
|
243
|
+
),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class BudgetOptimizationResultsConverter(_BudgetOptimizationConverter):
|
|
248
|
+
"""Outputs a table of budget optimization results objectives.
|
|
249
|
+
|
|
250
|
+
When called, this converter returns a data frame with the columns:
|
|
251
|
+
|
|
252
|
+
* "Group ID"
|
|
253
|
+
A UUID generated for a budget optimization result present in the output.
|
|
254
|
+
* "Channel"
|
|
255
|
+
* "Is Revenue KPI"
|
|
256
|
+
Whether the KPI is revenue or not.
|
|
257
|
+
* "Optimal Spend"
|
|
258
|
+
* "Optimal Spend Share"
|
|
259
|
+
* "Optimal Impression Effectiveness"
|
|
260
|
+
* "Optimal ROI"
|
|
261
|
+
* "Optimal mROI"
|
|
262
|
+
* "Optimal CPC"
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
def _handle_budget_optimization_results(
|
|
266
|
+
self, results: Sequence[mmm.BudgetOptimizationResult]
|
|
267
|
+
) -> Iterator[tuple[str, pd.DataFrame]]:
|
|
268
|
+
data = []
|
|
269
|
+
|
|
270
|
+
for budget_opt_result in results:
|
|
271
|
+
group_id = (
|
|
272
|
+
str(budget_opt_result.group_id) if budget_opt_result.group_id else ""
|
|
273
|
+
)
|
|
274
|
+
marketing_analysis = budget_opt_result.optimized_marketing_analysis
|
|
275
|
+
|
|
276
|
+
media_channel_analyses = marketing_analysis.channel_mapped_media_analyses
|
|
277
|
+
for channel, media_analysis in media_channel_analyses.items():
|
|
278
|
+
# Skip "All Channels" pseudo-channel.
|
|
279
|
+
if channel == c.ALL_CHANNELS:
|
|
280
|
+
continue
|
|
281
|
+
|
|
282
|
+
spend = media_analysis.spend_info_pb.spend
|
|
283
|
+
spend_share = media_analysis.spend_info_pb.spend_share
|
|
284
|
+
|
|
285
|
+
revenue_outcome = media_analysis.maybe_revenue_outcome
|
|
286
|
+
nonrevenue_outcome = media_analysis.maybe_non_revenue_outcome
|
|
287
|
+
|
|
288
|
+
# pylint: disable=cell-var-from-loop
|
|
289
|
+
def _append_outcome_data(
|
|
290
|
+
outcome: mmm.Outcome | None,
|
|
291
|
+
is_revenue_kpi: bool,
|
|
292
|
+
) -> None:
|
|
293
|
+
if outcome is None:
|
|
294
|
+
return
|
|
295
|
+
effectiveness = outcome.effectiveness_pb.value.value
|
|
296
|
+
roi = outcome.roi_pb.value
|
|
297
|
+
mroi = outcome.marginal_roi_pb.value
|
|
298
|
+
cpc = outcome.cost_per_contribution_pb.value
|
|
299
|
+
data.append([
|
|
300
|
+
group_id,
|
|
301
|
+
channel,
|
|
302
|
+
is_revenue_kpi,
|
|
303
|
+
spend,
|
|
304
|
+
spend_share,
|
|
305
|
+
effectiveness,
|
|
306
|
+
roi,
|
|
307
|
+
mroi,
|
|
308
|
+
cpc,
|
|
309
|
+
])
|
|
310
|
+
|
|
311
|
+
_append_outcome_data(revenue_outcome, True)
|
|
312
|
+
_append_outcome_data(nonrevenue_outcome, False)
|
|
313
|
+
# pylint: enable=cell-var-from-loop
|
|
314
|
+
|
|
315
|
+
yield (
|
|
316
|
+
dc.OPTIMIZATION_RESULTS,
|
|
317
|
+
pd.DataFrame(
|
|
318
|
+
data,
|
|
319
|
+
columns=[
|
|
320
|
+
dc.OPTIMIZATION_GROUP_ID_COLUMN,
|
|
321
|
+
dc.OPTIMIZATION_CHANNEL_COLUMN,
|
|
322
|
+
dc.OPTIMIZATION_RESULT_IS_REVENUE_KPI_COLUMN,
|
|
323
|
+
dc.OPTIMIZATION_RESULT_SPEND_COLUMN,
|
|
324
|
+
dc.OPTIMIZATION_RESULT_SPEND_SHARE_COLUMN,
|
|
325
|
+
dc.OPTIMIZATION_RESULT_EFFECTIVENESS_COLUMN,
|
|
326
|
+
dc.OPTIMIZATION_RESULT_ROI_COLUMN,
|
|
327
|
+
dc.OPTIMIZATION_RESULT_MROI_COLUMN,
|
|
328
|
+
dc.OPTIMIZATION_RESULT_CPC_COLUMN,
|
|
329
|
+
],
|
|
330
|
+
),
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class BudgetOptimizationResponseCurvesConverter(_BudgetOptimizationConverter):
|
|
335
|
+
"""Outputs a table of budget optimization response curves.
|
|
336
|
+
|
|
337
|
+
When called, this converter returns a data frame with the columns:
|
|
338
|
+
|
|
339
|
+
* "Group ID"
|
|
340
|
+
A UUID generated for a budget optimization result present in the output.
|
|
341
|
+
* "Channel"
|
|
342
|
+
* "Spend"
|
|
343
|
+
* "Incremental Outcome"
|
|
344
|
+
"""
|
|
345
|
+
|
|
346
|
+
def _handle_budget_optimization_results(
|
|
347
|
+
self, results: Sequence[mmm.BudgetOptimizationResult]
|
|
348
|
+
) -> Iterator[tuple[str, pd.DataFrame]]:
|
|
349
|
+
response_curve_data = []
|
|
350
|
+
for budget_opt_result in results:
|
|
351
|
+
group_id = (
|
|
352
|
+
str(budget_opt_result.group_id) if budget_opt_result.group_id else ""
|
|
353
|
+
)
|
|
354
|
+
curves = budget_opt_result.response_curves
|
|
355
|
+
for curve in curves:
|
|
356
|
+
for spend, incremental_outcome in curve.response_points:
|
|
357
|
+
response_curve_data.append([
|
|
358
|
+
group_id,
|
|
359
|
+
curve.channel_name,
|
|
360
|
+
spend,
|
|
361
|
+
incremental_outcome,
|
|
362
|
+
])
|
|
363
|
+
|
|
364
|
+
yield (
|
|
365
|
+
dc.OPTIMIZATION_RESPONSE_CURVES,
|
|
366
|
+
pd.DataFrame(
|
|
367
|
+
response_curve_data,
|
|
368
|
+
columns=[
|
|
369
|
+
dc.OPTIMIZATION_GROUP_ID_COLUMN,
|
|
370
|
+
dc.OPTIMIZATION_CHANNEL_COLUMN,
|
|
371
|
+
dc.OPTIMIZATION_GRID_SPEND_COLUMN,
|
|
372
|
+
dc.OPTIMIZATION_GRID_INCREMENTAL_OUTCOME_COLUMN,
|
|
373
|
+
],
|
|
374
|
+
),
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
CONVERTERS = [
|
|
379
|
+
NamedOptimizationGridConverter,
|
|
380
|
+
BudgetOptimizationSpecsConverter,
|
|
381
|
+
BudgetOptimizationResultsConverter,
|
|
382
|
+
BudgetOptimizationResponseCurvesConverter,
|
|
383
|
+
]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Copyright 2025 The Meridian Authors.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Common utility functions in this package."""
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
|
|
19
|
+
from mmm.v1.common import target_metric_pb2 as target_metric_pb
|
|
20
|
+
from scenarioplanner.converters.dataframe import constants as dc
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def map_target_metric_str(metric: target_metric_pb.TargetMetric) -> str:
|
|
24
|
+
"""Maps a TargetMetric enum to a string.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
metric: The TargetMetric enum to map.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
The string representation of the TargetMetric enum.
|
|
31
|
+
"""
|
|
32
|
+
match metric:
|
|
33
|
+
case target_metric_pb.TargetMetric.KPI:
|
|
34
|
+
return dc.OPTIMIZATION_SPEC_TARGET_METRIC_KPI
|
|
35
|
+
case target_metric_pb.TargetMetric.ROI:
|
|
36
|
+
return dc.OPTIMIZATION_SPEC_TARGET_METRIC_ROI
|
|
37
|
+
case target_metric_pb.TargetMetric.MARGINAL_ROI:
|
|
38
|
+
return dc.OPTIMIZATION_SPEC_TARGET_METRIC_MARGINAL_ROI
|
|
39
|
+
case target_metric_pb.TargetMetric.COST_PER_INCREMENTAL_KPI:
|
|
40
|
+
return dc.OPTIMIZATION_SPEC_TARGET_METRIC_CPIK
|
|
41
|
+
case _:
|
|
42
|
+
raise ValueError(f"Unsupported target metric: {metric}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _to_sheet_name_format(s: str) -> str:
|
|
46
|
+
"""Converts a string to a sheet name format.
|
|
47
|
+
|
|
48
|
+
Replace consecutive spaces with a single underscore using regex.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
s: The string to convert.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
The converted sheet name.
|
|
55
|
+
"""
|
|
56
|
+
return re.sub(r"\s+", dc.SHEET_NAME_DELIMITER, s)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def create_grid_sheet_name(prefix: str, grid_name: str) -> str:
|
|
60
|
+
"""Creates a grid sheet name with the given prefix and grid name.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
prefix: The prefix of the sheet name.
|
|
64
|
+
grid_name: The name of the grid.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The grid sheet name.
|
|
68
|
+
"""
|
|
69
|
+
grid_sheet_name = _to_sheet_name_format(grid_name)
|
|
70
|
+
sheet_prefix = _to_sheet_name_format(prefix)
|
|
71
|
+
return f"{sheet_prefix}_{grid_sheet_name}"
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Copyright 2025 The Meridian Authors.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Dataframe converter constants."""
|
|
16
|
+
|
|
17
|
+
SHEET_NAME_DELIMITER = "_"
|
|
18
|
+
|
|
19
|
+
# Special analysis aggregation tags.
|
|
20
|
+
ANALYSIS_TAG_ALL = "ALL"
|
|
21
|
+
|
|
22
|
+
# ModelFit table column names
|
|
23
|
+
MODEL_FIT = "ModelFit"
|
|
24
|
+
MODEL_FIT_TIME_COLUMN = "Time"
|
|
25
|
+
MODEL_FIT_EXPECTED_CI_LOW_COLUMN = "Expected CI Low"
|
|
26
|
+
MODEL_FIT_EXPECTED_CI_HIGH_COLUMN = "Expected CI High"
|
|
27
|
+
MODEL_FIT_EXPECTED_COLUMN = "Expected"
|
|
28
|
+
MODEL_FIT_BASELINE_COLUMN = "Baseline"
|
|
29
|
+
MODEL_FIT_ACTUAL_COLUMN = "Actual"
|
|
30
|
+
|
|
31
|
+
# ModelDiagnostics table column names
|
|
32
|
+
MODEL_DIAGNOSTICS = "ModelDiagnostics"
|
|
33
|
+
MODEL_DIAGNOSTICS_DATASET_COLUMN = "Dataset"
|
|
34
|
+
MODEL_DIAGNOSTICS_R_SQUARED_COLUMN = "R Squared"
|
|
35
|
+
MODEL_DIAGNOSTICS_MAPE_COLUMN = "MAPE"
|
|
36
|
+
MODEL_DIAGNOSTICS_WMAPE_COLUMN = "wMAPE"
|
|
37
|
+
|
|
38
|
+
# Common column names
|
|
39
|
+
ANALYSIS_PERIOD_COLUMN = "Analysis Period"
|
|
40
|
+
ANALYSIS_DATE_START_COLUMN = "Analysis Date Start"
|
|
41
|
+
ANALYSIS_DATE_END_COLUMN = "Analysis Date End"
|
|
42
|
+
|
|
43
|
+
# MediaOutcome table column names
|
|
44
|
+
MEDIA_OUTCOME = "MediaOutcome"
|
|
45
|
+
MEDIA_OUTCOME_CHANNEL_INDEX_COLUMN = "Channel Index"
|
|
46
|
+
MEDIA_OUTCOME_CHANNEL_COLUMN = "Channel"
|
|
47
|
+
MEDIA_OUTCOME_INCREMENTAL_OUTCOME_COLUMN = "Incremental Outcome"
|
|
48
|
+
MEDIA_OUTCOME_CONTRIBUTION_SHARE_COLUMN = "Contribution Share"
|
|
49
|
+
MEDIA_OUTCOME_BASELINE_PSEUDO_CHANNEL_INDEX = 0
|
|
50
|
+
MEDIA_OUTCOME_ALL_CHANNELS_PSEUDO_CHANNEL_INDEX = 1
|
|
51
|
+
MEDIA_OUTCOME_CHANNEL_INDEX = 2
|
|
52
|
+
|
|
53
|
+
# MediaSpend table column names
|
|
54
|
+
MEDIA_SPEND = "MediaSpend"
|
|
55
|
+
MEDIA_SPEND_CHANNEL_COLUMN = "Channel"
|
|
56
|
+
MEDIA_SPEND_SHARE_VALUE_COLUMN = "Share Value"
|
|
57
|
+
MEDIA_SPEND_LABEL_COLUMN = "Label"
|
|
58
|
+
# The "Label" column enums
|
|
59
|
+
MEDIA_SPEND_LABEL_SPEND_SHARE = "Spend Share"
|
|
60
|
+
MEDIA_SPEND_LABEL_REVENUE_SHARE = "Revenue Share"
|
|
61
|
+
MEDIA_SPEND_LABEL_KPI_SHARE = "KPI Share"
|
|
62
|
+
|
|
63
|
+
# MediaROI table column names
|
|
64
|
+
MEDIA_ROI = "MediaROI"
|
|
65
|
+
MEDIA_ROI_CHANNEL_COLUMN = "Channel"
|
|
66
|
+
MEDIA_ROI_SPEND_COLUMN = "Spend"
|
|
67
|
+
MEDIA_ROI_EFFECTIVENESS_COLUMN = "Effectiveness"
|
|
68
|
+
MEDIA_ROI_ROI_COLUMN = "ROI"
|
|
69
|
+
MEDIA_ROI_ROI_CI_LOW_COLUMN = "ROI CI Low"
|
|
70
|
+
MEDIA_ROI_ROI_CI_HIGH_COLUMN = "ROI CI High"
|
|
71
|
+
MEDIA_ROI_MARGINAL_ROI_COLUMN = "Marginal ROI"
|
|
72
|
+
MEDIA_ROI_IS_REVENUE_KPI_COLUMN = "Is Revenue KPI"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Shared column names among Optimization tables
|
|
76
|
+
OPTIMIZATION_GROUP_ID_COLUMN = "Group ID"
|
|
77
|
+
OPTIMIZATION_CHANNEL_COLUMN = "Channel"
|
|
78
|
+
|
|
79
|
+
# Optimization grid table column names
|
|
80
|
+
# (Table name is user-generated from the spec)
|
|
81
|
+
OPTIMIZATION_GRID_SPEND_COLUMN = "Spend"
|
|
82
|
+
OPTIMIZATION_GRID_INCREMENTAL_OUTCOME_COLUMN = "Incremental Outcome"
|
|
83
|
+
|
|
84
|
+
# R&F Optimization grid table column names
|
|
85
|
+
# (Table name is user-generated from the spec)
|
|
86
|
+
RF_OPTIMIZATION_GRID_FREQ_COLUMN = "Frequency"
|
|
87
|
+
RF_OPTIMIZATION_GRID_ROI_OUTCOME_COLUMN = "ROI"
|
|
88
|
+
|
|
89
|
+
# Budget optimization grid table name
|
|
90
|
+
OPTIMIZATION_GRID_NAME_PREFIX = "budget_opt_grid"
|
|
91
|
+
|
|
92
|
+
# R&F optimization grid table name
|
|
93
|
+
RF_OPTIMIZATION_GRID_NAME_PREFIX = "rf_opt_grid"
|
|
94
|
+
|
|
95
|
+
# Optimization spec table column names and enum values
|
|
96
|
+
OPTIMIZATION_SPECS = "budget_opt_specs"
|
|
97
|
+
OPTIMIZATION_SPEC_DATE_INTERVAL_START_COLUMN = "Date Interval Start"
|
|
98
|
+
OPTIMIZATION_SPEC_DATE_INTERVAL_END_COLUMN = "Date Interval End"
|
|
99
|
+
OPTIMIZATION_SPEC_OBJECTIVE_COLUMN = "Objective"
|
|
100
|
+
OPTIMIZATION_SPEC_SCENARIO_TYPE_COLUMN = "Scenario Type"
|
|
101
|
+
OPTIMIZATION_SPEC_SCENARIO_FIXED = "Fixed"
|
|
102
|
+
OPTIMIZATION_SPEC_SCENARIO_FLEXIBLE = "Flexible"
|
|
103
|
+
OPTIMIZATION_SPEC_INITIAL_CHANNEL_SPEND_COLUMN = "Initial Channel Spend"
|
|
104
|
+
OPTIMIZATION_SPEC_TARGET_METRIC_CONSTRAINT_COLUMN = "Target Metric Constraint"
|
|
105
|
+
OPTIMIZATION_SPEC_TARGET_METRIC_KPI = "KPI"
|
|
106
|
+
OPTIMIZATION_SPEC_TARGET_METRIC_ROI = "ROI"
|
|
107
|
+
OPTIMIZATION_SPEC_TARGET_METRIC_MARGINAL_ROI = "Marginal ROI"
|
|
108
|
+
OPTIMIZATION_SPEC_TARGET_METRIC_CPIK = "Cost per Incremental KPI"
|
|
109
|
+
OPTIMIZATION_SPEC_TARGET_METRIC_VALUE_COLUMN = "Target Metric Value"
|
|
110
|
+
OPTIMIZATION_SPEC_CHANNEL_COLUMN = "Channel"
|
|
111
|
+
OPTIMIZATION_SPEC_CHANNEL_SPEND_MIN_COLUMN = "Channel Spend Min"
|
|
112
|
+
OPTIMIZATION_SPEC_CHANNEL_SPEND_MAX_COLUMN = "Channel Spend Max"
|
|
113
|
+
|
|
114
|
+
# R&F Optimization spec table column names and enum values
|
|
115
|
+
RF_OPTIMIZATION_SPECS = "rf_opt_specs"
|
|
116
|
+
RF_OPTIMIZATION_SPEC_CHANNEL_FREQUENCY_MIN_COLUMN = "Channel Frequency Min"
|
|
117
|
+
RF_OPTIMIZATION_SPEC_CHANNEL_FREQUENCY_MAX_COLUMN = "Channel Frequency Max"
|
|
118
|
+
|
|
119
|
+
# Optimization results table column names
|
|
120
|
+
OPTIMIZATION_RESULTS = "budget_opt_results"
|
|
121
|
+
OPTIMIZATION_RESULT_SPEND_COLUMN = "Optimal Spend"
|
|
122
|
+
OPTIMIZATION_RESULT_SPEND_SHARE_COLUMN = "Optimal Spend Share"
|
|
123
|
+
OPTIMIZATION_RESULT_EFFECTIVENESS_COLUMN = "Optimal Impression Effectiveness"
|
|
124
|
+
OPTIMIZATION_RESULT_ROI_COLUMN = "Optimal ROI"
|
|
125
|
+
OPTIMIZATION_RESULT_MROI_COLUMN = "Optimal mROI"
|
|
126
|
+
OPTIMIZATION_RESULT_CPC_COLUMN = "Optimal CPC"
|
|
127
|
+
OPTIMIZATION_RESULT_IS_REVENUE_KPI_COLUMN = "Is Revenue KPI"
|
|
128
|
+
|
|
129
|
+
# R&F Optimization results table column names
|
|
130
|
+
RF_OPTIMIZATION_RESULTS = "rf_opt_results"
|
|
131
|
+
RF_OPTIMIZATION_RESULT_INITIAL_SPEND_COLUMN = "Initial Spend"
|
|
132
|
+
RF_OPTIMIZATION_RESULT_AVG_FREQ_COLUMN = "Optimal Avg Frequency"
|
|
133
|
+
|
|
134
|
+
# Optimization results' response curves table column names
|
|
135
|
+
OPTIMIZATION_RESPONSE_CURVES = "response_curves"
|
|
136
|
+
OPTIMIZATION_RESPONSE_CURVE_SPEND_COLUMN = "Spend"
|
|
137
|
+
OPTIMIZATION_RESPONSE_CURVE_INCREMENTAL_OUTCOME_COLUMN = "Incremental Outcome"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Copyright 2025 The Meridian Authors.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""`Converter` class for all dataframe converters."""
|
|
16
|
+
|
|
17
|
+
import abc
|
|
18
|
+
from collections.abc import Iterator
|
|
19
|
+
|
|
20
|
+
from scenarioplanner.converters import mmm
|
|
21
|
+
import pandas as pd
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = ["Converter"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Converter(abc.ABC):
|
|
28
|
+
"""Converts a trained model and analyses to one or more data frame tables.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
mmm: An `Mmm` proto wrapper.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
mmm_wrapper: mmm.Mmm,
|
|
37
|
+
):
|
|
38
|
+
self._mmm = mmm_wrapper
|
|
39
|
+
|
|
40
|
+
@abc.abstractmethod
|
|
41
|
+
def __call__(self) -> Iterator[tuple[str, pd.DataFrame]]:
|
|
42
|
+
raise NotImplementedError()
|