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,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()