google-meridian 1.3.0__py3-none-any.whl → 1.3.2__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/METADATA +209 -0
- google_meridian-1.3.2.dist-info/RECORD +76 -0
- {google_meridian-1.3.0.dist-info → google_meridian-1.3.2.dist-info}/top_level.txt +1 -0
- meridian/analysis/__init__.py +1 -2
- meridian/analysis/analyzer.py +0 -1
- meridian/analysis/optimizer.py +5 -3
- meridian/analysis/review/checks.py +81 -30
- meridian/analysis/review/constants.py +4 -0
- meridian/analysis/review/results.py +40 -9
- meridian/analysis/summarizer.py +1 -1
- meridian/analysis/visualizer.py +1 -1
- meridian/backend/__init__.py +229 -24
- meridian/backend/test_utils.py +194 -0
- meridian/constants.py +1 -0
- meridian/data/load.py +2 -0
- meridian/model/eda/__init__.py +0 -1
- meridian/model/eda/constants.py +12 -2
- meridian/model/eda/eda_engine.py +353 -45
- meridian/model/eda/eda_outcome.py +21 -1
- meridian/model/knots.py +17 -0
- meridian/model/model_test_data.py +15 -0
- meridian/{analysis/templates → templates}/card.html.jinja +1 -1
- meridian/{analysis/templates → templates}/chart.html.jinja +1 -1
- meridian/{analysis/templates → templates}/chips.html.jinja +1 -1
- meridian/{analysis → templates}/formatter.py +12 -1
- meridian/templates/formatter_test.py +216 -0
- meridian/{analysis/templates → templates}/insights.html.jinja +1 -1
- meridian/{analysis/templates → templates}/stats.html.jinja +1 -1
- meridian/{analysis/templates → templates}/style.css +1 -1
- meridian/{analysis/templates → templates}/style.scss +1 -1
- meridian/{analysis/templates → templates}/summary.html.jinja +4 -2
- meridian/{analysis/templates → templates}/table.html.jinja +1 -1
- meridian/version.py +1 -1
- schema/__init__.py +30 -0
- schema/serde/__init__.py +26 -0
- schema/serde/constants.py +48 -0
- schema/serde/distribution.py +515 -0
- schema/serde/eda_spec.py +192 -0
- schema/serde/function_registry.py +143 -0
- schema/serde/hyperparameters.py +363 -0
- schema/serde/inference_data.py +105 -0
- schema/serde/marketing_data.py +1321 -0
- schema/serde/meridian_serde.py +413 -0
- schema/serde/serde.py +47 -0
- schema/serde/test_data.py +4608 -0
- schema/utils/__init__.py +17 -0
- schema/utils/time_record.py +156 -0
- google_meridian-1.3.0.dist-info/METADATA +0 -409
- google_meridian-1.3.0.dist-info/RECORD +0 -62
- meridian/model/eda/meridian_eda.py +0 -220
- {google_meridian-1.3.0.dist-info → google_meridian-1.3.2.dist-info}/WHEEL +0 -0
- {google_meridian-1.3.0.dist-info → google_meridian-1.3.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -29,6 +29,7 @@ __all__ = [
|
|
|
29
29
|
"StandardDeviationArtifact",
|
|
30
30
|
"VIFArtifact",
|
|
31
31
|
"KpiInvariabilityArtifact",
|
|
32
|
+
"CostPerMediaUnitArtifact",
|
|
32
33
|
"EDACheckType",
|
|
33
34
|
"ArtifactType",
|
|
34
35
|
"EDAOutcome",
|
|
@@ -101,7 +102,8 @@ class PairwiseCorrArtifact(AnalysisArtifact):
|
|
|
101
102
|
Attributes:
|
|
102
103
|
corr_matrix: Pairwise correlation matrix.
|
|
103
104
|
extreme_corr_var_pairs: DataFrame of variable pairs exceeding the
|
|
104
|
-
correlation threshold.
|
|
105
|
+
correlation threshold. Includes 'correlation' and 'abs_correlation'
|
|
106
|
+
columns, and is sorted by 'abs_correlation' in descending order.
|
|
105
107
|
extreme_corr_threshold: The threshold used to identify extreme correlation
|
|
106
108
|
pairs.
|
|
107
109
|
"""
|
|
@@ -153,6 +155,23 @@ class KpiInvariabilityArtifact(AnalysisArtifact):
|
|
|
153
155
|
kpi_stdev: xr.DataArray
|
|
154
156
|
|
|
155
157
|
|
|
158
|
+
@dataclasses.dataclass(frozen=True)
|
|
159
|
+
class CostPerMediaUnitArtifact(AnalysisArtifact):
|
|
160
|
+
"""Encapsulates artifacts from a Cost per Media Unit analysis.
|
|
161
|
+
|
|
162
|
+
Attributes:
|
|
163
|
+
cost_per_media_unit_da: DataArray of cost per media unit.
|
|
164
|
+
cost_media_unit_inconsistency_df: DataFrame of time periods where cost and
|
|
165
|
+
media units are inconsistent (e.g., zero cost with positive media units,
|
|
166
|
+
or positive cost with zero media units).
|
|
167
|
+
outlier_df: DataFrame with outliers of cost per media unit.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
cost_per_media_unit_da: xr.DataArray
|
|
171
|
+
cost_media_unit_inconsistency_df: pd.DataFrame
|
|
172
|
+
outlier_df: pd.DataFrame
|
|
173
|
+
|
|
174
|
+
|
|
156
175
|
@enum.unique
|
|
157
176
|
class EDACheckType(enum.Enum):
|
|
158
177
|
"""Enumeration for the type of an EDA check."""
|
|
@@ -161,6 +180,7 @@ class EDACheckType(enum.Enum):
|
|
|
161
180
|
STANDARD_DEVIATION = enum.auto()
|
|
162
181
|
MULTICOLLINEARITY = enum.auto()
|
|
163
182
|
KPI_INVARIABILITY = enum.auto()
|
|
183
|
+
COST_PER_MEDIA_UNIT = enum.auto()
|
|
164
184
|
|
|
165
185
|
|
|
166
186
|
ArtifactType = typing.TypeVar("ArtifactType", bound="AnalysisArtifact")
|
meridian/model/knots.py
CHANGED
|
@@ -19,6 +19,7 @@ from collections.abc import Collection, Sequence
|
|
|
19
19
|
import copy
|
|
20
20
|
import dataclasses
|
|
21
21
|
import math
|
|
22
|
+
import pprint
|
|
22
23
|
from typing import Any
|
|
23
24
|
from meridian import constants
|
|
24
25
|
from meridian.data import input_data
|
|
@@ -289,6 +290,22 @@ class AKS:
|
|
|
289
290
|
penalty = geo_scaling_factor * base_penalty
|
|
290
291
|
|
|
291
292
|
aspline = self.aspline(x=x, y=y, knots=knots, penalty=penalty)
|
|
293
|
+
# Ensure defined knot range covers at least one of the available knot sets.
|
|
294
|
+
available_knots_lengths = np.unique(
|
|
295
|
+
np.fromiter(
|
|
296
|
+
(len(x) for x in aspline[constants.KNOTS_SELECTED]), dtype=int
|
|
297
|
+
)
|
|
298
|
+
).tolist()
|
|
299
|
+
if not any(
|
|
300
|
+
min_internal_knots <= k <= max_internal_knots
|
|
301
|
+
for k in available_knots_lengths
|
|
302
|
+
):
|
|
303
|
+
raise ValueError(
|
|
304
|
+
f'The range [{min_internal_knots}, {max_internal_knots}] does not'
|
|
305
|
+
' contain any of the available knot lengths:'
|
|
306
|
+
f' {pprint.pformat(available_knots_lengths)}'
|
|
307
|
+
)
|
|
308
|
+
|
|
292
309
|
n_knots = np.array([len(x) for x in aspline[constants.KNOTS_SELECTED]])
|
|
293
310
|
feasible_idx = np.where(
|
|
294
311
|
(n_knots >= min_internal_knots) & (n_knots <= max_internal_knots)
|
|
@@ -143,6 +143,7 @@ class WithInputDataSamples:
|
|
|
143
143
|
_short_input_data_with_rf_only: input_data.InputData
|
|
144
144
|
_short_input_data_with_media_and_rf: input_data.InputData
|
|
145
145
|
_national_input_data_media_only: input_data.InputData
|
|
146
|
+
_national_input_data_rf_only: input_data.InputData
|
|
146
147
|
_national_input_data_media_and_rf: input_data.InputData
|
|
147
148
|
_test_dist_media_and_rf: collections.OrderedDict[str, backend.Tensor]
|
|
148
149
|
_test_dist_media_only: collections.OrderedDict[str, backend.Tensor]
|
|
@@ -282,6 +283,16 @@ class WithInputDataSamples:
|
|
|
282
283
|
seed=0,
|
|
283
284
|
)
|
|
284
285
|
)
|
|
286
|
+
cls._national_input_data_rf_only = (
|
|
287
|
+
test_utils.sample_input_data_non_revenue_revenue_per_kpi(
|
|
288
|
+
n_geos=cls._N_GEOS_NATIONAL,
|
|
289
|
+
n_times=cls._N_TIMES,
|
|
290
|
+
n_media_times=cls._N_MEDIA_TIMES,
|
|
291
|
+
n_controls=cls._N_CONTROLS,
|
|
292
|
+
n_rf_channels=cls._N_RF_CHANNELS,
|
|
293
|
+
seed=0,
|
|
294
|
+
)
|
|
295
|
+
)
|
|
285
296
|
cls._national_input_data_media_only = (
|
|
286
297
|
test_utils.sample_input_data_non_revenue_revenue_per_kpi(
|
|
287
298
|
n_geos=cls._N_GEOS_NATIONAL,
|
|
@@ -581,6 +592,10 @@ class WithInputDataSamples:
|
|
|
581
592
|
def national_input_data_media_only(self) -> input_data.InputData:
|
|
582
593
|
return self._national_input_data_media_only.copy(deep=True)
|
|
583
594
|
|
|
595
|
+
@property
|
|
596
|
+
def national_input_data_rf_only(self) -> input_data.InputData:
|
|
597
|
+
return self._national_input_data_rf_only.copy(deep=True)
|
|
598
|
+
|
|
584
599
|
@property
|
|
585
600
|
def national_input_data_media_and_rf(self) -> input_data.InputData:
|
|
586
601
|
return self._national_input_data_media_and_rf.copy(deep=True)
|
|
@@ -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__))
|
|
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
|
|
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
|
-
{%
|
|
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. #}
|
meridian/version.py
CHANGED
schema/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
"""Module containing MMM schema library."""
|
|
16
|
+
|
|
17
|
+
try: # pylint: disable=g-statement-before-imports
|
|
18
|
+
# A quick check for schema dependencies.
|
|
19
|
+
# If this fails, it's likely because meridian was installed without
|
|
20
|
+
# `pip install google-meridian[schema]`.
|
|
21
|
+
from mmm.v1.model.meridian import meridian_model_pb2 # pylint: disable=g-import-not-at-top
|
|
22
|
+
except ModuleNotFoundError as exc:
|
|
23
|
+
raise ImportError(
|
|
24
|
+
'Schema dependencies not found. Please install meridian with '
|
|
25
|
+
'`pip install google-meridian[schema]`.'
|
|
26
|
+
) from exc
|
|
27
|
+
|
|
28
|
+
# pylint: disable=g-import-not-at-top
|
|
29
|
+
from schema import serde
|
|
30
|
+
from schema import utils
|
schema/serde/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
"""A serialization and deserialization library for Meridian models.
|
|
16
|
+
|
|
17
|
+
For entry points API, see `meridian_serde` module docs.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from schema.serde import constants
|
|
21
|
+
from schema.serde import distribution
|
|
22
|
+
from schema.serde import eda_spec
|
|
23
|
+
from schema.serde import hyperparameters
|
|
24
|
+
from schema.serde import inference_data
|
|
25
|
+
from schema.serde import meridian_serde
|
|
26
|
+
from schema.serde import serde
|
|
@@ -0,0 +1,48 @@
|
|
|
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 across the Meridian serde library."""
|
|
16
|
+
|
|
17
|
+
# Constants for hyperparameters protobuf structure
|
|
18
|
+
BASELINE_GEO_ONEOF = 'baseline_geo_oneof'
|
|
19
|
+
BASELINE_GEO_INT = 'baseline_geo_int'
|
|
20
|
+
BASELINE_GEO_STRING = 'baseline_geo_string'
|
|
21
|
+
CONTROL_POPULATION_SCALING_ID = 'control_population_scaling_id'
|
|
22
|
+
HOLDOUT_ID = 'holdout_id'
|
|
23
|
+
NON_MEDIA_POPULATION_SCALING_ID = 'non_media_population_scaling_id'
|
|
24
|
+
ADSTOCK_DECAY_SPEC = 'adstock_decay_spec'
|
|
25
|
+
GLOBAL_ADSTOCK_DECAY = 'global_adstock_decay'
|
|
26
|
+
ADSTOCK_DECAY_BY_CHANNEL = 'adstock_decay_by_channel'
|
|
27
|
+
DEFAULT_DECAY = 'geometric'
|
|
28
|
+
|
|
29
|
+
# Constants for marketing data protobuf structure
|
|
30
|
+
GEO_INFO = 'geo_info'
|
|
31
|
+
METADATA = 'metadata'
|
|
32
|
+
REACH_FREQUENCY = 'reach_frequency'
|
|
33
|
+
|
|
34
|
+
# Constants for distribution protobuf structure
|
|
35
|
+
DISTRIBUTION_TYPE = 'distribution_type'
|
|
36
|
+
BATCH_BROADCAST_DISTRIBUTION = 'batch_broadcast'
|
|
37
|
+
DETERMINISTIC_DISTRIBUTION = 'deterministic'
|
|
38
|
+
HALF_NORMAL_DISTRIBUTION = 'half_normal'
|
|
39
|
+
LOG_NORMAL_DISTRIBUTION = 'log_normal'
|
|
40
|
+
NORMAL_DISTRIBUTION = 'normal'
|
|
41
|
+
TRANSFORMED_DISTRIBUTION = 'transformed'
|
|
42
|
+
TRUNCATED_NORMAL_DISTRIBUTION = 'truncated_normal'
|
|
43
|
+
UNIFORM_DISTRIBUTION = 'uniform'
|
|
44
|
+
BETA_DISTRIBUTION = 'beta'
|
|
45
|
+
BIJECTOR_TYPE = 'bijector_type'
|
|
46
|
+
SHIFT_BIJECTOR = 'shift'
|
|
47
|
+
SCALE_BIJECTOR = 'scale'
|
|
48
|
+
RECIPROCAL_BIJECTOR = 'reciprocal'
|