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,714 @@
|
|
|
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
|
+
"""Shared test data."""
|
|
16
|
+
|
|
17
|
+
from collections.abc import Iterator, Sequence
|
|
18
|
+
|
|
19
|
+
from meridian import constants as c
|
|
20
|
+
from mmm.v1.common import date_interval_pb2 as date_interval_pb
|
|
21
|
+
from mmm.v1.common import estimate_pb2 as estimate_pb
|
|
22
|
+
from mmm.v1.common import kpi_type_pb2 as kpi_type_pb
|
|
23
|
+
from mmm.v1.common import target_metric_pb2 as target_metric_pb
|
|
24
|
+
from mmm.v1.fit import model_fit_pb2 as fit_pb
|
|
25
|
+
from mmm.v1.marketing import marketing_data_pb2 as marketing_data_pb
|
|
26
|
+
from mmm.v1.marketing.analysis import marketing_analysis_pb2 as marketing_pb
|
|
27
|
+
from mmm.v1.marketing.analysis import media_analysis_pb2 as media_pb
|
|
28
|
+
from mmm.v1.marketing.analysis import non_media_analysis_pb2 as non_media_pb
|
|
29
|
+
from mmm.v1.marketing.analysis import outcome_pb2 as outcome_pb
|
|
30
|
+
from mmm.v1.marketing.analysis import response_curve_pb2 as response_curve_pb
|
|
31
|
+
from mmm.v1.marketing.optimization import budget_optimization_pb2 as budget_pb
|
|
32
|
+
from mmm.v1.marketing.optimization import constraints_pb2 as constraints_pb
|
|
33
|
+
from mmm.v1.marketing.optimization import reach_frequency_optimization_pb2 as rf_pb
|
|
34
|
+
from scenarioplanner.converters.dataframe import constants as cc
|
|
35
|
+
|
|
36
|
+
from google.type import date_pb2 as date_pb
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
DATES = [
|
|
40
|
+
date_pb.Date(year=2024, month=1, day=1),
|
|
41
|
+
date_pb.Date(year=2024, month=1, day=8),
|
|
42
|
+
date_pb.Date(year=2024, month=1, day=15),
|
|
43
|
+
]
|
|
44
|
+
DATE_INTERVALS = [
|
|
45
|
+
date_interval_pb.DateInterval(
|
|
46
|
+
start_date=DATES[0],
|
|
47
|
+
end_date=DATES[1],
|
|
48
|
+
tag="Week1",
|
|
49
|
+
),
|
|
50
|
+
date_interval_pb.DateInterval(
|
|
51
|
+
start_date=DATES[1],
|
|
52
|
+
end_date=DATES[2],
|
|
53
|
+
tag="Week2",
|
|
54
|
+
),
|
|
55
|
+
]
|
|
56
|
+
ALL_DATE_INTERVAL = date_interval_pb.DateInterval(
|
|
57
|
+
start_date=DATES[0],
|
|
58
|
+
end_date=DATES[2],
|
|
59
|
+
tag=cc.ANALYSIS_TAG_ALL,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
GEO_INFOS = [
|
|
64
|
+
marketing_data_pb.GeoInfo(
|
|
65
|
+
geo_id="geo-1",
|
|
66
|
+
population=100,
|
|
67
|
+
),
|
|
68
|
+
marketing_data_pb.GeoInfo(
|
|
69
|
+
geo_id="geo-2",
|
|
70
|
+
population=200,
|
|
71
|
+
),
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
MEDIA_CHANNELS = [
|
|
76
|
+
"Channel 1",
|
|
77
|
+
"Channel 2",
|
|
78
|
+
]
|
|
79
|
+
RF_CHANNELS = [
|
|
80
|
+
"RF Channel 1",
|
|
81
|
+
"RF Channel 2",
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
BASE_MEDIA_SPEND = 100.0
|
|
86
|
+
BASE_RF_MEDIA_SPEND = 110.0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _create_marketing_data(
|
|
90
|
+
create_rf_data: bool = True,
|
|
91
|
+
) -> Iterator[marketing_data_pb.MarketingDataPoint]:
|
|
92
|
+
"""Generator for default `MarketingDataPoint`s for each geo and date interval defined above."""
|
|
93
|
+
for geo_info in GEO_INFOS:
|
|
94
|
+
for date_interval in DATE_INTERVALS:
|
|
95
|
+
media_vars = []
|
|
96
|
+
rf_vars = []
|
|
97
|
+
for channel in MEDIA_CHANNELS:
|
|
98
|
+
media_var = marketing_data_pb.MediaVariable(
|
|
99
|
+
channel_name=channel,
|
|
100
|
+
# For simplicity, set all media spend to be the same across all
|
|
101
|
+
# channels and across all geo and time dimensions.
|
|
102
|
+
# Add function parameters if more sophisticated test data
|
|
103
|
+
# generator is warranted here.
|
|
104
|
+
media_spend=BASE_MEDIA_SPEND,
|
|
105
|
+
)
|
|
106
|
+
media_vars.append(media_var)
|
|
107
|
+
if create_rf_data:
|
|
108
|
+
for channel in RF_CHANNELS:
|
|
109
|
+
rf_media_var = marketing_data_pb.ReachFrequencyVariable(
|
|
110
|
+
channel_name=channel,
|
|
111
|
+
spend=BASE_RF_MEDIA_SPEND,
|
|
112
|
+
reach=10_000,
|
|
113
|
+
average_frequency=1.1,
|
|
114
|
+
)
|
|
115
|
+
rf_vars.append(rf_media_var)
|
|
116
|
+
yield marketing_data_pb.MarketingDataPoint(
|
|
117
|
+
date_interval=date_interval,
|
|
118
|
+
geo_info=geo_info,
|
|
119
|
+
media_variables=media_vars,
|
|
120
|
+
reach_frequency_variables=rf_vars,
|
|
121
|
+
# `kpi` and `control_variables` fields are not set, since no test
|
|
122
|
+
# needs it just yet. Fill them in when needed.
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
MARKETING_DATA = marketing_data_pb.MarketingData(
|
|
127
|
+
marketing_data_points=list(_create_marketing_data()),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
PERFORMANCE_TEST = fit_pb.Performance(
|
|
132
|
+
r_squared=0.99,
|
|
133
|
+
mape=67.7,
|
|
134
|
+
weighted_mape=59.8,
|
|
135
|
+
rmse=55.05,
|
|
136
|
+
)
|
|
137
|
+
PERFORMANCE_TRAIN = fit_pb.Performance(
|
|
138
|
+
r_squared=0.91,
|
|
139
|
+
mape=60.6,
|
|
140
|
+
weighted_mape=55.5,
|
|
141
|
+
rmse=59.87,
|
|
142
|
+
)
|
|
143
|
+
PERFORMANCE_ALL_DATA = fit_pb.Performance(
|
|
144
|
+
r_squared=0.94,
|
|
145
|
+
mape=60.0,
|
|
146
|
+
weighted_mape=55.4,
|
|
147
|
+
rmse=52.0,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _create_model_fit_result(
|
|
152
|
+
name: str,
|
|
153
|
+
performance: fit_pb.Performance,
|
|
154
|
+
) -> fit_pb.Result:
|
|
155
|
+
return fit_pb.Result(
|
|
156
|
+
name=name,
|
|
157
|
+
performance=performance,
|
|
158
|
+
predictions=[
|
|
159
|
+
fit_pb.Prediction(
|
|
160
|
+
date_interval=DATE_INTERVALS[0],
|
|
161
|
+
predicted_outcome=estimate_pb.Estimate(
|
|
162
|
+
value=100.0,
|
|
163
|
+
uncertainties=[
|
|
164
|
+
estimate_pb.Estimate.Uncertainty(
|
|
165
|
+
probability=0.9,
|
|
166
|
+
lowerbound=90.0,
|
|
167
|
+
upperbound=110.0,
|
|
168
|
+
)
|
|
169
|
+
],
|
|
170
|
+
),
|
|
171
|
+
predicted_baseline=estimate_pb.Estimate(
|
|
172
|
+
value=90.0,
|
|
173
|
+
uncertainties=[
|
|
174
|
+
estimate_pb.Estimate.Uncertainty(
|
|
175
|
+
probability=0.9,
|
|
176
|
+
lowerbound=89.0,
|
|
177
|
+
upperbound=111.0,
|
|
178
|
+
)
|
|
179
|
+
],
|
|
180
|
+
),
|
|
181
|
+
actual_value=105.0,
|
|
182
|
+
),
|
|
183
|
+
fit_pb.Prediction(
|
|
184
|
+
date_interval=DATE_INTERVALS[1],
|
|
185
|
+
predicted_outcome=estimate_pb.Estimate(
|
|
186
|
+
value=110.0,
|
|
187
|
+
uncertainties=[
|
|
188
|
+
estimate_pb.Estimate.Uncertainty(
|
|
189
|
+
probability=0.9,
|
|
190
|
+
lowerbound=100.0,
|
|
191
|
+
upperbound=120.0,
|
|
192
|
+
)
|
|
193
|
+
],
|
|
194
|
+
),
|
|
195
|
+
predicted_baseline=estimate_pb.Estimate(
|
|
196
|
+
value=109.0,
|
|
197
|
+
uncertainties=[
|
|
198
|
+
estimate_pb.Estimate.Uncertainty(
|
|
199
|
+
probability=0.9,
|
|
200
|
+
lowerbound=90.0,
|
|
201
|
+
upperbound=125.0,
|
|
202
|
+
)
|
|
203
|
+
],
|
|
204
|
+
),
|
|
205
|
+
actual_value=115.0,
|
|
206
|
+
),
|
|
207
|
+
],
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
MODEL_FIT_RESULT_TEST = _create_model_fit_result(
|
|
212
|
+
name=c.TEST,
|
|
213
|
+
performance=PERFORMANCE_TEST,
|
|
214
|
+
)
|
|
215
|
+
MODEL_FIT_RESULT_TRAIN = _create_model_fit_result(
|
|
216
|
+
name=c.TRAIN,
|
|
217
|
+
performance=PERFORMANCE_TRAIN,
|
|
218
|
+
)
|
|
219
|
+
MODEL_FIT_RESULT_ALL_DATA = _create_model_fit_result(
|
|
220
|
+
name=c.ALL_DATA,
|
|
221
|
+
performance=PERFORMANCE_ALL_DATA,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def create_outcome(
|
|
226
|
+
incremental_outcome: float,
|
|
227
|
+
pct_of_contribution: float,
|
|
228
|
+
effectiveness: float,
|
|
229
|
+
roi: float,
|
|
230
|
+
mroi: float,
|
|
231
|
+
cpik: float,
|
|
232
|
+
is_revenue_type: bool,
|
|
233
|
+
) -> outcome_pb.Outcome:
|
|
234
|
+
return outcome_pb.Outcome(
|
|
235
|
+
kpi_type=(
|
|
236
|
+
kpi_type_pb.REVENUE if is_revenue_type else kpi_type_pb.NON_REVENUE
|
|
237
|
+
),
|
|
238
|
+
contribution=outcome_pb.Contribution(
|
|
239
|
+
value=estimate_pb.Estimate(value=incremental_outcome),
|
|
240
|
+
share=estimate_pb.Estimate(value=pct_of_contribution),
|
|
241
|
+
),
|
|
242
|
+
effectiveness=outcome_pb.Effectiveness(
|
|
243
|
+
media_unit=c.IMPRESSIONS,
|
|
244
|
+
value=estimate_pb.Estimate(value=effectiveness),
|
|
245
|
+
),
|
|
246
|
+
roi=estimate_pb.Estimate(
|
|
247
|
+
value=roi,
|
|
248
|
+
uncertainties=[
|
|
249
|
+
estimate_pb.Estimate.Uncertainty(
|
|
250
|
+
probability=0.9,
|
|
251
|
+
lowerbound=roi * 0.9,
|
|
252
|
+
upperbound=roi * 1.1,
|
|
253
|
+
)
|
|
254
|
+
],
|
|
255
|
+
),
|
|
256
|
+
marginal_roi=estimate_pb.Estimate(value=mroi),
|
|
257
|
+
cost_per_contribution=estimate_pb.Estimate(
|
|
258
|
+
value=cpik,
|
|
259
|
+
uncertainties=[
|
|
260
|
+
estimate_pb.Estimate.Uncertainty(
|
|
261
|
+
probability=0.9,
|
|
262
|
+
lowerbound=cpik * 0.9,
|
|
263
|
+
upperbound=cpik * 1.1,
|
|
264
|
+
)
|
|
265
|
+
],
|
|
266
|
+
),
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
REVENUE_OUTCOME = create_outcome(
|
|
271
|
+
incremental_outcome=100.0,
|
|
272
|
+
pct_of_contribution=0.1,
|
|
273
|
+
effectiveness=3.3,
|
|
274
|
+
roi=1.0,
|
|
275
|
+
mroi=10.0,
|
|
276
|
+
cpik=5.0,
|
|
277
|
+
is_revenue_type=True,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
NON_REVENUE_OUTCOME = create_outcome(
|
|
281
|
+
incremental_outcome=100.0,
|
|
282
|
+
pct_of_contribution=0.1,
|
|
283
|
+
effectiveness=4.4,
|
|
284
|
+
roi=10.0,
|
|
285
|
+
mroi=100.0,
|
|
286
|
+
cpik=100.0,
|
|
287
|
+
is_revenue_type=False,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
SPENDS = {
|
|
292
|
+
MEDIA_CHANNELS[0]: 75_000,
|
|
293
|
+
MEDIA_CHANNELS[1]: 25_000,
|
|
294
|
+
RF_CHANNELS[0]: 30_000,
|
|
295
|
+
RF_CHANNELS[1]: 20_000,
|
|
296
|
+
}
|
|
297
|
+
TOTAL_SPEND = sum(SPENDS.values())
|
|
298
|
+
SPENDS[c.ALL_CHANNELS] = TOTAL_SPEND
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def create_media_analysis(
|
|
302
|
+
channel: str,
|
|
303
|
+
multiplier: float = 1.0,
|
|
304
|
+
make_revenue_outcome: bool = True,
|
|
305
|
+
make_non_revenue_outcome: bool = True,
|
|
306
|
+
) -> media_pb.MediaAnalysis:
|
|
307
|
+
"""Creates a `MediaAnalysis` test proto."""
|
|
308
|
+
# `multiplier` is used to create unique metric numbers for the given channel
|
|
309
|
+
# from the base template metrics above.
|
|
310
|
+
outcomes = []
|
|
311
|
+
if make_revenue_outcome:
|
|
312
|
+
outcomes.append(
|
|
313
|
+
create_outcome(
|
|
314
|
+
incremental_outcome=100.0 * multiplier,
|
|
315
|
+
pct_of_contribution=0.1 * multiplier,
|
|
316
|
+
effectiveness=2.2 * multiplier,
|
|
317
|
+
roi=1.0 * multiplier,
|
|
318
|
+
mroi=10.0 * multiplier,
|
|
319
|
+
cpik=5.0 * multiplier,
|
|
320
|
+
is_revenue_type=True,
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
if make_non_revenue_outcome:
|
|
324
|
+
outcomes.append(
|
|
325
|
+
create_outcome(
|
|
326
|
+
incremental_outcome=100.0 * multiplier,
|
|
327
|
+
pct_of_contribution=0.1 * multiplier,
|
|
328
|
+
effectiveness=5.5 * multiplier,
|
|
329
|
+
roi=10.0 * multiplier,
|
|
330
|
+
mroi=100.0 * multiplier,
|
|
331
|
+
cpik=100.0 * multiplier,
|
|
332
|
+
is_revenue_type=False,
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
response_curve = response_curve_pb.ResponseCurve(
|
|
337
|
+
input_name="Spend",
|
|
338
|
+
response_points=[
|
|
339
|
+
response_curve_pb.ResponsePoint(
|
|
340
|
+
input_value=1 * multiplier,
|
|
341
|
+
incremental_kpi=100.0 * multiplier,
|
|
342
|
+
),
|
|
343
|
+
response_curve_pb.ResponsePoint(
|
|
344
|
+
input_value=2 * multiplier,
|
|
345
|
+
incremental_kpi=200.0 * multiplier,
|
|
346
|
+
),
|
|
347
|
+
],
|
|
348
|
+
)
|
|
349
|
+
return media_pb.MediaAnalysis(
|
|
350
|
+
channel_name=channel,
|
|
351
|
+
spend_info=media_pb.SpendInfo(
|
|
352
|
+
spend=SPENDS[channel],
|
|
353
|
+
spend_share=SPENDS[channel] / TOTAL_SPEND,
|
|
354
|
+
),
|
|
355
|
+
media_outcomes=outcomes,
|
|
356
|
+
response_curve=response_curve,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
MEDIA_ANALYSES_BOTH_OUTCOMES = [
|
|
361
|
+
create_media_analysis(channel, (idx + 1))
|
|
362
|
+
for (idx, channel) in enumerate(MEDIA_CHANNELS)
|
|
363
|
+
]
|
|
364
|
+
RF_ANALYSES_BOTH_OUTCOMES = [
|
|
365
|
+
create_media_analysis(channel, (idx + 1))
|
|
366
|
+
for (idx, channel) in enumerate(RF_CHANNELS)
|
|
367
|
+
]
|
|
368
|
+
MEDIA_ANALYSES_NONREVENUE = [
|
|
369
|
+
create_media_analysis(
|
|
370
|
+
channel,
|
|
371
|
+
# use a different multiplier value to distinquish from the above
|
|
372
|
+
(idx + 1.2),
|
|
373
|
+
make_revenue_outcome=False,
|
|
374
|
+
)
|
|
375
|
+
for (idx, channel) in enumerate(MEDIA_CHANNELS)
|
|
376
|
+
]
|
|
377
|
+
RF_ANALYSES_NONREVENUE = [
|
|
378
|
+
create_media_analysis(
|
|
379
|
+
channel,
|
|
380
|
+
# use a different multiplier value to distinquish from the above
|
|
381
|
+
(idx + 1.2),
|
|
382
|
+
make_revenue_outcome=False,
|
|
383
|
+
)
|
|
384
|
+
for (idx, channel) in enumerate(RF_CHANNELS)
|
|
385
|
+
]
|
|
386
|
+
|
|
387
|
+
ALL_CHANNELS_ANALYSIS_BOTH_OUTCOMES = create_media_analysis(
|
|
388
|
+
c.ALL_CHANNELS, multiplier=10
|
|
389
|
+
)
|
|
390
|
+
ALL_CHANNELS_ANALYSIS_NONREVENUE = create_media_analysis(
|
|
391
|
+
c.ALL_CHANNELS, multiplier=12, make_revenue_outcome=False
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
BASELINE_NONREVENUE_OUTCOME = create_outcome(
|
|
395
|
+
incremental_outcome=40.0,
|
|
396
|
+
pct_of_contribution=0.04,
|
|
397
|
+
effectiveness=4.4,
|
|
398
|
+
cpik=75.0,
|
|
399
|
+
roi=7.0,
|
|
400
|
+
mroi=70.0,
|
|
401
|
+
is_revenue_type=False,
|
|
402
|
+
)
|
|
403
|
+
BASELINE_ANALYSIS_NONREVENUE = non_media_pb.NonMediaAnalysis(
|
|
404
|
+
non_media_name=c.BASELINE,
|
|
405
|
+
non_media_outcomes=[BASELINE_NONREVENUE_OUTCOME],
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
BASELINE_REVENUE_OUTCOME = create_outcome(
|
|
409
|
+
incremental_outcome=50.0,
|
|
410
|
+
pct_of_contribution=0.05,
|
|
411
|
+
effectiveness=5.5,
|
|
412
|
+
roi=1.0,
|
|
413
|
+
mroi=10.0,
|
|
414
|
+
cpik=0.5,
|
|
415
|
+
is_revenue_type=True,
|
|
416
|
+
)
|
|
417
|
+
BASELINE_ANALYSIS_REVENUE = non_media_pb.NonMediaAnalysis(
|
|
418
|
+
non_media_name=c.BASELINE,
|
|
419
|
+
non_media_outcomes=[BASELINE_REVENUE_OUTCOME],
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
BASELINE_ANALYSIS_BOTH_OUTCOMES = non_media_pb.NonMediaAnalysis(
|
|
423
|
+
non_media_name=c.BASELINE,
|
|
424
|
+
non_media_outcomes=[BASELINE_NONREVENUE_OUTCOME, BASELINE_REVENUE_OUTCOME],
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def create_marketing_analysis(
|
|
429
|
+
date_interval: date_interval_pb.DateInterval,
|
|
430
|
+
baseline_analysis: non_media_pb.NonMediaAnalysis = BASELINE_ANALYSIS_BOTH_OUTCOMES,
|
|
431
|
+
explicit_channel_analyses: Sequence[media_pb.MediaAnalysis] | None = None,
|
|
432
|
+
explicit_all_channels_analysis: media_pb.MediaAnalysis | None = None,
|
|
433
|
+
) -> marketing_pb.MarketingAnalysis:
|
|
434
|
+
"""Create a `MarketingAnalysis` for the given analysis period and tag."""
|
|
435
|
+
media_analyses = (
|
|
436
|
+
list(explicit_channel_analyses)
|
|
437
|
+
if explicit_channel_analyses
|
|
438
|
+
else (MEDIA_ANALYSES_BOTH_OUTCOMES + RF_ANALYSES_BOTH_OUTCOMES)
|
|
439
|
+
)
|
|
440
|
+
media_analyses.append(
|
|
441
|
+
explicit_all_channels_analysis
|
|
442
|
+
if explicit_all_channels_analysis
|
|
443
|
+
else ALL_CHANNELS_ANALYSIS_BOTH_OUTCOMES
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
return marketing_pb.MarketingAnalysis(
|
|
447
|
+
date_interval=date_interval,
|
|
448
|
+
non_media_analyses=[baseline_analysis],
|
|
449
|
+
media_analyses=media_analyses,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# All of the below test analyses data contain both media and R&F channels.
|
|
454
|
+
|
|
455
|
+
ALL_TAG_MARKETING_ANALYSIS_BOTH_OUTCOMES = create_marketing_analysis(
|
|
456
|
+
date_interval=ALL_DATE_INTERVAL,
|
|
457
|
+
baseline_analysis=BASELINE_ANALYSIS_BOTH_OUTCOMES,
|
|
458
|
+
)
|
|
459
|
+
ALL_TAG_MARKETING_ANALYSIS_NONREVENUE = create_marketing_analysis(
|
|
460
|
+
date_interval=ALL_DATE_INTERVAL,
|
|
461
|
+
baseline_analysis=BASELINE_ANALYSIS_NONREVENUE,
|
|
462
|
+
explicit_channel_analyses=(
|
|
463
|
+
MEDIA_ANALYSES_NONREVENUE + RF_ANALYSES_NONREVENUE
|
|
464
|
+
),
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
DATED_MARKETING_ANALYSES_BOTH_OUTCOMES = [
|
|
468
|
+
create_marketing_analysis(
|
|
469
|
+
date_interval=date_interval,
|
|
470
|
+
baseline_analysis=BASELINE_ANALYSIS_BOTH_OUTCOMES,
|
|
471
|
+
)
|
|
472
|
+
for date_interval in DATE_INTERVALS
|
|
473
|
+
]
|
|
474
|
+
DATED_MARKETING_ANALYSES_NONREVENUE = [
|
|
475
|
+
create_marketing_analysis(
|
|
476
|
+
date_interval=date_interval,
|
|
477
|
+
baseline_analysis=BASELINE_ANALYSIS_NONREVENUE,
|
|
478
|
+
explicit_channel_analyses=(
|
|
479
|
+
MEDIA_ANALYSES_NONREVENUE + RF_ANALYSES_NONREVENUE
|
|
480
|
+
),
|
|
481
|
+
)
|
|
482
|
+
for date_interval in DATE_INTERVALS
|
|
483
|
+
]
|
|
484
|
+
|
|
485
|
+
MARKETING_ANALYSIS_LIST_BOTH_OUTCOMES = marketing_pb.MarketingAnalysisList(
|
|
486
|
+
marketing_analyses=(
|
|
487
|
+
[ALL_TAG_MARKETING_ANALYSIS_BOTH_OUTCOMES]
|
|
488
|
+
+ DATED_MARKETING_ANALYSES_BOTH_OUTCOMES
|
|
489
|
+
),
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
MARKETING_ANALYSIS_LIST_NONREVENUE = marketing_pb.MarketingAnalysisList(
|
|
493
|
+
marketing_analyses=(
|
|
494
|
+
[ALL_TAG_MARKETING_ANALYSIS_NONREVENUE]
|
|
495
|
+
+ DATED_MARKETING_ANALYSES_NONREVENUE
|
|
496
|
+
),
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
# Incremental outcome grids (budget) are only relevant for non-RF media
|
|
501
|
+
# channels.
|
|
502
|
+
|
|
503
|
+
INCREMENTAL_OUTCOME_GRID_FOO = budget_pb.IncrementalOutcomeGrid(
|
|
504
|
+
name="incremental outcome grid foo",
|
|
505
|
+
channel_cells=[
|
|
506
|
+
budget_pb.IncrementalOutcomeGrid.ChannelCells(
|
|
507
|
+
channel_name=MEDIA_CHANNELS[0],
|
|
508
|
+
cells=[
|
|
509
|
+
budget_pb.IncrementalOutcomeGrid.Cell(
|
|
510
|
+
spend=10000.0,
|
|
511
|
+
incremental_outcome=estimate_pb.Estimate(value=100.0),
|
|
512
|
+
),
|
|
513
|
+
budget_pb.IncrementalOutcomeGrid.Cell(
|
|
514
|
+
spend=20000.0,
|
|
515
|
+
incremental_outcome=estimate_pb.Estimate(value=200.0),
|
|
516
|
+
),
|
|
517
|
+
],
|
|
518
|
+
),
|
|
519
|
+
budget_pb.IncrementalOutcomeGrid.ChannelCells(
|
|
520
|
+
channel_name=MEDIA_CHANNELS[1],
|
|
521
|
+
cells=[
|
|
522
|
+
budget_pb.IncrementalOutcomeGrid.Cell(
|
|
523
|
+
spend=10000.0,
|
|
524
|
+
incremental_outcome=estimate_pb.Estimate(value=100.0),
|
|
525
|
+
),
|
|
526
|
+
budget_pb.IncrementalOutcomeGrid.Cell(
|
|
527
|
+
spend=20000.0,
|
|
528
|
+
incremental_outcome=estimate_pb.Estimate(value=200.0),
|
|
529
|
+
),
|
|
530
|
+
],
|
|
531
|
+
),
|
|
532
|
+
],
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
INCREMENTAL_OUTCOME_GRID_BAR = budget_pb.IncrementalOutcomeGrid(
|
|
536
|
+
name="incremental outcome grid bar",
|
|
537
|
+
channel_cells=[
|
|
538
|
+
budget_pb.IncrementalOutcomeGrid.ChannelCells(
|
|
539
|
+
channel_name=MEDIA_CHANNELS[0],
|
|
540
|
+
cells=[
|
|
541
|
+
budget_pb.IncrementalOutcomeGrid.Cell(
|
|
542
|
+
spend=1000.0,
|
|
543
|
+
incremental_outcome=estimate_pb.Estimate(value=10.0),
|
|
544
|
+
),
|
|
545
|
+
budget_pb.IncrementalOutcomeGrid.Cell(
|
|
546
|
+
spend=2000.0,
|
|
547
|
+
incremental_outcome=estimate_pb.Estimate(value=20.0),
|
|
548
|
+
),
|
|
549
|
+
],
|
|
550
|
+
),
|
|
551
|
+
budget_pb.IncrementalOutcomeGrid.ChannelCells(
|
|
552
|
+
channel_name=MEDIA_CHANNELS[1],
|
|
553
|
+
cells=[
|
|
554
|
+
budget_pb.IncrementalOutcomeGrid.Cell(
|
|
555
|
+
spend=1000.0,
|
|
556
|
+
incremental_outcome=estimate_pb.Estimate(value=10.0),
|
|
557
|
+
),
|
|
558
|
+
budget_pb.IncrementalOutcomeGrid.Cell(
|
|
559
|
+
spend=2000.0,
|
|
560
|
+
incremental_outcome=estimate_pb.Estimate(value=20.0),
|
|
561
|
+
),
|
|
562
|
+
],
|
|
563
|
+
),
|
|
564
|
+
],
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
# A fixed budget scenario for the entire time interval in the test data above.
|
|
568
|
+
BUDGET_OPTIMIZATION_SPEC_FIXED_ALL_DATES = budget_pb.BudgetOptimizationSpec(
|
|
569
|
+
date_interval=ALL_DATE_INTERVAL,
|
|
570
|
+
objective=target_metric_pb.TargetMetric.ROI,
|
|
571
|
+
fixed_budget_scenario=budget_pb.FixedBudgetScenario(total_budget=100000.0),
|
|
572
|
+
# No individual channel constraints. Expect implicit constraints: max budget
|
|
573
|
+
# applied for each channel.
|
|
574
|
+
)
|
|
575
|
+
BUDGET_OPTIMIZATION_RESULT_FIXED_BOTH_OUTCOMES = (
|
|
576
|
+
budget_pb.BudgetOptimizationResult(
|
|
577
|
+
name="budget optimization result foo",
|
|
578
|
+
group_id="group-foo",
|
|
579
|
+
optimized_marketing_analysis=ALL_TAG_MARKETING_ANALYSIS_BOTH_OUTCOMES,
|
|
580
|
+
spec=BUDGET_OPTIMIZATION_SPEC_FIXED_ALL_DATES,
|
|
581
|
+
incremental_outcome_grid=INCREMENTAL_OUTCOME_GRID_FOO,
|
|
582
|
+
)
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
# A flexible budget scenario for the second time interval only.
|
|
586
|
+
BUDGET_OPTIMIZATION_SPEC_FLEX_SELECT_DATES = budget_pb.BudgetOptimizationSpec(
|
|
587
|
+
date_interval=DATE_INTERVALS[1],
|
|
588
|
+
objective=target_metric_pb.TargetMetric.KPI,
|
|
589
|
+
flexible_budget_scenario=budget_pb.FlexibleBudgetScenario(
|
|
590
|
+
total_budget_constraint=constraints_pb.BudgetConstraint(
|
|
591
|
+
min_budget=1000.0,
|
|
592
|
+
max_budget=2000.0,
|
|
593
|
+
),
|
|
594
|
+
target_metric_constraints=[
|
|
595
|
+
constraints_pb.TargetMetricConstraint(
|
|
596
|
+
target_metric=target_metric_pb.COST_PER_INCREMENTAL_KPI,
|
|
597
|
+
target_value=10.0,
|
|
598
|
+
)
|
|
599
|
+
],
|
|
600
|
+
),
|
|
601
|
+
# Define explicit channel constraints.
|
|
602
|
+
channel_constraints=[
|
|
603
|
+
budget_pb.ChannelConstraint(
|
|
604
|
+
channel_name=MEDIA_CHANNELS[0],
|
|
605
|
+
budget_constraint=constraints_pb.BudgetConstraint(
|
|
606
|
+
min_budget=1100.0,
|
|
607
|
+
max_budget=1500.0,
|
|
608
|
+
),
|
|
609
|
+
),
|
|
610
|
+
budget_pb.ChannelConstraint(
|
|
611
|
+
channel_name=MEDIA_CHANNELS[1],
|
|
612
|
+
budget_constraint=constraints_pb.BudgetConstraint(
|
|
613
|
+
min_budget=1000.0,
|
|
614
|
+
max_budget=1800.0,
|
|
615
|
+
),
|
|
616
|
+
),
|
|
617
|
+
],
|
|
618
|
+
)
|
|
619
|
+
BUDGET_OPTIMIZATION_RESULT_FLEX_NONREV = budget_pb.BudgetOptimizationResult(
|
|
620
|
+
name="budget optimization result bar",
|
|
621
|
+
group_id="group-bar",
|
|
622
|
+
optimized_marketing_analysis=ALL_TAG_MARKETING_ANALYSIS_NONREVENUE,
|
|
623
|
+
spec=BUDGET_OPTIMIZATION_SPEC_FLEX_SELECT_DATES,
|
|
624
|
+
incremental_outcome_grid=INCREMENTAL_OUTCOME_GRID_BAR,
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
# Frequency outcome grids are only relevant for R&F media channels.
|
|
629
|
+
|
|
630
|
+
FREQUENCY_OUTCOME_GRID_FOO = rf_pb.FrequencyOutcomeGrid(
|
|
631
|
+
name="frequency outcome grid foo",
|
|
632
|
+
channel_cells=[
|
|
633
|
+
rf_pb.FrequencyOutcomeGrid.ChannelCells(
|
|
634
|
+
channel_name=RF_CHANNELS[0],
|
|
635
|
+
cells=[
|
|
636
|
+
rf_pb.FrequencyOutcomeGrid.Cell(
|
|
637
|
+
reach_frequency=rf_pb.ReachFrequency(
|
|
638
|
+
reach=10000,
|
|
639
|
+
average_frequency=1.0,
|
|
640
|
+
),
|
|
641
|
+
outcome=estimate_pb.Estimate(value=100.0),
|
|
642
|
+
),
|
|
643
|
+
rf_pb.FrequencyOutcomeGrid.Cell(
|
|
644
|
+
reach_frequency=rf_pb.ReachFrequency(
|
|
645
|
+
reach=20000,
|
|
646
|
+
average_frequency=2.0,
|
|
647
|
+
),
|
|
648
|
+
outcome=estimate_pb.Estimate(value=200.0),
|
|
649
|
+
),
|
|
650
|
+
],
|
|
651
|
+
),
|
|
652
|
+
rf_pb.FrequencyOutcomeGrid.ChannelCells(
|
|
653
|
+
channel_name=RF_CHANNELS[1],
|
|
654
|
+
cells=[
|
|
655
|
+
rf_pb.FrequencyOutcomeGrid.Cell(
|
|
656
|
+
reach_frequency=rf_pb.ReachFrequency(
|
|
657
|
+
reach=10000,
|
|
658
|
+
average_frequency=1.0,
|
|
659
|
+
),
|
|
660
|
+
outcome=estimate_pb.Estimate(value=100.0),
|
|
661
|
+
),
|
|
662
|
+
rf_pb.FrequencyOutcomeGrid.Cell(
|
|
663
|
+
reach_frequency=rf_pb.ReachFrequency(
|
|
664
|
+
reach=20000,
|
|
665
|
+
average_frequency=2.0,
|
|
666
|
+
),
|
|
667
|
+
outcome=estimate_pb.Estimate(value=200.0),
|
|
668
|
+
),
|
|
669
|
+
],
|
|
670
|
+
),
|
|
671
|
+
],
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
RF_OPTIMIZATION_SPEC_ALL_DATES = rf_pb.ReachFrequencyOptimizationSpec(
|
|
675
|
+
date_interval=ALL_DATE_INTERVAL,
|
|
676
|
+
objective=target_metric_pb.TargetMetric.KPI,
|
|
677
|
+
total_budget_constraint=constraints_pb.BudgetConstraint(
|
|
678
|
+
min_budget=100000.0,
|
|
679
|
+
max_budget=200000.0,
|
|
680
|
+
),
|
|
681
|
+
rf_channel_constraints=[
|
|
682
|
+
rf_pb.RfChannelConstraint(
|
|
683
|
+
channel_name=RF_CHANNELS[0],
|
|
684
|
+
frequency_constraint=constraints_pb.FrequencyConstraint(
|
|
685
|
+
max_frequency=5.0,
|
|
686
|
+
),
|
|
687
|
+
),
|
|
688
|
+
rf_pb.RfChannelConstraint(
|
|
689
|
+
channel_name=RF_CHANNELS[1],
|
|
690
|
+
frequency_constraint=constraints_pb.FrequencyConstraint(
|
|
691
|
+
min_frequency=1.3,
|
|
692
|
+
max_frequency=6.6,
|
|
693
|
+
),
|
|
694
|
+
),
|
|
695
|
+
],
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
RF_OPTIMIZATION_RESULT_FOO = rf_pb.ReachFrequencyOptimizationResult(
|
|
699
|
+
name="reach frequency optimization result foo",
|
|
700
|
+
group_id="group-foo",
|
|
701
|
+
spec=RF_OPTIMIZATION_SPEC_ALL_DATES,
|
|
702
|
+
optimized_channel_frequencies=[
|
|
703
|
+
rf_pb.OptimizedChannelFrequency(
|
|
704
|
+
channel_name=RF_CHANNELS[0],
|
|
705
|
+
optimal_average_frequency=3.3,
|
|
706
|
+
),
|
|
707
|
+
rf_pb.OptimizedChannelFrequency(
|
|
708
|
+
channel_name=RF_CHANNELS[1],
|
|
709
|
+
optimal_average_frequency=5.6,
|
|
710
|
+
),
|
|
711
|
+
],
|
|
712
|
+
optimized_marketing_analysis=ALL_TAG_MARKETING_ANALYSIS_BOTH_OUTCOMES,
|
|
713
|
+
frequency_outcome_grid=FREQUENCY_OUTCOME_GRID_FOO,
|
|
714
|
+
)
|