google-meridian 1.3.2__py3-none-any.whl → 1.5.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.5.0.dist-info}/METADATA +18 -11
- google_meridian-1.5.0.dist-info/RECORD +112 -0
- {google_meridian-1.3.2.dist-info → google_meridian-1.5.0.dist-info}/WHEEL +1 -1
- {google_meridian-1.3.2.dist-info → google_meridian-1.5.0.dist-info}/top_level.txt +1 -0
- meridian/analysis/analyzer.py +558 -398
- meridian/analysis/optimizer.py +90 -68
- meridian/analysis/review/reviewer.py +4 -1
- meridian/analysis/summarizer.py +13 -3
- meridian/analysis/test_utils.py +2911 -2102
- meridian/analysis/visualizer.py +37 -14
- meridian/backend/__init__.py +106 -0
- meridian/constants.py +2 -0
- meridian/data/input_data.py +30 -52
- meridian/data/input_data_builder.py +2 -9
- meridian/data/test_utils.py +107 -51
- meridian/data/validator.py +48 -0
- meridian/mlflow/autolog.py +19 -9
- meridian/model/__init__.py +2 -0
- meridian/model/adstock_hill.py +3 -5
- meridian/model/context.py +1059 -0
- meridian/model/eda/constants.py +335 -4
- meridian/model/eda/eda_engine.py +723 -312
- meridian/model/eda/eda_outcome.py +177 -33
- meridian/model/equations.py +418 -0
- meridian/model/knots.py +58 -47
- meridian/model/model.py +228 -878
- meridian/model/model_test_data.py +38 -0
- meridian/model/posterior_sampler.py +103 -62
- meridian/model/prior_sampler.py +114 -94
- meridian/model/spec.py +23 -14
- meridian/templates/card.html.jinja +9 -7
- meridian/templates/chart.html.jinja +1 -6
- meridian/templates/finding.html.jinja +19 -0
- meridian/templates/findings.html.jinja +33 -0
- meridian/templates/formatter.py +41 -5
- meridian/templates/formatter_test.py +127 -0
- meridian/templates/style.css +66 -9
- meridian/templates/style.scss +85 -4
- meridian/templates/table.html.jinja +1 -0
- 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 +355 -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 +1137 -0
- schema/processors/model_fit_processor.py +367 -0
- schema/processors/model_kernel_processor.py +117 -0
- schema/processors/model_processor.py +415 -0
- schema/processors/reach_frequency_optimization_processor.py +584 -0
- schema/serde/distribution.py +12 -7
- schema/serde/hyperparameters.py +54 -107
- schema/serde/meridian_serde.py +6 -1
- schema/test_data.py +380 -0
- schema/utils/__init__.py +2 -0
- schema/utils/date_range_bucketing.py +117 -0
- schema/utils/proto_enum_converter.py +127 -0
- google_meridian-1.3.2.dist-info/RECORD +0 -76
- {google_meridian-1.3.2.dist-info → google_meridian-1.5.0.dist-info}/licenses/LICENSE +0 -0
meridian/templates/formatter.py
CHANGED
|
@@ -18,6 +18,7 @@ from collections.abc import Sequence
|
|
|
18
18
|
import dataclasses
|
|
19
19
|
import math
|
|
20
20
|
import os
|
|
21
|
+
import re
|
|
21
22
|
|
|
22
23
|
import altair as alt
|
|
23
24
|
import immutabledict
|
|
@@ -46,6 +47,9 @@ class ChartSpec:
|
|
|
46
47
|
id: str
|
|
47
48
|
chart_json: str
|
|
48
49
|
description: str | None = None
|
|
50
|
+
errors: Sequence[str] | None = None
|
|
51
|
+
warnings: Sequence[str] | None = None
|
|
52
|
+
infos: Sequence[str] | None = None
|
|
49
53
|
|
|
50
54
|
|
|
51
55
|
@dataclasses.dataclass(frozen=True)
|
|
@@ -55,6 +59,9 @@ class TableSpec:
|
|
|
55
59
|
column_headers: Sequence[str]
|
|
56
60
|
row_values: Sequence[Sequence[str]]
|
|
57
61
|
description: str | None = None
|
|
62
|
+
errors: Sequence[str] | None = None
|
|
63
|
+
warnings: Sequence[str] | None = None
|
|
64
|
+
infos: Sequence[str] | None = None
|
|
58
65
|
|
|
59
66
|
|
|
60
67
|
@dataclasses.dataclass(frozen=True)
|
|
@@ -198,6 +205,25 @@ def format_monetary_num(num: float, currency: str) -> str:
|
|
|
198
205
|
return compact_number(num, precision=precision, currency=currency)
|
|
199
206
|
|
|
200
207
|
|
|
208
|
+
def format_col_names(headers: Sequence[str]) -> Sequence[str]:
|
|
209
|
+
"""Turns underscores to spaces and capitalizes words.
|
|
210
|
+
|
|
211
|
+
Ex. ['col_name', ...] to ['Col Name', ...])
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
headers: The list of column names to format.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Human readable list of column names.
|
|
218
|
+
"""
|
|
219
|
+
# \b matches the start of a word
|
|
220
|
+
# [a-z] matches only if the first letter is lowercase
|
|
221
|
+
return [
|
|
222
|
+
re.sub(r'\b[a-z]', lambda m: m.group().upper(), header.replace('_', ' '))
|
|
223
|
+
for header in headers
|
|
224
|
+
]
|
|
225
|
+
|
|
226
|
+
|
|
201
227
|
def create_template_env() -> jinja2.Environment:
|
|
202
228
|
"""Creates a Jinja2 template environment."""
|
|
203
229
|
return jinja2.Environment(
|
|
@@ -220,19 +246,20 @@ def create_summary_html(
|
|
|
220
246
|
def create_card_html(
|
|
221
247
|
template_env: jinja2.Environment,
|
|
222
248
|
card_spec: CardSpec,
|
|
223
|
-
insights: str,
|
|
249
|
+
insights: str | None = None,
|
|
224
250
|
chart_specs: Sequence[ChartSpec | TableSpec] | None = None,
|
|
225
251
|
stats_specs: Sequence[StatsSpec] | None = None,
|
|
226
252
|
) -> str:
|
|
227
253
|
"""Creates a card's HTML snippet that includes given card and chart specs."""
|
|
228
|
-
insights_html = template_env.get_template('insights.html.jinja').render(
|
|
229
|
-
text_html=insights
|
|
230
|
-
)
|
|
231
254
|
card_params = dataclasses.asdict(card_spec)
|
|
232
255
|
card_params[c.CARD_CHARTS] = (
|
|
233
256
|
_create_charts_htmls(template_env, chart_specs) if chart_specs else None
|
|
234
257
|
)
|
|
235
|
-
|
|
258
|
+
if insights:
|
|
259
|
+
insights_html = template_env.get_template('insights.html.jinja').render(
|
|
260
|
+
text_html=insights
|
|
261
|
+
)
|
|
262
|
+
card_params[c.CARD_INSIGHTS] = insights_html
|
|
236
263
|
card_params[c.CARD_STATS] = (
|
|
237
264
|
_create_stats_htmls(template_env, stats_specs) if stats_specs else None
|
|
238
265
|
)
|
|
@@ -267,3 +294,12 @@ def _create_charts_htmls(
|
|
|
267
294
|
else:
|
|
268
295
|
htmls.append(table_template.render(dataclasses.asdict(spec)))
|
|
269
296
|
return htmls
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def create_finding_html(
|
|
300
|
+
template_env: jinja2.Environment, text: str, finding_type: str
|
|
301
|
+
) -> str:
|
|
302
|
+
"""Generates an HTML tag for the table finding."""
|
|
303
|
+
return template_env.get_template('finding.html.jinja').render(
|
|
304
|
+
finding_class=finding_type, text=text
|
|
305
|
+
)
|
|
@@ -92,6 +92,36 @@ class FormatterTest(parameterized.TestCase):
|
|
|
92
92
|
formatted_number = formatter.compact_number(num, precision, currency)
|
|
93
93
|
self.assertEqual(formatted_number, expected)
|
|
94
94
|
|
|
95
|
+
@parameterized.named_parameters(
|
|
96
|
+
dict(
|
|
97
|
+
testcase_name='basic_snake_case',
|
|
98
|
+
input_headers=['finding_cause'],
|
|
99
|
+
expected=['Finding Cause'],
|
|
100
|
+
),
|
|
101
|
+
dict(
|
|
102
|
+
testcase_name='multiple_columns',
|
|
103
|
+
input_headers=['geo', 'time_index', 'channel_name'],
|
|
104
|
+
expected=['Geo', 'Time Index', 'Channel Name'],
|
|
105
|
+
),
|
|
106
|
+
dict(
|
|
107
|
+
testcase_name='preserves_acronyms',
|
|
108
|
+
input_headers=['VIF_score', 'national_KPI'],
|
|
109
|
+
expected=['VIF Score', 'National KPI'],
|
|
110
|
+
),
|
|
111
|
+
dict(
|
|
112
|
+
testcase_name='handles_tuples_input',
|
|
113
|
+
input_headers=('row_id', 'value'),
|
|
114
|
+
expected=['Row Id', 'Value'],
|
|
115
|
+
),
|
|
116
|
+
dict(
|
|
117
|
+
testcase_name='empty_input',
|
|
118
|
+
input_headers=[],
|
|
119
|
+
expected=[],
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
def test_format_col_names(self, input_headers, expected):
|
|
123
|
+
self.assertEqual(formatter.format_col_names(input_headers), expected)
|
|
124
|
+
|
|
95
125
|
def test_create_summary_html(self):
|
|
96
126
|
template_env = formatter.create_template_env()
|
|
97
127
|
title = 'Integration Test Report'
|
|
@@ -211,6 +241,103 @@ class FormatterTest(parameterized.TestCase):
|
|
|
211
241
|
self.assertEqual(stats_html[0][2].tag, 'delta')
|
|
212
242
|
self.assertContainsSubset('+0.3', stats_html[0][2].text)
|
|
213
243
|
|
|
244
|
+
def test_create_card_html_no_insights(self):
|
|
245
|
+
template_env = formatter.create_template_env()
|
|
246
|
+
card_spec = formatter.CardSpec(id='test_id', title='test_title')
|
|
247
|
+
stats_spec = formatter.StatsSpec(title='stats_title', stat='test_stat')
|
|
248
|
+
|
|
249
|
+
card_html = ET.fromstring(
|
|
250
|
+
formatter.create_card_html(
|
|
251
|
+
template_env, card_spec, insights=None, stats_specs=[stats_spec]
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
self.assertEqual(card_html.tag, 'card')
|
|
256
|
+
self.assertIsNone(card_html.find('card-insights'))
|
|
257
|
+
self.assertIsNotNone(card_html.find('stats-section'))
|
|
258
|
+
|
|
259
|
+
def test_create_card_html_chart_findings(self):
|
|
260
|
+
"""Tests that errors, warnings, and infos render inside a chart."""
|
|
261
|
+
template_env = formatter.create_template_env()
|
|
262
|
+
card_spec = formatter.CardSpec(id='test_id', title='test_title')
|
|
263
|
+
chart_spec = formatter.ChartSpec(
|
|
264
|
+
id='id',
|
|
265
|
+
chart_json='{}',
|
|
266
|
+
errors=['Chart Error'],
|
|
267
|
+
warnings=['Chart Warning'],
|
|
268
|
+
infos=['Chart Info'],
|
|
269
|
+
)
|
|
270
|
+
card_html = ET.fromstring(
|
|
271
|
+
formatter.create_card_html(
|
|
272
|
+
template_env, card_spec, insights=None, chart_specs=[chart_spec]
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
charts_elem = card_html.find('charts')
|
|
277
|
+
self.assertIsNotNone(charts_elem)
|
|
278
|
+
chart_elem = charts_elem.find('chart')
|
|
279
|
+
self.assertIsNotNone(chart_elem)
|
|
280
|
+
|
|
281
|
+
error_elem = chart_elem.find('errors')
|
|
282
|
+
self.assertIsNotNone(error_elem)
|
|
283
|
+
error_p = error_elem.find('p')
|
|
284
|
+
self.assertIsNotNone(error_p)
|
|
285
|
+
self.assertIn('Chart Error', error_p.text)
|
|
286
|
+
|
|
287
|
+
warning_elem = chart_elem.find('warnings')
|
|
288
|
+
self.assertIsNotNone(warning_elem)
|
|
289
|
+
warning_p = warning_elem.find('p')
|
|
290
|
+
self.assertIsNotNone(warning_p)
|
|
291
|
+
self.assertIn('Chart Warning', warning_p.text)
|
|
292
|
+
|
|
293
|
+
info_elem = chart_elem.find('infos')
|
|
294
|
+
self.assertIsNotNone(info_elem)
|
|
295
|
+
info_p = info_elem.find('p')
|
|
296
|
+
self.assertIsNotNone(info_p)
|
|
297
|
+
self.assertIn('Chart Info', info_p.text)
|
|
298
|
+
|
|
299
|
+
def test_create_card_html_table_findings(self):
|
|
300
|
+
"""Tests that errors, warnings, and infos render inside a table."""
|
|
301
|
+
template_env = formatter.create_template_env()
|
|
302
|
+
card_spec = formatter.CardSpec(id='test_id', title='test_title')
|
|
303
|
+
table_spec = formatter.TableSpec(
|
|
304
|
+
id='table_id',
|
|
305
|
+
title='Table Title',
|
|
306
|
+
column_headers=['Col1'],
|
|
307
|
+
row_values=[['Val1']],
|
|
308
|
+
errors=['Table Error'],
|
|
309
|
+
warnings=['Table Warning'],
|
|
310
|
+
infos=['Table Info'],
|
|
311
|
+
)
|
|
312
|
+
card_html = ET.fromstring(
|
|
313
|
+
formatter.create_card_html(
|
|
314
|
+
template_env, card_spec, insights=None, chart_specs=[table_spec]
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
charts_elem = card_html.find('charts')
|
|
319
|
+
self.assertIsNotNone(charts_elem)
|
|
320
|
+
table_elem = charts_elem.find('chart-table')
|
|
321
|
+
self.assertIsNotNone(table_elem)
|
|
322
|
+
|
|
323
|
+
error_elem = table_elem.find('errors')
|
|
324
|
+
self.assertIsNotNone(error_elem)
|
|
325
|
+
error_p = error_elem.find('p')
|
|
326
|
+
self.assertIsNotNone(error_p)
|
|
327
|
+
self.assertIn('Table Error', error_p.text)
|
|
328
|
+
|
|
329
|
+
warning_elem = table_elem.find('warnings')
|
|
330
|
+
self.assertIsNotNone(warning_elem)
|
|
331
|
+
warning_p = warning_elem.find('p')
|
|
332
|
+
self.assertIsNotNone(warning_p)
|
|
333
|
+
self.assertIn('Table Warning', warning_p.text)
|
|
334
|
+
|
|
335
|
+
info_elem = table_elem.find('infos')
|
|
336
|
+
self.assertIsNotNone(info_elem)
|
|
337
|
+
info_p = info_elem.find('p')
|
|
338
|
+
self.assertIsNotNone(info_p)
|
|
339
|
+
self.assertIn('Table Info', info_p.text)
|
|
340
|
+
|
|
214
341
|
|
|
215
342
|
if __name__ == '__main__':
|
|
216
343
|
absltest.main()
|
meridian/templates/style.css
CHANGED
|
@@ -86,11 +86,30 @@ card {
|
|
|
86
86
|
font-weight: 400;
|
|
87
87
|
line-height: 50px; }
|
|
88
88
|
card > card-insights {
|
|
89
|
+
background: #eee; }
|
|
90
|
+
card errors,
|
|
91
|
+
card warnings,
|
|
92
|
+
card infos {
|
|
93
|
+
margin-top: -39px;
|
|
94
|
+
margin-left: -69px;
|
|
95
|
+
margin-right: -69px; }
|
|
96
|
+
card errors {
|
|
97
|
+
background: #f8d7da; }
|
|
98
|
+
card warnings {
|
|
99
|
+
background: #fff3cd; }
|
|
100
|
+
card infos {
|
|
101
|
+
background: #dae8fc; }
|
|
102
|
+
card > card-insights,
|
|
103
|
+
card errors,
|
|
104
|
+
card warnings,
|
|
105
|
+
card infos {
|
|
89
106
|
display: flex;
|
|
90
107
|
flex-direction: row;
|
|
91
|
-
padding: 22px 20px;
|
|
92
|
-
|
|
93
|
-
card
|
|
108
|
+
padding: 22px 20px; }
|
|
109
|
+
card > card-insights p,
|
|
110
|
+
card errors p,
|
|
111
|
+
card warnings p,
|
|
112
|
+
card infos p {
|
|
94
113
|
margin: 0px 25px;
|
|
95
114
|
color: var(--Grey-800, #3c4043);
|
|
96
115
|
font-family: Roboto;
|
|
@@ -110,7 +129,10 @@ charts {
|
|
|
110
129
|
charts chart {
|
|
111
130
|
display: flex;
|
|
112
131
|
flex-flow: column nowrap;
|
|
113
|
-
gap: 12px;
|
|
132
|
+
gap: 12px;
|
|
133
|
+
flex: 1 1 auto;
|
|
134
|
+
min-width: 0;
|
|
135
|
+
max-width: 100%; }
|
|
114
136
|
charts chart > chart-description {
|
|
115
137
|
color: var(--Grey-800, #3c4043);
|
|
116
138
|
font-family: "Google Sans Display", "Google Sans", sans-serif;
|
|
@@ -119,10 +141,18 @@ charts {
|
|
|
119
141
|
font-weight: 400;
|
|
120
142
|
line-height: 16px;
|
|
121
143
|
max-width: 450px; }
|
|
144
|
+
charts chart-embed {
|
|
145
|
+
display: block;
|
|
146
|
+
width: 100%;
|
|
147
|
+
overflow-x: auto;
|
|
148
|
+
padding: 10px 5px 20px 2px; }
|
|
122
149
|
charts chart-table {
|
|
123
150
|
display: flex;
|
|
124
151
|
flex-flow: column nowrap;
|
|
125
|
-
gap: 12px;
|
|
152
|
+
gap: 12px;
|
|
153
|
+
flex: 1 1 auto;
|
|
154
|
+
min-width: 0;
|
|
155
|
+
max-width: 100%; }
|
|
126
156
|
charts chart-table .chart-table-title {
|
|
127
157
|
color: var(--Grey-800, #3c4043);
|
|
128
158
|
font-family: "Google Sans Display", "Google Sans", sans-serif;
|
|
@@ -138,7 +168,10 @@ charts {
|
|
|
138
168
|
font-style: normal;
|
|
139
169
|
font-weight: 400;
|
|
140
170
|
line-height: 20px;
|
|
141
|
-
letter-spacing: 0.2px;
|
|
171
|
+
letter-spacing: 0.2px;
|
|
172
|
+
width: 100%;
|
|
173
|
+
overflow-x: auto;
|
|
174
|
+
padding-bottom: 20px; }
|
|
142
175
|
charts chart-table .chart-table-content table {
|
|
143
176
|
border-radius: 4px;
|
|
144
177
|
border: 1px solid var(--Grey-300, #dadce0);
|
|
@@ -160,15 +193,39 @@ charts {
|
|
|
160
193
|
font-weight: 400;
|
|
161
194
|
line-height: 16px;
|
|
162
195
|
max-width: 450px; }
|
|
196
|
+
charts chart-table finding {
|
|
197
|
+
display: flex;
|
|
198
|
+
width: fit-content;
|
|
199
|
+
align-items: center;
|
|
200
|
+
justify-content: center;
|
|
201
|
+
white-space: nowrap;
|
|
202
|
+
padding: 4px 12px;
|
|
203
|
+
border-radius: 16px;
|
|
204
|
+
margin: 2px 4px 2px 0;
|
|
205
|
+
font-family: "Google Sans Display", "Google Sans", sans-serif;
|
|
206
|
+
font-weight: 500;
|
|
207
|
+
font-size: 13px;
|
|
208
|
+
line-height: 20px; }
|
|
209
|
+
charts chart-table finding.error {
|
|
210
|
+
background-color: #f8d7da;
|
|
211
|
+
color: var(--Red-600, #d93025); }
|
|
212
|
+
charts chart-table finding.attention {
|
|
213
|
+
background-color: #fff3cd;
|
|
214
|
+
color: #664d03; }
|
|
215
|
+
charts chart-table finding.info {
|
|
216
|
+
background-color: #dae8fc;
|
|
217
|
+
color: var(--blue-700, #1967d2); }
|
|
163
218
|
|
|
164
219
|
stats-section {
|
|
165
220
|
display: flex;
|
|
166
|
-
flex-
|
|
167
|
-
justify-content:
|
|
221
|
+
flex-flow: row wrap;
|
|
222
|
+
justify-content: flex-start;
|
|
223
|
+
gap: 40px;
|
|
168
224
|
padding: 32px; }
|
|
169
225
|
stats-section stats {
|
|
170
226
|
display: flex;
|
|
171
|
-
flex-direction: column;
|
|
227
|
+
flex-direction: column;
|
|
228
|
+
flex: 0 0 auto; }
|
|
172
229
|
stats-section stats > stats-title {
|
|
173
230
|
color: var(--grey-900, #202124);
|
|
174
231
|
font-family: Roboto;
|
meridian/templates/style.scss
CHANGED
|
@@ -20,6 +20,10 @@ $insights_bg_grey: #eee;
|
|
|
20
20
|
$text_grey: var(--Grey-800, #3c4043);
|
|
21
21
|
$text_green: var(--Green-500, #34a853);
|
|
22
22
|
$text_red: var(--Red-600, #d93025);
|
|
23
|
+
$error_red: #f8d7da;
|
|
24
|
+
$text_yellow: #664d03;
|
|
25
|
+
$warning_yellow: #fff3cd;
|
|
26
|
+
$info_blue: #dae8fc;
|
|
23
27
|
|
|
24
28
|
$google_sans: 'Google Sans Display', 'Google Sans', sans-serif;
|
|
25
29
|
$roboto: Roboto;
|
|
@@ -112,12 +116,38 @@ card {
|
|
|
112
116
|
}
|
|
113
117
|
|
|
114
118
|
> card-insights {
|
|
119
|
+
background: $insights_bg_grey;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
errors,
|
|
123
|
+
warnings,
|
|
124
|
+
infos {
|
|
125
|
+
margin-top: -39px;
|
|
126
|
+
margin-left: -69px;
|
|
127
|
+
margin-right: -69px;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
errors {
|
|
131
|
+
background: $error_red;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
warnings {
|
|
135
|
+
background: $warning_yellow;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
infos {
|
|
139
|
+
background: $info_blue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
> card-insights,
|
|
143
|
+
errors,
|
|
144
|
+
warnings,
|
|
145
|
+
infos {
|
|
115
146
|
display: flex;
|
|
116
147
|
flex-direction: row;
|
|
117
148
|
padding: 22px 20px;
|
|
118
|
-
background: $insights_bg_grey;
|
|
119
149
|
|
|
120
|
-
p
|
|
150
|
+
p {
|
|
121
151
|
margin: 0px 25px;
|
|
122
152
|
color: $text_grey;
|
|
123
153
|
|
|
@@ -149,6 +179,9 @@ charts {
|
|
|
149
179
|
|
|
150
180
|
chart {
|
|
151
181
|
@include chart-style;
|
|
182
|
+
flex: 1 1 auto;
|
|
183
|
+
min-width: 0;
|
|
184
|
+
max-width: 100%;
|
|
152
185
|
|
|
153
186
|
> chart-description {
|
|
154
187
|
color: $text_grey;
|
|
@@ -162,8 +195,19 @@ charts {
|
|
|
162
195
|
}
|
|
163
196
|
}
|
|
164
197
|
|
|
198
|
+
chart-embed {
|
|
199
|
+
display: block;
|
|
200
|
+
width: 100%;
|
|
201
|
+
|
|
202
|
+
overflow-x: auto;
|
|
203
|
+
padding: 10px 5px 20px 2px;
|
|
204
|
+
}
|
|
205
|
+
|
|
165
206
|
chart-table {
|
|
166
207
|
@include chart-style;
|
|
208
|
+
flex: 1 1 auto;
|
|
209
|
+
min-width: 0;
|
|
210
|
+
max-width: 100%;
|
|
167
211
|
|
|
168
212
|
.chart-table-title {
|
|
169
213
|
color: $text_grey;
|
|
@@ -185,6 +229,9 @@ charts {
|
|
|
185
229
|
line-height: 20px;
|
|
186
230
|
letter-spacing: 0.2px;
|
|
187
231
|
|
|
232
|
+
width: 100%;
|
|
233
|
+
overflow-x: auto;
|
|
234
|
+
padding-bottom: 20px;
|
|
188
235
|
@mixin border-style {
|
|
189
236
|
border-radius: 4px;
|
|
190
237
|
border: 1px solid var(--Grey-300, #dadce0);
|
|
@@ -216,18 +263,52 @@ charts {
|
|
|
216
263
|
line-height: 16px;
|
|
217
264
|
max-width: 450px;
|
|
218
265
|
}
|
|
266
|
+
|
|
267
|
+
finding {
|
|
268
|
+
display: flex;
|
|
269
|
+
width: fit-content;
|
|
270
|
+
align-items: center;
|
|
271
|
+
justify-content: center;
|
|
272
|
+
white-space: nowrap;
|
|
273
|
+
|
|
274
|
+
padding: 4px 12px;
|
|
275
|
+
border-radius: 16px;
|
|
276
|
+
margin: 2px 4px 2px 0;
|
|
277
|
+
|
|
278
|
+
font-family: $google_sans;
|
|
279
|
+
font-weight: 500;
|
|
280
|
+
font-size: 13px;
|
|
281
|
+
line-height: 20px;
|
|
282
|
+
|
|
283
|
+
&.error {
|
|
284
|
+
background-color: $error_red;
|
|
285
|
+
color: $text_red;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
&.attention {
|
|
289
|
+
background-color: $warning_yellow;
|
|
290
|
+
color: $text_yellow;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
&.info {
|
|
294
|
+
background-color: $info_blue;
|
|
295
|
+
color: $chip_blue;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
219
298
|
}
|
|
220
299
|
}
|
|
221
300
|
|
|
222
301
|
stats-section {
|
|
223
302
|
display: flex;
|
|
224
|
-
flex-
|
|
225
|
-
justify-content:
|
|
303
|
+
flex-flow: row wrap;
|
|
304
|
+
justify-content: flex-start;
|
|
305
|
+
gap: 40px;
|
|
226
306
|
padding: 32px;
|
|
227
307
|
|
|
228
308
|
stats {
|
|
229
309
|
display: flex;
|
|
230
310
|
flex-direction: column;
|
|
311
|
+
flex: 0 0 auto;
|
|
231
312
|
|
|
232
313
|
> stats-title {
|
|
233
314
|
color: $title_dark_grey;
|
meridian/version.py
CHANGED
|
@@ -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
|
+
"""Generates Meridian Scenario Planner Dashboards in Looker Studio.
|
|
16
|
+
|
|
17
|
+
This package provides tools to create and manage Meridian dashboards. It helps
|
|
18
|
+
transform data from the MMM (Marketing Mix Modeling) schema into a custom
|
|
19
|
+
Looker Studio dashboard, which can be shared via a URL.
|
|
20
|
+
|
|
21
|
+
The typical workflow is:
|
|
22
|
+
|
|
23
|
+
1. Analyze MMM data into the appropriate schema.
|
|
24
|
+
2. Generate UI-specific proto messages from this data using
|
|
25
|
+
`mmm_ui_proto_generator`.
|
|
26
|
+
3. Build a Looker Studio URL that embeds this UI proto data using
|
|
27
|
+
`linkingapi`.
|
|
28
|
+
|
|
29
|
+
Key functionalities include:
|
|
30
|
+
|
|
31
|
+
- `linkingapi`: Builds Looker Studio report URLs with embedded data sources.
|
|
32
|
+
This allows for the creation of pre-configured reports.
|
|
33
|
+
- `mmm_ui_proto_generator`: Generates a `Mmm` proto message for the Meridian
|
|
34
|
+
Scenario Planner UI. It takes structured MMM data and transforms it into the
|
|
35
|
+
specific proto format that the dashboard frontend expects.
|
|
36
|
+
- `converters`: Provides utilities to convert and transform analyzed model
|
|
37
|
+
data into a data format that Looker Studio expects.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from scenarioplanner import converters
|
|
41
|
+
from scenarioplanner import linkingapi
|
|
42
|
+
from scenarioplanner import mmm_ui_proto_generator
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
"""Provides tools for converting and wrapping MMM schema data.
|
|
16
|
+
|
|
17
|
+
This package contains modules to transform Marketing Mix Modeling (MMM) protocol
|
|
18
|
+
buffer data into other formats and provides high-level wrappers for easier data
|
|
19
|
+
manipulation, analysis, and reporting.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from scenarioplanner.converters import dataframe
|
|
23
|
+
from scenarioplanner.converters import mmm
|
|
24
|
+
from scenarioplanner.converters import mmm_converter
|
|
25
|
+
from scenarioplanner.converters import sheets
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
"""Converters for `Mmm` protos to flat dataframes.
|
|
16
|
+
|
|
17
|
+
This package provides a set of tools for transforming data from `Mmm`
|
|
18
|
+
protos into flat dataframes. This conversion makes the data easier to analyze,
|
|
19
|
+
visualize, and use in other data processing pipelines.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from scenarioplanner.converters.dataframe import budget_opt_converters
|
|
23
|
+
from scenarioplanner.converters.dataframe import common
|
|
24
|
+
from scenarioplanner.converters.dataframe import constants
|
|
25
|
+
from scenarioplanner.converters.dataframe import converter
|
|
26
|
+
from scenarioplanner.converters.dataframe import dataframe_model_converter
|
|
27
|
+
from scenarioplanner.converters.dataframe import marketing_analyses_converters
|
|
28
|
+
from scenarioplanner.converters.dataframe import rf_opt_converters
|