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,314 @@
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
+ """Reach and frequency optimization output converters.
16
+
17
+ This module defines various classes that convert
18
+ `ReachFrequencyOptimizationResult`s 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 scenarioplanner.converters import mmm
26
+ from scenarioplanner.converters.dataframe import common
27
+ from scenarioplanner.converters.dataframe import constants as dc
28
+ from scenarioplanner.converters.dataframe import converter
29
+ import pandas as pd
30
+
31
+
32
+ __all__ = [
33
+ "NamedRfOptimizationGridConverter",
34
+ "RfOptimizationSpecsConverter",
35
+ "RfOptimizationResultsConverter",
36
+ ]
37
+
38
+
39
+ class _RfOptimizationConverter(converter.Converter, abc.ABC):
40
+ """An abstract class for dealing with `ReachFrequencyOptimizationResult`s."""
41
+
42
+ def __call__(self) -> Iterator[tuple[str, pd.DataFrame]]:
43
+ results = self._mmm.reach_frequency_optimization_results
44
+ if not results:
45
+ return
46
+
47
+ # Validate group IDs.
48
+ group_ids = [result.group_id for result in results if result.group_id]
49
+ if len(set(group_ids)) != len(group_ids):
50
+ raise ValueError(
51
+ "Specified group_id must be unique or unset among the given group of"
52
+ " results."
53
+ )
54
+
55
+ yield from self._handle_rf_optimization_results(
56
+ self._mmm.reach_frequency_optimization_results
57
+ )
58
+
59
+ def _handle_rf_optimization_results(
60
+ self, results: Sequence[mmm.ReachFrequencyOptimizationResult]
61
+ ) -> Iterator[tuple[str, pd.DataFrame]]:
62
+ raise NotImplementedError()
63
+
64
+
65
+ class NamedRfOptimizationGridConverter(_RfOptimizationConverter):
66
+ """Outputs named tables for Reach & Frequency optimization grids.
67
+
68
+ When called, this converter returns a data frame with the columns:
69
+
70
+ * "Group ID"
71
+ A UUID generated for each named incremental outcome grid.
72
+ * "Channel"
73
+ * "Frequency"
74
+ * "ROI"
75
+ For each named R&F optimization result in the MMM output proto.
76
+ """
77
+
78
+ def _handle_rf_optimization_results(
79
+ self, results: Sequence[mmm.ReachFrequencyOptimizationResult]
80
+ ) -> Iterator[tuple[str, pd.DataFrame]]:
81
+ for rf_opt_result in results:
82
+ # There should be one unique ID for each result.
83
+ group_id = str(rf_opt_result.group_id) if rf_opt_result.group_id else ""
84
+ grid = rf_opt_result.frequency_outcome_grid
85
+
86
+ # Each grid yields its own data frame table.
87
+ rf_optimization_grid_data = []
88
+ for channel, cells in grid.channel_frequency_grids.items():
89
+ for frequency, outcome in cells:
90
+ rf_optimization_grid_data.append([
91
+ group_id,
92
+ channel,
93
+ frequency,
94
+ outcome,
95
+ ])
96
+
97
+ yield (
98
+ common.create_grid_sheet_name(
99
+ dc.RF_OPTIMIZATION_GRID_NAME_PREFIX, grid.name
100
+ ),
101
+ pd.DataFrame(
102
+ rf_optimization_grid_data,
103
+ columns=[
104
+ dc.OPTIMIZATION_GROUP_ID_COLUMN,
105
+ dc.OPTIMIZATION_CHANNEL_COLUMN,
106
+ dc.RF_OPTIMIZATION_GRID_FREQ_COLUMN,
107
+ dc.RF_OPTIMIZATION_GRID_ROI_OUTCOME_COLUMN,
108
+ ],
109
+ ),
110
+ )
111
+
112
+
113
+ class RfOptimizationSpecsConverter(_RfOptimizationConverter):
114
+ """Outputs a table of R&F optimization specs.
115
+
116
+ When called, this converter returns a data frame with the columns:
117
+
118
+ * "Group ID"
119
+ A UUID generated for an R&F frequency outcome grid present in the output.
120
+ * "Date Interval Start"
121
+ * "Date Interval End"
122
+ * "Objective"
123
+ * "Initial Channel Spend"
124
+ * "Channel"
125
+ * "Channel Frequency Min"
126
+ * "Channel Frequency Max"
127
+ """
128
+
129
+ def _handle_rf_optimization_results(
130
+ self, results: Sequence[mmm.ReachFrequencyOptimizationResult]
131
+ ) -> Iterator[tuple[str, pd.DataFrame]]:
132
+ spec_data = []
133
+ for rf_opt_result in results:
134
+ # There should be one unique ID for each result.
135
+ group_id = str(rf_opt_result.group_id) if rf_opt_result.group_id else ""
136
+ spec = rf_opt_result.spec
137
+
138
+ objective = common.map_target_metric_str(spec.objective)
139
+ # These are the start and end dates for the requested R&F optimization in
140
+ # this spec.
141
+ date_interval_start, date_interval_end = (
142
+ d.strftime(c.DATE_FORMAT) for d in spec.date_interval.date_interval
143
+ )
144
+ rf_date_interval = (date_interval_start, date_interval_end)
145
+
146
+ # aka historical spend from marketing data in the model kernel
147
+ initial_channel_spends = self._mmm.marketing_data.rf_channel_spends(
148
+ rf_date_interval
149
+ )
150
+
151
+ # When the constraint of a channel is not specified, that channel will
152
+ # have a default frequency constraint of `[1.0, max_freq]`.
153
+ #
154
+ # NOTE: We assume that the processor has already done this max_freq
155
+ # computation. And so we can assert here that channel constraints are
156
+ # always fully specified for R&F channels.
157
+ channel_constraints = spec.channel_constraints
158
+ if not channel_constraints:
159
+ raise ValueError(
160
+ "R&F optimization spec must have channel constraints specified."
161
+ )
162
+ if set([
163
+ channel_constraint.channel_name
164
+ for channel_constraint in channel_constraints
165
+ ]) != set(self._mmm.marketing_data.rf_channels):
166
+ raise ValueError(
167
+ "R&F optimization spec must have channel constraints specified for"
168
+ " all R&F channels."
169
+ )
170
+
171
+ for channel_constraint in channel_constraints:
172
+ min_freq = channel_constraint.frequency_constraint.min_frequency or 1.0
173
+ max_freq = channel_constraint.frequency_constraint.max_frequency
174
+ if not max_freq:
175
+ raise ValueError(
176
+ "Channel constraint in R&F optimization spec must have max"
177
+ " frequency specified. Missing for channel:"
178
+ f" {channel_constraint.channel_name}"
179
+ )
180
+ spec_data.append([
181
+ group_id,
182
+ date_interval_start,
183
+ date_interval_end,
184
+ objective,
185
+ initial_channel_spends.get(channel_constraint.channel_name, 0.0),
186
+ channel_constraint.channel_name,
187
+ min_freq,
188
+ max_freq,
189
+ ])
190
+
191
+ yield (
192
+ dc.RF_OPTIMIZATION_SPECS,
193
+ pd.DataFrame(
194
+ spec_data,
195
+ columns=[
196
+ dc.OPTIMIZATION_GROUP_ID_COLUMN,
197
+ dc.OPTIMIZATION_SPEC_DATE_INTERVAL_START_COLUMN,
198
+ dc.OPTIMIZATION_SPEC_DATE_INTERVAL_END_COLUMN,
199
+ dc.OPTIMIZATION_SPEC_OBJECTIVE_COLUMN,
200
+ dc.OPTIMIZATION_SPEC_INITIAL_CHANNEL_SPEND_COLUMN,
201
+ dc.OPTIMIZATION_CHANNEL_COLUMN,
202
+ dc.RF_OPTIMIZATION_SPEC_CHANNEL_FREQUENCY_MIN_COLUMN,
203
+ dc.RF_OPTIMIZATION_SPEC_CHANNEL_FREQUENCY_MAX_COLUMN,
204
+ ],
205
+ ),
206
+ )
207
+
208
+
209
+ class RfOptimizationResultsConverter(_RfOptimizationConverter):
210
+ """Outputs a table of R&F optimization results.
211
+
212
+ When called, this converter returns a data frame with the columns:
213
+
214
+ * "Group ID"
215
+ A UUID generated for a budget optimization result present in the output.
216
+ * "Channel"
217
+ * "Is Revenue KPI"
218
+ Whether the KPI is revenue or not.
219
+ * "Initial Spend"
220
+ * "Optimal Avg Frequency"
221
+ * "Optimal Impression Effectiveness"
222
+ * "Optimal ROI"
223
+ * "Optimal mROI"
224
+ * "Optimal CPC"
225
+ """
226
+
227
+ def _handle_rf_optimization_results(
228
+ self, results: Sequence[mmm.ReachFrequencyOptimizationResult]
229
+ ) -> Iterator[tuple[str, pd.DataFrame]]:
230
+ data = []
231
+ for rf_opt_result in results:
232
+ group_id = str(rf_opt_result.group_id) if rf_opt_result.group_id else ""
233
+ marketing_analysis = rf_opt_result.optimized_marketing_analysis
234
+
235
+ spec = rf_opt_result.spec
236
+ # These are the start and end dates for the requested R&F optimization in
237
+ # this spec.
238
+ date_interval_start, date_interval_end = (
239
+ d.strftime(c.DATE_FORMAT) for d in spec.date_interval.date_interval
240
+ )
241
+ rf_date_interval = (date_interval_start, date_interval_end)
242
+ # aka historical spend from marketing data in the model kernel
243
+ initial_budget = self._mmm.marketing_data.rf_channel_spends(
244
+ rf_date_interval
245
+ )
246
+
247
+ media_channel_analyses = marketing_analysis.channel_mapped_media_analyses
248
+ for channel, media_analysis in media_channel_analyses.items():
249
+ # Skip "All Channels" pseudo-channel.
250
+ if channel == c.ALL_CHANNELS:
251
+ continue
252
+ # Skip non-R&F channels.
253
+ if channel not in self._mmm.marketing_data.rf_channels:
254
+ continue
255
+
256
+ initial_spend = initial_budget[channel]
257
+ optimal_avg_freq = rf_opt_result.channel_mapped_optimized_frequencies[
258
+ channel
259
+ ]
260
+
261
+ revenue_outcome = media_analysis.maybe_revenue_outcome
262
+ nonrevenue_outcome = media_analysis.maybe_non_revenue_outcome
263
+
264
+ # pylint: disable=cell-var-from-loop
265
+ def _append_outcome_data(
266
+ outcome: mmm.Outcome | None,
267
+ is_revenue_kpi: bool,
268
+ ) -> None:
269
+ if outcome is None:
270
+ return
271
+ effectiveness = outcome.effectiveness_pb.value.value
272
+ roi = outcome.roi_pb.value
273
+ mroi = outcome.marginal_roi_pb.value
274
+ cpc = outcome.cost_per_contribution_pb.value
275
+ data.append([
276
+ group_id,
277
+ channel,
278
+ is_revenue_kpi,
279
+ initial_spend,
280
+ optimal_avg_freq,
281
+ effectiveness,
282
+ roi,
283
+ mroi,
284
+ cpc,
285
+ ])
286
+
287
+ _append_outcome_data(revenue_outcome, True)
288
+ _append_outcome_data(nonrevenue_outcome, False)
289
+ # pylint: enable=cell-var-from-loop
290
+
291
+ yield (
292
+ dc.RF_OPTIMIZATION_RESULTS,
293
+ pd.DataFrame(
294
+ data,
295
+ columns=[
296
+ dc.OPTIMIZATION_GROUP_ID_COLUMN,
297
+ dc.OPTIMIZATION_CHANNEL_COLUMN,
298
+ dc.OPTIMIZATION_RESULT_IS_REVENUE_KPI_COLUMN,
299
+ dc.RF_OPTIMIZATION_RESULT_INITIAL_SPEND_COLUMN,
300
+ dc.RF_OPTIMIZATION_RESULT_AVG_FREQ_COLUMN,
301
+ dc.OPTIMIZATION_RESULT_EFFECTIVENESS_COLUMN,
302
+ dc.OPTIMIZATION_RESULT_ROI_COLUMN,
303
+ dc.OPTIMIZATION_RESULT_MROI_COLUMN,
304
+ dc.OPTIMIZATION_RESULT_CPC_COLUMN,
305
+ ],
306
+ ),
307
+ )
308
+
309
+
310
+ CONVERTERS = [
311
+ NamedRfOptimizationGridConverter,
312
+ RfOptimizationSpecsConverter,
313
+ RfOptimizationResultsConverter,
314
+ ]