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.
- {google_meridian-1.3.2.dist-info → google_meridian-1.4.0.dist-info}/METADATA +8 -4
- {google_meridian-1.3.2.dist-info → google_meridian-1.4.0.dist-info}/RECORD +49 -17
- {google_meridian-1.3.2.dist-info → google_meridian-1.4.0.dist-info}/top_level.txt +1 -0
- meridian/analysis/summarizer.py +7 -2
- meridian/analysis/test_utils.py +934 -485
- meridian/analysis/visualizer.py +10 -6
- meridian/constants.py +1 -0
- meridian/data/test_utils.py +82 -10
- meridian/model/__init__.py +2 -0
- meridian/model/context.py +925 -0
- meridian/model/eda/constants.py +1 -0
- meridian/model/equations.py +418 -0
- meridian/model/knots.py +58 -47
- meridian/model/model.py +93 -792
- 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 +5 -2
- 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.2.dist-info → google_meridian-1.4.0.dist-info}/WHEEL +0 -0
- {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:
|
|
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
|
|
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)
|