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.
Files changed (74) hide show
  1. {google_meridian-1.3.1.dist-info → google_meridian-1.4.0.dist-info}/METADATA +13 -9
  2. google_meridian-1.4.0.dist-info/RECORD +108 -0
  3. {google_meridian-1.3.1.dist-info → google_meridian-1.4.0.dist-info}/top_level.txt +1 -0
  4. meridian/analysis/__init__.py +1 -2
  5. meridian/analysis/analyzer.py +0 -1
  6. meridian/analysis/optimizer.py +5 -3
  7. meridian/analysis/review/checks.py +81 -30
  8. meridian/analysis/review/constants.py +4 -0
  9. meridian/analysis/review/results.py +40 -9
  10. meridian/analysis/summarizer.py +8 -3
  11. meridian/analysis/test_utils.py +934 -485
  12. meridian/analysis/visualizer.py +11 -7
  13. meridian/backend/__init__.py +53 -5
  14. meridian/backend/test_utils.py +72 -0
  15. meridian/constants.py +2 -0
  16. meridian/data/load.py +2 -0
  17. meridian/data/test_utils.py +82 -10
  18. meridian/model/__init__.py +2 -0
  19. meridian/model/context.py +925 -0
  20. meridian/model/eda/__init__.py +0 -1
  21. meridian/model/eda/constants.py +13 -2
  22. meridian/model/eda/eda_engine.py +299 -37
  23. meridian/model/eda/eda_outcome.py +21 -1
  24. meridian/model/equations.py +418 -0
  25. meridian/model/knots.py +75 -47
  26. meridian/model/model.py +93 -792
  27. meridian/{analysis/templates → templates}/card.html.jinja +1 -1
  28. meridian/{analysis/templates → templates}/chart.html.jinja +1 -1
  29. meridian/{analysis/templates → templates}/chips.html.jinja +1 -1
  30. meridian/{analysis → templates}/formatter.py +12 -1
  31. meridian/templates/formatter_test.py +216 -0
  32. meridian/{analysis/templates → templates}/insights.html.jinja +1 -1
  33. meridian/{analysis/templates → templates}/stats.html.jinja +1 -1
  34. meridian/{analysis/templates → templates}/style.css +1 -1
  35. meridian/{analysis/templates → templates}/style.scss +1 -1
  36. meridian/{analysis/templates → templates}/summary.html.jinja +4 -2
  37. meridian/{analysis/templates → templates}/table.html.jinja +1 -1
  38. meridian/version.py +1 -1
  39. scenarioplanner/__init__.py +42 -0
  40. scenarioplanner/converters/__init__.py +25 -0
  41. scenarioplanner/converters/dataframe/__init__.py +28 -0
  42. scenarioplanner/converters/dataframe/budget_opt_converters.py +383 -0
  43. scenarioplanner/converters/dataframe/common.py +71 -0
  44. scenarioplanner/converters/dataframe/constants.py +137 -0
  45. scenarioplanner/converters/dataframe/converter.py +42 -0
  46. scenarioplanner/converters/dataframe/dataframe_model_converter.py +70 -0
  47. scenarioplanner/converters/dataframe/marketing_analyses_converters.py +543 -0
  48. scenarioplanner/converters/dataframe/rf_opt_converters.py +314 -0
  49. scenarioplanner/converters/mmm.py +743 -0
  50. scenarioplanner/converters/mmm_converter.py +58 -0
  51. scenarioplanner/converters/sheets.py +156 -0
  52. scenarioplanner/converters/test_data.py +714 -0
  53. scenarioplanner/linkingapi/__init__.py +47 -0
  54. scenarioplanner/linkingapi/constants.py +27 -0
  55. scenarioplanner/linkingapi/url_generator.py +131 -0
  56. scenarioplanner/mmm_ui_proto_generator.py +354 -0
  57. schema/__init__.py +15 -0
  58. schema/mmm_proto_generator.py +71 -0
  59. schema/model_consumer.py +133 -0
  60. schema/processors/__init__.py +77 -0
  61. schema/processors/budget_optimization_processor.py +832 -0
  62. schema/processors/common.py +64 -0
  63. schema/processors/marketing_processor.py +1136 -0
  64. schema/processors/model_fit_processor.py +367 -0
  65. schema/processors/model_kernel_processor.py +117 -0
  66. schema/processors/model_processor.py +412 -0
  67. schema/processors/reach_frequency_optimization_processor.py +584 -0
  68. schema/test_data.py +380 -0
  69. schema/utils/__init__.py +1 -0
  70. schema/utils/date_range_bucketing.py +117 -0
  71. google_meridian-1.3.1.dist-info/RECORD +0 -76
  72. meridian/model/eda/meridian_eda.py +0 -220
  73. {google_meridian-1.3.1.dist-info → google_meridian-1.4.0.dist-info}/WHEEL +0 -0
  74. {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
+ )