google-meridian 1.3.2__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 (49) hide show
  1. {google_meridian-1.3.2.dist-info → google_meridian-1.4.0.dist-info}/METADATA +8 -4
  2. {google_meridian-1.3.2.dist-info → google_meridian-1.4.0.dist-info}/RECORD +49 -17
  3. {google_meridian-1.3.2.dist-info → google_meridian-1.4.0.dist-info}/top_level.txt +1 -0
  4. meridian/analysis/summarizer.py +7 -2
  5. meridian/analysis/test_utils.py +934 -485
  6. meridian/analysis/visualizer.py +10 -6
  7. meridian/constants.py +1 -0
  8. meridian/data/test_utils.py +82 -10
  9. meridian/model/__init__.py +2 -0
  10. meridian/model/context.py +925 -0
  11. meridian/model/eda/constants.py +1 -0
  12. meridian/model/equations.py +418 -0
  13. meridian/model/knots.py +58 -47
  14. meridian/model/model.py +93 -792
  15. meridian/version.py +1 -1
  16. scenarioplanner/__init__.py +42 -0
  17. scenarioplanner/converters/__init__.py +25 -0
  18. scenarioplanner/converters/dataframe/__init__.py +28 -0
  19. scenarioplanner/converters/dataframe/budget_opt_converters.py +383 -0
  20. scenarioplanner/converters/dataframe/common.py +71 -0
  21. scenarioplanner/converters/dataframe/constants.py +137 -0
  22. scenarioplanner/converters/dataframe/converter.py +42 -0
  23. scenarioplanner/converters/dataframe/dataframe_model_converter.py +70 -0
  24. scenarioplanner/converters/dataframe/marketing_analyses_converters.py +543 -0
  25. scenarioplanner/converters/dataframe/rf_opt_converters.py +314 -0
  26. scenarioplanner/converters/mmm.py +743 -0
  27. scenarioplanner/converters/mmm_converter.py +58 -0
  28. scenarioplanner/converters/sheets.py +156 -0
  29. scenarioplanner/converters/test_data.py +714 -0
  30. scenarioplanner/linkingapi/__init__.py +47 -0
  31. scenarioplanner/linkingapi/constants.py +27 -0
  32. scenarioplanner/linkingapi/url_generator.py +131 -0
  33. scenarioplanner/mmm_ui_proto_generator.py +354 -0
  34. schema/__init__.py +5 -2
  35. schema/mmm_proto_generator.py +71 -0
  36. schema/model_consumer.py +133 -0
  37. schema/processors/__init__.py +77 -0
  38. schema/processors/budget_optimization_processor.py +832 -0
  39. schema/processors/common.py +64 -0
  40. schema/processors/marketing_processor.py +1136 -0
  41. schema/processors/model_fit_processor.py +367 -0
  42. schema/processors/model_kernel_processor.py +117 -0
  43. schema/processors/model_processor.py +412 -0
  44. schema/processors/reach_frequency_optimization_processor.py +584 -0
  45. schema/test_data.py +380 -0
  46. schema/utils/__init__.py +1 -0
  47. schema/utils/date_range_bucketing.py +117 -0
  48. {google_meridian-1.3.2.dist-info → google_meridian-1.4.0.dist-info}/WHEEL +0 -0
  49. {google_meridian-1.3.2.dist-info → google_meridian-1.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,47 @@
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
+ """Builds Looker Studio report URLs.
16
+
17
+ This package provides tools to construct URLs for Looker Studio reports that
18
+ embed data directly within the URL itself. This is achieved through the creation
19
+ of shareable, pre-configured reports without requiring a separate, pre-existing
20
+ data source.
21
+
22
+ The primary functionality is exposed through the `url_generator` module.
23
+
24
+ Typical Usage:
25
+
26
+ 1. Use `url_generator.create_report_url()` to create the complete URL, based
27
+ on a `sheets.Spreadsheet` object.
28
+
29
+ Example:
30
+
31
+ ```python
32
+ from lookerstudio.linkingapi import url_generator
33
+ from lookerstudio.converters import sheets
34
+
35
+ # Generate the URL
36
+ looker_studio_report_url = url_generator.create_report_url(
37
+ url="some_url",
38
+ id="some_id",
39
+ sheet_id_by_tab_name={},
40
+ )
41
+ # The `looker_studio_report_url` can now be shared to open a pre-populated
42
+ # report.
43
+ ```
44
+ """
45
+
46
+ from scenarioplanner.linkingapi import constants
47
+ from scenarioplanner.linkingapi import url_generator
@@ -0,0 +1,27 @@
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
+ """Constants shared for the Linking API usage for the Meridian UI.
16
+
17
+ Defines constants used in the URL generation process, such as API endpoints and
18
+ parameter names.
19
+ """
20
+
21
+ REPORT_TEMPLATE_ID = 'fbd3aeff-fc00-45fd-83f7-1ec5f21c9f56'
22
+ COMMUNITY_CONNECTOR_NAME = 'community'
23
+ COMMUNITY_CONNECTOR_ID = (
24
+ 'AKfycbz-xdEN-GbTuQ9MjEddS-64wLgXwMMTp9a4zFE4PO_kwT6wDgZPsN4Y19oKmLLHD6xk'
25
+ )
26
+ SHEETS_CONNECTOR_NAME = 'googleSheets'
27
+ GA4_MEASUREMENT_ID = 'G-R6C81BNHJ4'
@@ -0,0 +1,131 @@
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
+ """Utility library for generating a Looker Studio report and outputting the URL.
16
+
17
+ Contains the core logic for constructing the Looker Studio report URL, including
18
+ setting the correct parameters.
19
+
20
+ This library requires authentication.
21
+
22
+ * If you're developing locally, set up Application Default Credentials (ADC)
23
+ in
24
+ your local environment:
25
+
26
+ <https://cloud.google.com/docs/authentication/application-default-credentials>
27
+
28
+ * If you're working in Colab, run the following command in a cell to
29
+ authenticate:
30
+
31
+ ```python
32
+ from google.colab import auth
33
+ auth.authenticate_user()
34
+ ```
35
+
36
+ This command opens a window where you can complete the authentication.
37
+ """
38
+
39
+ import urllib.parse
40
+ import warnings
41
+ from scenarioplanner.converters import sheets
42
+ from scenarioplanner.converters.dataframe import constants as dc
43
+ from scenarioplanner.linkingapi import constants
44
+
45
+
46
+ def create_report_url(spreadsheet: sheets.Spreadsheet) -> str:
47
+ """Creates a Looker Studio report URL based on the given spreadsheet.
48
+
49
+ If there are some sheet tabs that are not in `spreadsheet`, the report will
50
+ display its demo data.
51
+
52
+ Args:
53
+ spreadsheet: The spreadsheet object that contains the data to visualize in a
54
+ Looker Studio report.
55
+
56
+ Returns:
57
+ The URL of the Looker Studio report.
58
+ """
59
+ params = []
60
+
61
+ encoded_sheet_url = urllib.parse.quote_plus(spreadsheet.url)
62
+
63
+ params.append(f'c.reportId={constants.REPORT_TEMPLATE_ID}')
64
+ params.append(f'r.measurementId={constants.GA4_MEASUREMENT_ID}')
65
+
66
+ if dc.OPTIMIZATION_SPECS in spreadsheet.sheet_id_by_tab_name:
67
+ params.append(f'ds.dscc.connector={constants.COMMUNITY_CONNECTOR_NAME}')
68
+ params.append(f'ds.dscc.connectorId={constants.COMMUNITY_CONNECTOR_ID}')
69
+ params.append(f'ds.dscc.spreadsheetUrl={encoded_sheet_url}')
70
+ else:
71
+ warnings.warn(
72
+ 'No optimization specs found in the spreadsheet. The report will'
73
+ ' display its demo data.'
74
+ )
75
+
76
+ params.append('ds.*.refreshFields=false')
77
+ params.append('ds.*.keepDatasourceName=true')
78
+ params.append(f'ds.*.connector={constants.SHEETS_CONNECTOR_NAME}')
79
+ params.append(f'ds.*.spreadsheetId={spreadsheet.id}')
80
+
81
+ if dc.MODEL_FIT in spreadsheet.sheet_id_by_tab_name:
82
+ worksheet_id = spreadsheet.sheet_id_by_tab_name[dc.MODEL_FIT]
83
+ params.append(f'ds.ds_model_fit.worksheetId={worksheet_id}')
84
+ else:
85
+ warnings.warn(
86
+ 'No model fit found in the spreadsheet. The report will'
87
+ ' display its demo data.'
88
+ )
89
+
90
+ if dc.MODEL_DIAGNOSTICS in spreadsheet.sheet_id_by_tab_name:
91
+ worksheet_id = spreadsheet.sheet_id_by_tab_name[dc.MODEL_DIAGNOSTICS]
92
+ params.append(f'ds.ds_model_diag.worksheetId={worksheet_id}')
93
+ else:
94
+ warnings.warn(
95
+ 'No model diagnostics found in the spreadsheet. The report will'
96
+ ' display its demo data.'
97
+ )
98
+
99
+ if dc.MEDIA_OUTCOME in spreadsheet.sheet_id_by_tab_name:
100
+ worksheet_id = spreadsheet.sheet_id_by_tab_name[dc.MEDIA_OUTCOME]
101
+ params.append(f'ds.ds_outcome.worksheetId={worksheet_id}')
102
+ else:
103
+ warnings.warn(
104
+ 'No media outcome found in the spreadsheet. The report will'
105
+ ' display its demo data.'
106
+ )
107
+
108
+ if dc.MEDIA_SPEND in spreadsheet.sheet_id_by_tab_name:
109
+ worksheet_id = spreadsheet.sheet_id_by_tab_name[dc.MEDIA_SPEND]
110
+ params.append(f'ds.ds_spend.worksheetId={worksheet_id}')
111
+ else:
112
+ warnings.warn(
113
+ 'No media spend found in the spreadsheet. The report will'
114
+ ' display its demo data.'
115
+ )
116
+
117
+ if dc.MEDIA_ROI in spreadsheet.sheet_id_by_tab_name:
118
+ worksheet_id = spreadsheet.sheet_id_by_tab_name[dc.MEDIA_ROI]
119
+ params.append(f'ds.ds_roi.worksheetId={worksheet_id}')
120
+ else:
121
+ warnings.warn(
122
+ 'No media ROI found in the spreadsheet. The report will'
123
+ ' display its demo data.'
124
+ )
125
+
126
+ joined_params = '&'.join(params)
127
+ report_url = (
128
+ 'https://lookerstudio.google.com/reporting/create?' + joined_params
129
+ )
130
+
131
+ return report_url
@@ -0,0 +1,354 @@
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
+ """Generates an `Mmm` (Marketing Mix Model) proto for Meridian UI.
16
+
17
+ The MMM proto schema contains parts collected from the core model as well as
18
+ analysis results from trained model processors.
19
+ """
20
+
21
+ import abc
22
+ from collections.abc import Collection, Sequence
23
+ import dataclasses
24
+ import datetime
25
+ from typing import TypeVar
26
+ import uuid
27
+ import warnings
28
+
29
+ from meridian.analysis import optimizer
30
+ from meridian.data import time_coordinates as tc
31
+ from meridian.model import model
32
+ from mmm.v1 import mmm_pb2 as mmm_pb
33
+ from scenarioplanner.converters.dataframe import constants as converter_constants
34
+ from schema import mmm_proto_generator
35
+ from schema.processors import budget_optimization_processor as bop
36
+ from schema.processors import marketing_processor
37
+ from schema.processors import model_fit_processor
38
+ from schema.processors import model_processor
39
+ from schema.processors import reach_frequency_optimization_processor as rfop
40
+ from schema.utils import date_range_bucketing
41
+
42
+
43
+ __all__ = [
44
+ "MmmUiProtoGenerator",
45
+ "create_mmm_ui_data_proto",
46
+ "create_tag",
47
+ ]
48
+
49
+
50
+ _ALLOWED_SPEC_TYPES_FOR_UI = frozenset({
51
+ model_fit_processor.ModelFitSpec,
52
+ marketing_processor.MarketingAnalysisSpec,
53
+ bop.BudgetOptimizationSpec,
54
+ })
55
+
56
+ _SPEC_TYPES_CREATE_SUBSPECS = frozenset({
57
+ marketing_processor.MarketingAnalysisSpec,
58
+ bop.BudgetOptimizationSpec,
59
+ rfop.ReachFrequencyOptimizationSpec,
60
+ })
61
+
62
+ _DATE_RANGE_GENERATORS = frozenset({
63
+ date_range_bucketing.MonthlyDateRangeGenerator,
64
+ date_range_bucketing.QuarterlyDateRangeGenerator,
65
+ date_range_bucketing.YearlyDateRangeGenerator,
66
+ })
67
+
68
+ SpecType = TypeVar("SpecType", bound=model_processor.Spec)
69
+ DatedSpecType = TypeVar("DatedSpecType", bound=model_processor.DatedSpec)
70
+ OptimizationSpecType = TypeVar(
71
+ "OptimizationSpecType", bound=model_processor.OptimizationSpec
72
+ )
73
+
74
+ _DERIVED_RF_OPT_NAME_PREFIX = "derived RF optimization from "
75
+ _DERIVED_RF_OPT_GRID_NAME_PREFIX = "derived_from_"
76
+
77
+
78
+ class MmmUiProtoGenerator:
79
+ """Creates `Mmm` proto for the Meridian Scenario Planner UI (Looker Studio).
80
+
81
+ Currently, it only accepts specs for Model Fit, Marketing Analysis, and Budget
82
+ Optimization, but not stand-alone Reach Frequency Optimization specs.
83
+ Reach Frequency Optimization spec will be derived from the Budget Optimization
84
+ spec; this is done so that we can structurally pair them.
85
+
86
+ Attributes:
87
+ mmm: A trained Meridian model. A trained model has its posterior
88
+ distributions already sampled.
89
+ specs: A sequence of specs that specify the analyses to run on the model.
90
+ model_id: An optional model identifier.
91
+ time_breakdown_generators: A list of generators that break down the given
92
+ specs by automatically generated time buckets. Currently, this time period
93
+ breakdown is only done on Marketing Analysis specs and Budget Optimization
94
+ specs. All other specs are processed in their original forms. The set of
95
+ default bucketers break down sub-specs with the following time periods:
96
+ [All (original spec's time period), Yearly, Quarterly, Monthly]
97
+ """
98
+
99
+ def __init__(
100
+ self,
101
+ mmm: model.Meridian,
102
+ specs: Sequence[SpecType],
103
+ model_id: str = "",
104
+ time_breakdown_generators: Collection[
105
+ type[date_range_bucketing.DateRangeBucketer]
106
+ ] = _DATE_RANGE_GENERATORS,
107
+ ):
108
+ self._mmm = mmm
109
+ self._input_specs = specs
110
+ self._model_id = model_id
111
+ self._time_breakdown_generators = time_breakdown_generators
112
+
113
+ @property
114
+ def _time_coordinates(self) -> tc.TimeCoordinates:
115
+ return self._mmm.input_data.time_coordinates
116
+
117
+ def __call__(self) -> mmm_pb.Mmm:
118
+ """Creates `Mmm` proto for the Meridian Scenario Planner UI (Looker Studio).
119
+
120
+ Returns:
121
+ A proto containing the model kernel at rest and its analysis results given
122
+ user specs.
123
+ """
124
+ seen_group_ids = set()
125
+
126
+ copy_specs = []
127
+ for spec in self._input_specs:
128
+ if not any(isinstance(spec, t) for t in _ALLOWED_SPEC_TYPES_FOR_UI):
129
+ raise ValueError(f"Unsupported spec type: {spec.__class__.__name__}")
130
+
131
+ if isinstance(spec, bop.BudgetOptimizationSpec):
132
+ group_id = spec.group_id
133
+ if not group_id:
134
+ group_id = str(uuid.uuid4())
135
+ copy_specs.append(dataclasses.replace(spec, group_id=group_id))
136
+ else:
137
+ if group_id in seen_group_ids:
138
+ raise ValueError(
139
+ f"Duplicate group ID found: {group_id}. Please provide a unique"
140
+ " group ID for each Budget Optimization spec."
141
+ )
142
+ seen_group_ids.add(group_id)
143
+ copy_specs.append(spec)
144
+
145
+ # If there are RF channels, derive a RF optimization spec from the
146
+ # Budget Optimization spec.
147
+ if self._mmm.input_data.rf_channel is not None:
148
+ copy_specs.append(
149
+ self._derive_rf_opt_spec_from_budget_opt_spec(copy_specs[-1])
150
+ )
151
+ else:
152
+ copy_specs.append(spec)
153
+
154
+ sub_specs = []
155
+ for spec in copy_specs:
156
+ to_create_subspecs = self._time_breakdown_generators and any(
157
+ isinstance(spec, t) for t in _SPEC_TYPES_CREATE_SUBSPECS
158
+ )
159
+
160
+ if to_create_subspecs:
161
+ dates = self._enumerate_dates_open_end(spec)
162
+ sub_specs.extend(
163
+ _create_subspecs(spec, dates, self._time_breakdown_generators)
164
+ )
165
+ else:
166
+ sub_specs.append(spec)
167
+
168
+ return mmm_proto_generator.create_mmm_proto(
169
+ self._mmm,
170
+ sub_specs,
171
+ model_id=self._model_id,
172
+ )
173
+
174
+ def _derive_rf_opt_spec_from_budget_opt_spec(
175
+ self,
176
+ budget_opt_spec: bop.BudgetOptimizationSpec,
177
+ ) -> rfop.ReachFrequencyOptimizationSpec:
178
+ """Derives a ReachFrequencyOptimizationSpec from a BudgetOptimizationSpec."""
179
+ rf_opt_name = (
180
+ f"{_DERIVED_RF_OPT_NAME_PREFIX}{budget_opt_spec.optimization_name}"
181
+ )
182
+ rf_opt_grid_name = (
183
+ f"{_DERIVED_RF_OPT_GRID_NAME_PREFIX}{budget_opt_spec.optimization_name}"
184
+ )
185
+
186
+ return rfop.ReachFrequencyOptimizationSpec(
187
+ start_date=budget_opt_spec.start_date,
188
+ end_date=budget_opt_spec.end_date,
189
+ date_interval_tag=budget_opt_spec.date_interval_tag,
190
+ optimization_name=rf_opt_name,
191
+ grid_name=rf_opt_grid_name,
192
+ group_id=budget_opt_spec.group_id,
193
+ confidence_level=budget_opt_spec.confidence_level,
194
+ )
195
+
196
+ def _enumerate_dates_open_end(
197
+ self, spec: DatedSpecType
198
+ ) -> list[datetime.date]:
199
+ """Enumerates date points with an open end date.
200
+
201
+ The date points are enumerated from the data's time coordinates based on the
202
+ spec's start and end dates. The last date point is the exclusive end date as
203
+ same as the spec's end date, if specified.
204
+
205
+ Args:
206
+ spec: A dated spec.
207
+
208
+ Returns:
209
+ A list of date points.
210
+ """
211
+ inclusive_date_strs = spec.resolver(
212
+ self._mmm.input_data.time_coordinates
213
+ ).resolve_to_enumerated_selected_times()
214
+
215
+ if inclusive_date_strs is None:
216
+ dates = self._time_coordinates.all_dates
217
+ else:
218
+ dates = [tc.normalize_date(date_str) for date_str in inclusive_date_strs]
219
+
220
+ # If the end date is not specified, compute the exclusive end date based on
221
+ # the last date in the time coordinates.
222
+ exclusive_end_date = spec.end_date or dates[-1] + datetime.timedelta(
223
+ days=self._time_coordinates.interval_days
224
+ )
225
+
226
+ dates.append(exclusive_end_date)
227
+
228
+ return dates
229
+
230
+
231
+ def create_mmm_ui_data_proto(
232
+ mmm: model.Meridian,
233
+ specs: Sequence[SpecType],
234
+ model_id: str = "",
235
+ time_breakdown_generators: Collection[
236
+ type[date_range_bucketing.DateRangeBucketer]
237
+ ] = _DATE_RANGE_GENERATORS,
238
+ ) -> mmm_pb.Mmm:
239
+ """Creates `Mmm` proto for the Meridian Scenario Planner UI (Looker Studio).
240
+
241
+ Currently, it only accepts specs for Model Fit, Marketing Analysis, and Budget
242
+ Optimization, but not stand-alone Reach Frequency Optimization specs.
243
+ Reach Frequency Optimization spec will be derived from the Budget Optimization
244
+ spec; this is done so that we can structurally pair them.
245
+
246
+ Args:
247
+ mmm: A trained Meridian model. A trained model has its posterior
248
+ distributions already sampled.
249
+ specs: A sequence of specs that specify the analyses to run on the model.
250
+ model_id: An optional model identifier.
251
+ time_breakdown_generators: A list of generators that break down the given
252
+ specs by automatically generated time buckets. Currently, this time period
253
+ breakdown is only done on Marketing Analysis specs and Budget Optimization
254
+ specs. All other specs are processed in their original forms. The set of
255
+ default bucketers break down sub-specs with the following time periods:
256
+ [All (original spec's time period), Yearly, Quarterly, Monthly]
257
+
258
+ Returns:
259
+ A proto containing the model kernel at rest and its analysis results given
260
+ user specs.
261
+ """
262
+ return MmmUiProtoGenerator(
263
+ mmm,
264
+ specs,
265
+ model_id,
266
+ time_breakdown_generators,
267
+ )()
268
+
269
+
270
+ def create_tag(
271
+ generator_class: type[abc.ABC], start_date: datetime.date
272
+ ) -> str:
273
+ """Creates a human-readable tag for a spec."""
274
+ if generator_class == date_range_bucketing.YearlyDateRangeGenerator:
275
+ return f"Y{start_date.year}"
276
+ elif generator_class == date_range_bucketing.QuarterlyDateRangeGenerator:
277
+ return f"Y{start_date.year} Q{(start_date.month - 1) // 3 + 1}"
278
+ elif generator_class == date_range_bucketing.MonthlyDateRangeGenerator:
279
+ return f"Y{start_date.year} {start_date.strftime('%b')}"
280
+ else:
281
+ raise ValueError(f"Unsupported generator class: {generator_class}")
282
+
283
+
284
+ def _normalize_optimization_spec_time_info(
285
+ spec: OptimizationSpecType,
286
+ date_interval_tag: str,
287
+ ) -> OptimizationSpecType:
288
+ """Adds time info to an optimization spec."""
289
+ formatted_date_interval_tag = date_interval_tag.replace(r" ", "_")
290
+ return dataclasses.replace(
291
+ spec,
292
+ group_id=f"{spec.group_id}:{date_interval_tag}",
293
+ optimization_name=f"{spec.optimization_name} for {date_interval_tag}",
294
+ grid_name=f"{spec.grid_name}_{formatted_date_interval_tag}",
295
+ )
296
+
297
+
298
+ def _create_subspecs(
299
+ spec: DatedSpecType,
300
+ date_range: list[datetime.date],
301
+ time_breakdown_generators: Collection[
302
+ type[date_range_bucketing.DateRangeBucketer]
303
+ ],
304
+ ) -> list[DatedSpecType]:
305
+ """Breaks down a spec into sub-specs for each time bucket."""
306
+ specs = []
307
+
308
+ all_period_spec = dataclasses.replace(
309
+ spec,
310
+ date_interval_tag=converter_constants.ANALYSIS_TAG_ALL,
311
+ )
312
+ if isinstance(all_period_spec, model_processor.OptimizationSpec):
313
+ all_period_spec = _normalize_optimization_spec_time_info(
314
+ all_period_spec, converter_constants.ANALYSIS_TAG_ALL
315
+ )
316
+ specs.append(all_period_spec)
317
+
318
+ for generator_class in time_breakdown_generators:
319
+ generator = generator_class(date_range) # pytype: disable=not-instantiable
320
+ date_intervals = generator.generate_date_intervals()
321
+ for start_date, end_date in date_intervals:
322
+ date_interval_tag = create_tag(generator_class, start_date)
323
+ new_spec = dataclasses.replace(
324
+ spec,
325
+ start_date=start_date,
326
+ end_date=end_date,
327
+ date_interval_tag=date_interval_tag,
328
+ )
329
+
330
+ if isinstance(new_spec, model_processor.OptimizationSpec):
331
+ new_spec = _normalize_optimization_spec_time_info(
332
+ new_spec, date_interval_tag
333
+ )
334
+
335
+ if (
336
+ isinstance(new_spec, bop.BudgetOptimizationSpec)
337
+ and isinstance(new_spec.scenario, optimizer.FixedBudgetScenario)
338
+ and new_spec.scenario.total_budget is not None
339
+ ):
340
+ # TODO: The budget amount should be adjusted based on the
341
+ # budget specified in the `all_period_spec` and the historical spend
342
+ # at the time period.
343
+ new_spec = dataclasses.replace(
344
+ new_spec,
345
+ scenario=optimizer.FixedBudgetScenario(total_budget=None),
346
+ )
347
+ warnings.warn(
348
+ "Using historical spend for budget optimization spec at the"
349
+ f" period of {date_interval_tag}",
350
+ )
351
+
352
+ specs.append(new_spec)
353
+
354
+ return specs
schema/__init__.py CHANGED
@@ -14,11 +14,11 @@
14
14
 
15
15
  """Module containing MMM schema library."""
16
16
 
17
- try: # pylint: disable=g-statement-before-imports
17
+ try:
18
18
  # A quick check for schema dependencies.
19
19
  # If this fails, it's likely because meridian was installed without
20
20
  # `pip install google-meridian[schema]`.
21
- from mmm.v1.model.meridian import meridian_model_pb2 # pylint: disable=g-import-not-at-top
21
+ from mmm.v1.model.meridian import meridian_model_pb2
22
22
  except ModuleNotFoundError as exc:
23
23
  raise ImportError(
24
24
  'Schema dependencies not found. Please install meridian with '
@@ -26,5 +26,8 @@ except ModuleNotFoundError as exc:
26
26
  ) from exc
27
27
 
28
28
  # pylint: disable=g-import-not-at-top
29
+ from schema import mmm_proto_generator
30
+ from schema import model_consumer
31
+ from schema import processors
29
32
  from schema import serde
30
33
  from schema import utils
@@ -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
+ """Generates an `Mmm` (Marketing Mix Model) proto for Meridian.
16
+
17
+ The MMM proto schema contains parts collected from the core model as well as
18
+ analysis results from trained model processors.
19
+ """
20
+
21
+ from collections.abc import Sequence
22
+ from typing import TypeVar
23
+
24
+ from meridian.model import model
25
+ from mmm.v1 import mmm_pb2 as mmm_pb
26
+ from schema import model_consumer
27
+ from schema.processors import budget_optimization_processor
28
+ from schema.processors import marketing_processor
29
+ from schema.processors import model_fit_processor
30
+ from schema.processors import model_processor
31
+ from schema.processors import reach_frequency_optimization_processor
32
+
33
+
34
+ __all__ = [
35
+ "create_mmm_proto",
36
+ ]
37
+
38
+
39
+ _TYPES = (
40
+ model_fit_processor.ModelFitProcessor,
41
+ marketing_processor.MarketingProcessor,
42
+ budget_optimization_processor.BudgetOptimizationProcessor,
43
+ reach_frequency_optimization_processor.ReachFrequencyOptimizationProcessor,
44
+ )
45
+
46
+ SpecType = TypeVar("SpecType", bound=model_processor.Spec)
47
+ DatedSpecType = TypeVar("DatedSpecType", bound=model_processor.DatedSpec)
48
+ OptimizationSpecType = TypeVar(
49
+ "OptimizationSpecType", bound=model_processor.OptimizationSpec
50
+ )
51
+
52
+
53
+ def create_mmm_proto(
54
+ mmm: model.Meridian,
55
+ specs: Sequence[SpecType],
56
+ model_id: str = "",
57
+ ) -> mmm_pb.Mmm:
58
+ """Creates a model schema and analyses for various time buckets.
59
+
60
+ Args:
61
+ mmm: A trained Meridian model. A trained model has its posterior
62
+ distributions already sampled.
63
+ specs: A sequence of specs that specify the analyses to run on the model.
64
+ model_id: An optional model identifier.
65
+
66
+ Returns:
67
+ A proto containing the model kernel at rest and its analysis results given
68
+ user specs.
69
+ """
70
+ consumer = model_consumer.ModelConsumer(_TYPES)
71
+ return consumer(mmm, specs, model_id)