google-meridian 1.3.1__py3-none-any.whl → 1.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. {google_meridian-1.3.1.dist-info → google_meridian-1.4.0.dist-info}/METADATA +13 -9
  2. google_meridian-1.4.0.dist-info/RECORD +108 -0
  3. {google_meridian-1.3.1.dist-info → google_meridian-1.4.0.dist-info}/top_level.txt +1 -0
  4. meridian/analysis/__init__.py +1 -2
  5. meridian/analysis/analyzer.py +0 -1
  6. meridian/analysis/optimizer.py +5 -3
  7. meridian/analysis/review/checks.py +81 -30
  8. meridian/analysis/review/constants.py +4 -0
  9. meridian/analysis/review/results.py +40 -9
  10. meridian/analysis/summarizer.py +8 -3
  11. meridian/analysis/test_utils.py +934 -485
  12. meridian/analysis/visualizer.py +11 -7
  13. meridian/backend/__init__.py +53 -5
  14. meridian/backend/test_utils.py +72 -0
  15. meridian/constants.py +2 -0
  16. meridian/data/load.py +2 -0
  17. meridian/data/test_utils.py +82 -10
  18. meridian/model/__init__.py +2 -0
  19. meridian/model/context.py +925 -0
  20. meridian/model/eda/__init__.py +0 -1
  21. meridian/model/eda/constants.py +13 -2
  22. meridian/model/eda/eda_engine.py +299 -37
  23. meridian/model/eda/eda_outcome.py +21 -1
  24. meridian/model/equations.py +418 -0
  25. meridian/model/knots.py +75 -47
  26. meridian/model/model.py +93 -792
  27. meridian/{analysis/templates → templates}/card.html.jinja +1 -1
  28. meridian/{analysis/templates → templates}/chart.html.jinja +1 -1
  29. meridian/{analysis/templates → templates}/chips.html.jinja +1 -1
  30. meridian/{analysis → templates}/formatter.py +12 -1
  31. meridian/templates/formatter_test.py +216 -0
  32. meridian/{analysis/templates → templates}/insights.html.jinja +1 -1
  33. meridian/{analysis/templates → templates}/stats.html.jinja +1 -1
  34. meridian/{analysis/templates → templates}/style.css +1 -1
  35. meridian/{analysis/templates → templates}/style.scss +1 -1
  36. meridian/{analysis/templates → templates}/summary.html.jinja +4 -2
  37. meridian/{analysis/templates → templates}/table.html.jinja +1 -1
  38. meridian/version.py +1 -1
  39. scenarioplanner/__init__.py +42 -0
  40. scenarioplanner/converters/__init__.py +25 -0
  41. scenarioplanner/converters/dataframe/__init__.py +28 -0
  42. scenarioplanner/converters/dataframe/budget_opt_converters.py +383 -0
  43. scenarioplanner/converters/dataframe/common.py +71 -0
  44. scenarioplanner/converters/dataframe/constants.py +137 -0
  45. scenarioplanner/converters/dataframe/converter.py +42 -0
  46. scenarioplanner/converters/dataframe/dataframe_model_converter.py +70 -0
  47. scenarioplanner/converters/dataframe/marketing_analyses_converters.py +543 -0
  48. scenarioplanner/converters/dataframe/rf_opt_converters.py +314 -0
  49. scenarioplanner/converters/mmm.py +743 -0
  50. scenarioplanner/converters/mmm_converter.py +58 -0
  51. scenarioplanner/converters/sheets.py +156 -0
  52. scenarioplanner/converters/test_data.py +714 -0
  53. scenarioplanner/linkingapi/__init__.py +47 -0
  54. scenarioplanner/linkingapi/constants.py +27 -0
  55. scenarioplanner/linkingapi/url_generator.py +131 -0
  56. scenarioplanner/mmm_ui_proto_generator.py +354 -0
  57. schema/__init__.py +15 -0
  58. schema/mmm_proto_generator.py +71 -0
  59. schema/model_consumer.py +133 -0
  60. schema/processors/__init__.py +77 -0
  61. schema/processors/budget_optimization_processor.py +832 -0
  62. schema/processors/common.py +64 -0
  63. schema/processors/marketing_processor.py +1136 -0
  64. schema/processors/model_fit_processor.py +367 -0
  65. schema/processors/model_kernel_processor.py +117 -0
  66. schema/processors/model_processor.py +412 -0
  67. schema/processors/reach_frequency_optimization_processor.py +584 -0
  68. schema/test_data.py +380 -0
  69. schema/utils/__init__.py +1 -0
  70. schema/utils/date_range_bucketing.py +117 -0
  71. google_meridian-1.3.1.dist-info/RECORD +0 -76
  72. meridian/model/eda/meridian_eda.py +0 -220
  73. {google_meridian-1.3.1.dist-info → google_meridian-1.4.0.dist-info}/WHEEL +0 -0
  74. {google_meridian-1.3.1.dist-info → google_meridian-1.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,5 @@
1
1
  {#
2
- Copyright 2024 Google LLC
2
+ Copyright 2025 Google LLC
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
1
1
  {#
2
- Copyright 2024 Google LLC
2
+ Copyright 2025 Google LLC
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
1
1
  {#
2
- Copyright 2024 Google LLC
2
+ Copyright 2025 Google LLC
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -88,7 +88,7 @@ AXIS_CONFIG = immutabledict.immutabledict({
88
88
 
89
89
 
90
90
  _template_loader = jinja2.FileSystemLoader(
91
- os.path.abspath(os.path.dirname(__file__)) + '/templates'
91
+ os.path.abspath(os.path.dirname(__file__))
92
92
  )
93
93
 
94
94
 
@@ -206,6 +206,17 @@ def create_template_env() -> jinja2.Environment:
206
206
  )
207
207
 
208
208
 
209
+ def create_summary_html(
210
+ template_env: jinja2.Environment,
211
+ title: str,
212
+ cards: Sequence[str],
213
+ ) -> str:
214
+ """Creates the HTML snippet for the summary page."""
215
+ return template_env.get_template('summary.html.jinja').render(
216
+ title=title, cards=cards
217
+ )
218
+
219
+
209
220
  def create_card_html(
210
221
  template_env: jinja2.Environment,
211
222
  card_spec: CardSpec,
@@ -0,0 +1,216 @@
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
+ from xml.etree import ElementTree as ET
16
+
17
+ from absl.testing import absltest
18
+ from absl.testing import parameterized
19
+ from meridian.templates import formatter
20
+
21
+
22
+ class FormatterTest(parameterized.TestCase):
23
+
24
+ def test_custom_title_params_correct(self):
25
+ title_params = formatter.custom_title_params('test title')
26
+ self.assertEqual(
27
+ title_params.to_dict(),
28
+ {
29
+ 'anchor': 'start',
30
+ 'color': '#3C4043',
31
+ 'font': 'Google Sans Display',
32
+ 'fontSize': 18,
33
+ 'fontWeight': 'normal',
34
+ 'offset': 10,
35
+ 'text': 'test title',
36
+ },
37
+ )
38
+
39
+ def test_bar_chart_width(self):
40
+ num_bars = 3
41
+ width = formatter.bar_chart_width(num_bars)
42
+ self.assertEqual(width, 186)
43
+
44
+ @parameterized.named_parameters(
45
+ ('zero_percent', 0.0, '0%'),
46
+ ('less_than_one_percent', 0.0005, '0.05%'),
47
+ ('one_percent', 0.01, '1%'),
48
+ ('greater_than_one_percent', 0.4257, '43%'),
49
+ )
50
+ def test_format_percent_correct(self, percent, expected):
51
+ formatted_percent = formatter.format_percent(percent)
52
+ self.assertEqual(formatted_percent, expected)
53
+
54
+ def test_compact_number_expr_default(self):
55
+ expr = formatter.compact_number_expr()
56
+ self.assertEqual(expr, "replace(format(datum.value, '.3~s'), 'G', 'B')")
57
+
58
+ def test_compact_number_expr_params(self):
59
+ expr = formatter.compact_number_expr('other', 2)
60
+ self.assertEqual(expr, "replace(format(datum.other, '.2~s'), 'G', 'B')")
61
+
62
+ @parameterized.named_parameters(
63
+ ('rounded_up_percent', 0.4257, 15, '42.6% (15)'),
64
+ ('rounded_down_percent', 0.4251, 15, '42.5% (15)'),
65
+ ('thousand_value', 0.42, 2e4, '42.0% (20k)'),
66
+ ('million_value', 0.42, 3e7, '42.0% (30M)'),
67
+ ('billion_value', 0.42, 4e9, '42.0% (4B)'),
68
+ )
69
+ def test_format_number_text_correct(self, percent, value, expected):
70
+ formatted_text = formatter.format_number_text(percent, value)
71
+ self.assertEqual(formatted_text, expected)
72
+
73
+ @parameterized.named_parameters(
74
+ ('zero_precision_thousands', 12345, '$', '$12k'),
75
+ ('round_up_thousands', 14900, '€', '€15k'),
76
+ ('million_value', 3.21e6, '£', '£3.2M'),
77
+ ('billion_value_round_up', 4.28e9, '¥', '¥4.3B'),
78
+ ('negative', -12345, '₮', '-₮12k'),
79
+ )
80
+ def test_format_monetary_num_correct(self, num, currency, expected):
81
+ formatted_number = formatter.format_monetary_num(num, currency)
82
+ self.assertEqual(formatted_number, expected)
83
+
84
+ @parameterized.named_parameters(
85
+ ('decimals', -0.1234, 2, '$', '-$0.12'),
86
+ ('zero_precision_thousands', 12345, 0, '', '12k'),
87
+ ('round_up_thousands', 14900, 0, '$', '$15k'),
88
+ ('million_value', 3.21e6, 2, '$', '$3.21M'),
89
+ ('negative', -12345, 0, '$', '-$12k'),
90
+ )
91
+ def test_compact_number_correct(self, num, precision, currency, expected):
92
+ formatted_number = formatter.compact_number(num, precision, currency)
93
+ self.assertEqual(formatted_number, expected)
94
+
95
+ def test_create_summary_html(self):
96
+ template_env = formatter.create_template_env()
97
+ title = 'Integration Test Report'
98
+ cards = ['<card>Card 1</card>', '<card>Card 2</card>']
99
+
100
+ html_result = formatter.create_summary_html(template_env, title, cards)
101
+
102
+ # Since summary.html contains DOCTYPE (which breaks ElementTree XML parser),
103
+ # we verify the output using string assertions.
104
+ self.assertIn('<!DOCTYPE html>', html_result)
105
+ self.assertIn(title, html_result)
106
+ self.assertIn('<card>Card 1</card>', html_result)
107
+ self.assertIn('<card>Card 2</card>', html_result)
108
+
109
+ def test_create_card_html_structure(self):
110
+ template_env = formatter.create_template_env()
111
+ card_spec = formatter.CardSpec(id='test_id', title='test_title')
112
+ stats_spec = formatter.StatsSpec(title='stats_title', stat='test_stat')
113
+ chart_spec = formatter.ChartSpec(
114
+ 'test_chart_id', 'test_chart_json', 'test_chart_description'
115
+ )
116
+
117
+ card_html = ET.fromstring(
118
+ formatter.create_card_html(
119
+ template_env, card_spec, 'test_insights', [chart_spec], [stats_spec]
120
+ )
121
+ )
122
+ self.assertEqual(card_html.tag, 'card')
123
+ self.assertLen(card_html, 4)
124
+ self.assertEqual(card_html[0].tag, 'card-title')
125
+ self.assertEqual(card_html[1].tag, 'card-insights')
126
+ self.assertEqual(card_html[2].tag, 'stats-section')
127
+ self.assertEqual(card_html[3].tag, 'charts')
128
+
129
+ def test_create_card_html_text(self):
130
+ template_env = formatter.create_template_env()
131
+ card_spec = formatter.CardSpec(id='test_id', title='test_title')
132
+ chart_spec = formatter.ChartSpec(
133
+ 'test_chart_id', 'test_chart_json', 'test_chart_description'
134
+ )
135
+ card_html = ET.fromstring(
136
+ formatter.create_card_html(
137
+ template_env, card_spec, 'test_insights', [chart_spec]
138
+ )
139
+ )
140
+ self.assertContainsSubset('test_title', card_html[0].text)
141
+ self.assertContainsSubset('test_insights', card_html[1][1].text)
142
+
143
+ def test_create_card_html_multiple_charts(self):
144
+ template_env = formatter.create_template_env()
145
+ card_spec = formatter.CardSpec(id='test_id', title='test_title')
146
+ chart_spec1 = formatter.ChartSpec(
147
+ 'test_chart_id1', 'test_chart_json1', 'test_chart_description1'
148
+ )
149
+ chart_spec2 = formatter.ChartSpec(
150
+ 'test_chart_id2', 'test_chart_json2', 'test_chart_description2'
151
+ )
152
+ card_html = ET.fromstring(
153
+ formatter.create_card_html(
154
+ template_env, card_spec, 'test_insights', [chart_spec1, chart_spec2]
155
+ )
156
+ )
157
+ charts = card_html[2]
158
+ self.assertLen(charts, 4) # Each chart has 2 items, chart and script.
159
+
160
+ def test_create_card_html_chart_structure(self):
161
+ template_env = formatter.create_template_env()
162
+ card_spec = formatter.CardSpec(id='test_id', title='test_title')
163
+ chart_spec = formatter.ChartSpec(
164
+ 'test_chart_id', 'test_chart_json', 'test_chart_description'
165
+ )
166
+ card_html = ET.fromstring(
167
+ formatter.create_card_html(
168
+ template_env, card_spec, 'test_insights', [chart_spec]
169
+ )
170
+ )
171
+ chart_html = card_html[2]
172
+ self.assertEqual(chart_html.tag, 'charts')
173
+ self.assertEqual(chart_html[0].tag, 'chart')
174
+ self.assertEqual(chart_html[0][0].tag, 'chart-embed')
175
+ self.assertEqual(chart_html[0][1].tag, 'chart-description')
176
+ self.assertContainsSubset('test_chart_description', chart_html[0][1].text)
177
+ self.assertEqual(chart_html[1].tag, 'script')
178
+ self.assertContainsSubset('test_chart_json', chart_html[1].text)
179
+
180
+ def test_create_card_html_mulitple_stats(self):
181
+ template_env = formatter.create_template_env()
182
+ card_spec = formatter.CardSpec(id='test_id', title='test_title')
183
+ stat1 = formatter.StatsSpec(title='stats_title1', stat='test_stat1')
184
+ stat2 = formatter.StatsSpec(title='stats_title2', stat='test_stat2')
185
+ card_html = ET.fromstring(
186
+ formatter.create_card_html(
187
+ template_env, card_spec, 'test_insights', stats_specs=[stat1, stat2]
188
+ )
189
+ )
190
+ stats = card_html[2]
191
+ self.assertLen(stats, 2)
192
+
193
+ def test_create_card_html_stats_structure(self):
194
+ template_env = formatter.create_template_env()
195
+ card_spec = formatter.CardSpec(id='test_id', title='test_title')
196
+ stats_spec = formatter.StatsSpec(
197
+ title='stats_title', stat='test_stat', delta='+0.3'
198
+ )
199
+ card_html = ET.fromstring(
200
+ formatter.create_card_html(
201
+ template_env, card_spec, 'test_insights', stats_specs=[stats_spec]
202
+ )
203
+ )
204
+ stats_html = card_html[2]
205
+ self.assertEqual(stats_html.tag, 'stats-section')
206
+ self.assertEqual(stats_html[0].tag, 'stats')
207
+ self.assertEqual(stats_html[0][0].tag, 'stats-title')
208
+ self.assertEqual(stats_html[0][0].text, 'stats_title')
209
+ self.assertEqual(stats_html[0][1].tag, 'stat')
210
+ self.assertEqual(stats_html[0][1].text, 'test_stat')
211
+ self.assertEqual(stats_html[0][2].tag, 'delta')
212
+ self.assertContainsSubset('+0.3', stats_html[0][2].text)
213
+
214
+
215
+ if __name__ == '__main__':
216
+ absltest.main()
@@ -1,5 +1,5 @@
1
1
  {#
2
- Copyright 2024 Google LLC
2
+ Copyright 2025 Google LLC
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
1
1
  {#
2
- Copyright 2024 Google LLC
2
+ Copyright 2025 Google LLC
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Copyright 2024 Google LLC
2
+ * Copyright 2025 Google LLC
3
3
  *
4
4
  * Licensed under the Apache License, Version 2.0 (the "License");
5
5
  * you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Copyright 2024 Google LLC
2
+ * Copyright 2025 Google LLC
3
3
  *
4
4
  * Licensed under the Apache License, Version 2.0 (the "License");
5
5
  * you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
1
1
  <!--
2
- Copyright 2024 Google LLC
2
+ Copyright 2025 Google LLC
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -42,7 +42,9 @@ limitations under the License.
42
42
  </div>
43
43
  </div>
44
44
 
45
- {% include "chips.html.jinja" %}
45
+ {% if start_date is defined and start_date %}
46
+ {% include "chips.html.jinja" %}
47
+ {% endif %}
46
48
 
47
49
  <cards>
48
50
  {# Each card is laid out in a grid. See; .card display layout. #}
@@ -1,5 +1,5 @@
1
1
  {#
2
- Copyright 2024 Google LLC
2
+ Copyright 2025 Google LLC
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
meridian/version.py CHANGED
@@ -14,4 +14,4 @@
14
14
 
15
15
  """Module for Meridian version."""
16
16
 
17
- __version__ = "1.3.1"
17
+ __version__ = "1.4.0"
@@ -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