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,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
|
+
]
|