google-meridian 1.1.2__tar.gz → 1.1.4__tar.gz
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.1.2/google_meridian.egg-info → google_meridian-1.1.4}/PKG-INFO +2 -2
- {google_meridian-1.1.2 → google_meridian-1.1.4}/README.md +1 -1
- {google_meridian-1.1.2 → google_meridian-1.1.4/google_meridian.egg-info}/PKG-INFO +2 -2
- {google_meridian-1.1.2 → google_meridian-1.1.4}/google_meridian.egg-info/SOURCES.txt +1 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/__init__.py +6 -4
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/analysis/analyzer.py +68 -25
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/analysis/optimizer.py +298 -48
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/constants.py +3 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/data/data_frame_input_data_builder.py +41 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/data/input_data_builder.py +12 -4
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/data/load.py +262 -346
- google_meridian-1.1.4/meridian/mlflow/autolog.py +206 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/model/media.py +7 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/model/model.py +14 -16
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/model/posterior_sampler.py +13 -9
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/model/prior_sampler.py +4 -6
- google_meridian-1.1.4/meridian/version.py +17 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/pyproject.toml +1 -1
- google_meridian-1.1.2/meridian/mlflow/autolog.py +0 -54
- {google_meridian-1.1.2 → google_meridian-1.1.4}/LICENSE +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/MANIFEST.in +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/google_meridian.egg-info/dependency_links.txt +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/google_meridian.egg-info/requires.txt +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/google_meridian.egg-info/top_level.txt +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/analysis/__init__.py +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/analysis/formatter.py +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/analysis/summarizer.py +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/analysis/summary_text.py +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/analysis/templates/card.html.jinja +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/analysis/templates/chart.html.jinja +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/analysis/templates/chips.html.jinja +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/analysis/templates/insights.html.jinja +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/analysis/templates/stats.html.jinja +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/analysis/templates/style.scss +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/analysis/templates/summary.html.jinja +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/analysis/templates/table.html.jinja +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/analysis/test_utils.py +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/analysis/visualizer.py +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/data/__init__.py +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/data/arg_builder.py +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/data/input_data.py +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/data/nd_array_input_data_builder.py +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/data/test_utils.py +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/data/time_coordinates.py +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/mlflow/__init__.py +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/model/__init__.py +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/model/adstock_hill.py +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/model/knots.py +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/model/model_test_data.py +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/model/prior_distribution.py +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/model/spec.py +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/meridian/model/transformers.py +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/setup.cfg +0 -0
- {google_meridian-1.1.2 → google_meridian-1.1.4}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: google-meridian
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.4
|
|
4
4
|
Summary: Google's open source mixed marketing model library, helps you understand your return on investment and direct your ad spend with confidence.
|
|
5
5
|
Author-email: The Meridian Authors <no-reply@google.com>
|
|
6
6
|
License:
|
|
@@ -397,7 +397,7 @@ To cite this repository:
|
|
|
397
397
|
author = {Google Meridian Marketing Mix Modeling Team},
|
|
398
398
|
title = {Meridian: Marketing Mix Modeling},
|
|
399
399
|
url = {https://github.com/google/meridian},
|
|
400
|
-
version = {1.1.
|
|
400
|
+
version = {1.1.4},
|
|
401
401
|
year = {2025},
|
|
402
402
|
}
|
|
403
403
|
```
|
|
@@ -151,7 +151,7 @@ To cite this repository:
|
|
|
151
151
|
author = {Google Meridian Marketing Mix Modeling Team},
|
|
152
152
|
title = {Meridian: Marketing Mix Modeling},
|
|
153
153
|
url = {https://github.com/google/meridian},
|
|
154
|
-
version = {1.1.
|
|
154
|
+
version = {1.1.4},
|
|
155
155
|
year = {2025},
|
|
156
156
|
}
|
|
157
157
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: google-meridian
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.4
|
|
4
4
|
Summary: Google's open source mixed marketing model library, helps you understand your return on investment and direct your ad spend with confidence.
|
|
5
5
|
Author-email: The Meridian Authors <no-reply@google.com>
|
|
6
6
|
License:
|
|
@@ -397,7 +397,7 @@ To cite this repository:
|
|
|
397
397
|
author = {Google Meridian Marketing Mix Modeling Team},
|
|
398
398
|
title = {Meridian: Marketing Mix Modeling},
|
|
399
399
|
url = {https://github.com/google/meridian},
|
|
400
|
-
version = {1.1.
|
|
400
|
+
version = {1.1.4},
|
|
401
401
|
year = {2025},
|
|
402
402
|
}
|
|
403
403
|
```
|
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
"""Meridian API."""
|
|
16
|
-
|
|
17
|
-
__version__ = "1.1.2"
|
|
18
|
-
|
|
19
|
-
|
|
20
16
|
from meridian import analysis
|
|
21
17
|
from meridian import data
|
|
22
18
|
from meridian import model
|
|
19
|
+
from meridian.version import __version__
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
from meridian import mlflow # pylint: disable=g-import-not-at-top
|
|
23
|
+
except ImportError:
|
|
24
|
+
pass
|
|
@@ -59,14 +59,20 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
59
59
|
Attributes:
|
|
60
60
|
media: Optional tensor with dimensions `(n_geos, T, n_media_channels)` for
|
|
61
61
|
any time dimension `T`.
|
|
62
|
-
media_spend: Optional tensor with dimensions `(
|
|
63
|
-
for any time dimension `T`.
|
|
62
|
+
media_spend: Optional tensor with dimensions `(n_media_channels,)` or
|
|
63
|
+
`(n_geos, T, n_media_channels)` for any time dimension `T`. If the object
|
|
64
|
+
includes variables with modified time periods, then this tensor must be
|
|
65
|
+
provided at the geo and time granularity.
|
|
64
66
|
reach: Optional tensor with dimensions `(n_geos, T, n_rf_channels)` for any
|
|
65
67
|
time dimension `T`.
|
|
66
68
|
frequency: Optional tensor with dimensions `(n_geos, T, n_rf_channels)` for
|
|
67
69
|
any time dimension `T`.
|
|
68
|
-
|
|
69
|
-
any time dimension `T`.
|
|
70
|
+
rf_impressions: Optional tensor with dimensions `(n_geos, T, n_rf_channels)`
|
|
71
|
+
for any time dimension `T`.
|
|
72
|
+
rf_spend: Optional tensor with dimensions `(n_rf_channels,)` or `(n_geos, T,
|
|
73
|
+
n_rf_channels)` for any time dimension `T`. If the object includes
|
|
74
|
+
variables with modified time periods, then this tensor must be provided at
|
|
75
|
+
the geo and time granularity.
|
|
70
76
|
organic_media: Optional tensor with dimensions `(n_geos, T,
|
|
71
77
|
n_organic_media_channels)` for any time dimension `T`.
|
|
72
78
|
organic_reach: Optional tensor with dimensions `(n_geos, T,
|
|
@@ -86,6 +92,7 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
86
92
|
media_spend: Optional[tf.Tensor]
|
|
87
93
|
reach: Optional[tf.Tensor]
|
|
88
94
|
frequency: Optional[tf.Tensor]
|
|
95
|
+
rf_impressions: Optional[tf.Tensor]
|
|
89
96
|
rf_spend: Optional[tf.Tensor]
|
|
90
97
|
organic_media: Optional[tf.Tensor]
|
|
91
98
|
organic_reach: Optional[tf.Tensor]
|
|
@@ -101,6 +108,7 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
101
108
|
media_spend: Optional[tf.Tensor] = None,
|
|
102
109
|
reach: Optional[tf.Tensor] = None,
|
|
103
110
|
frequency: Optional[tf.Tensor] = None,
|
|
111
|
+
rf_impressions: Optional[tf.Tensor] = None,
|
|
104
112
|
rf_spend: Optional[tf.Tensor] = None,
|
|
105
113
|
organic_media: Optional[tf.Tensor] = None,
|
|
106
114
|
organic_reach: Optional[tf.Tensor] = None,
|
|
@@ -118,6 +126,11 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
118
126
|
self.frequency = (
|
|
119
127
|
tf.cast(frequency, tf.float32) if frequency is not None else None
|
|
120
128
|
)
|
|
129
|
+
self.rf_impressions = (
|
|
130
|
+
tf.cast(rf_impressions, tf.float32)
|
|
131
|
+
if rf_impressions is not None
|
|
132
|
+
else None
|
|
133
|
+
)
|
|
121
134
|
self.rf_spend = (
|
|
122
135
|
tf.cast(rf_spend, tf.float32) if rf_spend is not None else None
|
|
123
136
|
)
|
|
@@ -189,7 +202,10 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
189
202
|
"""
|
|
190
203
|
for field in self._tf_extension_type_fields():
|
|
191
204
|
new_tensor = getattr(self, field.name)
|
|
192
|
-
|
|
205
|
+
if field.name == constants.RF_IMPRESSIONS:
|
|
206
|
+
old_tensor = getattr(meridian.rf_tensors, field.name)
|
|
207
|
+
else:
|
|
208
|
+
old_tensor = getattr(meridian.input_data, field.name)
|
|
193
209
|
# The time dimension is always the second dimension, except for when spend
|
|
194
210
|
# data is provided with only one dimension of (n_channels).
|
|
195
211
|
if (
|
|
@@ -293,7 +309,13 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
293
309
|
"This is not supported and will be ignored."
|
|
294
310
|
)
|
|
295
311
|
if field.name in required_variables:
|
|
296
|
-
if
|
|
312
|
+
if field.name == constants.RF_IMPRESSIONS:
|
|
313
|
+
if meridian.n_rf_channels == 0:
|
|
314
|
+
raise ValueError(
|
|
315
|
+
"New `rf_impressions` is not allowed because there are no R&F"
|
|
316
|
+
" channels in the Meridian model."
|
|
317
|
+
)
|
|
318
|
+
elif getattr(meridian.input_data, field.name) is None:
|
|
297
319
|
raise ValueError(
|
|
298
320
|
f"New `{field.name}` is not allowed because the input data to the"
|
|
299
321
|
f" Meridian model does not contain `{field.name}`."
|
|
@@ -322,7 +344,10 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
322
344
|
if var_name in [constants.REVENUE_PER_KPI, constants.TIME]:
|
|
323
345
|
continue
|
|
324
346
|
new_tensor = getattr(self, var_name)
|
|
325
|
-
|
|
347
|
+
if var_name == constants.RF_IMPRESSIONS:
|
|
348
|
+
old_tensor = getattr(meridian.rf_tensors, var_name)
|
|
349
|
+
else:
|
|
350
|
+
old_tensor = getattr(meridian.input_data, var_name)
|
|
326
351
|
if new_tensor is not None:
|
|
327
352
|
assert old_tensor is not None
|
|
328
353
|
if new_tensor.shape[-1] != old_tensor.shape[-1]:
|
|
@@ -337,7 +362,10 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
337
362
|
"""Validates the time dimension of the specified data variables."""
|
|
338
363
|
for var_name in required_fields:
|
|
339
364
|
new_tensor = getattr(self, var_name)
|
|
340
|
-
|
|
365
|
+
if var_name == constants.RF_IMPRESSIONS:
|
|
366
|
+
old_tensor = getattr(meridian.rf_tensors, var_name)
|
|
367
|
+
else:
|
|
368
|
+
old_tensor = getattr(meridian.input_data, var_name)
|
|
341
369
|
|
|
342
370
|
# Skip spend data with only 1 dimension of (n_channels).
|
|
343
371
|
if (
|
|
@@ -375,17 +403,13 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
375
403
|
missing_params = []
|
|
376
404
|
for var_name in required_fields:
|
|
377
405
|
new_tensor = getattr(self, var_name)
|
|
378
|
-
|
|
406
|
+
if var_name == constants.RF_IMPRESSIONS:
|
|
407
|
+
old_tensor = getattr(meridian.rf_tensors, var_name)
|
|
408
|
+
else:
|
|
409
|
+
old_tensor = getattr(meridian.input_data, var_name)
|
|
379
410
|
|
|
380
411
|
if old_tensor is None:
|
|
381
412
|
continue
|
|
382
|
-
# Skip spend data with only 1 dimension of (n_channels).
|
|
383
|
-
if (
|
|
384
|
-
var_name in [constants.MEDIA_SPEND, constants.RF_SPEND]
|
|
385
|
-
and new_tensor is not None
|
|
386
|
-
and new_tensor.ndim == 1
|
|
387
|
-
):
|
|
388
|
-
continue
|
|
389
413
|
|
|
390
414
|
if new_tensor is None:
|
|
391
415
|
missing_params.append(var_name)
|
|
@@ -397,6 +421,16 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
397
421
|
"time periods, which does not match the modified number of time "
|
|
398
422
|
f"periods, {new_n_times}.",
|
|
399
423
|
)
|
|
424
|
+
elif (
|
|
425
|
+
var_name in [constants.MEDIA_SPEND, constants.RF_SPEND]
|
|
426
|
+
and new_tensor.ndim == 1
|
|
427
|
+
):
|
|
428
|
+
raise ValueError(
|
|
429
|
+
"If the time dimension of any variable in `new_data` is modified, "
|
|
430
|
+
"then spend variables must be provided at the geo and time "
|
|
431
|
+
"granularity with thee same number of time periods as the other "
|
|
432
|
+
f"new data variables. Found `{var_name}` with only 1 dimension."
|
|
433
|
+
)
|
|
400
434
|
elif new_tensor.ndim > 1 and new_tensor.shape[1] != new_n_times:
|
|
401
435
|
raise ValueError(
|
|
402
436
|
"If the time dimension of any variable in `new_data` is "
|
|
@@ -3415,6 +3449,7 @@ class Analyzer:
|
|
|
3415
3449
|
def optimal_freq(
|
|
3416
3450
|
self,
|
|
3417
3451
|
new_data: DataTensors | None = None,
|
|
3452
|
+
max_frequency: float | None = None,
|
|
3418
3453
|
freq_grid: Sequence[float] | None = None,
|
|
3419
3454
|
use_posterior: bool = True,
|
|
3420
3455
|
use_kpi: bool = False,
|
|
@@ -3443,7 +3478,7 @@ class Analyzer:
|
|
|
3443
3478
|
ROI numerator is KPI units.
|
|
3444
3479
|
|
|
3445
3480
|
Args:
|
|
3446
|
-
new_data: Optional `DataTensors` object containing `
|
|
3481
|
+
new_data: Optional `DataTensors` object containing `rf_impressions`,
|
|
3447
3482
|
`rf_spend`, and `revenue_per_kpi`. If provided, the optimal frequency is
|
|
3448
3483
|
calculated using the values of the tensors passed in `new_data` and the
|
|
3449
3484
|
original values of all the remaining tensors. If `None`, the historical
|
|
@@ -3451,6 +3486,10 @@ class Analyzer:
|
|
|
3451
3486
|
tensors in `new_data` is provided with a different number of time
|
|
3452
3487
|
periods than in `InputData`, then all tensors must be provided with the
|
|
3453
3488
|
same number of time periods.
|
|
3489
|
+
max_frequency: Maximum frequency value used to calculate the frequency
|
|
3490
|
+
grid. If `None`, the maximum frequency value is calculated from the
|
|
3491
|
+
historic frequency (maximum value of Meridian.input_data, not
|
|
3492
|
+
`new_data`). If `freq_grid` is provided, this argument has no effect.
|
|
3454
3493
|
freq_grid: List of frequency values. The ROI of each channel is calculated
|
|
3455
3494
|
for each frequency value in the list. By default, the list includes
|
|
3456
3495
|
numbers from `1.0` to the maximum frequency in increments of `0.1`.
|
|
@@ -3506,7 +3545,11 @@ class Analyzer:
|
|
|
3506
3545
|
)
|
|
3507
3546
|
|
|
3508
3547
|
filled_data = new_data.validate_and_fill_missing_data(
|
|
3509
|
-
|
|
3548
|
+
[
|
|
3549
|
+
constants.RF_IMPRESSIONS,
|
|
3550
|
+
constants.RF_SPEND,
|
|
3551
|
+
constants.REVENUE_PER_KPI,
|
|
3552
|
+
],
|
|
3510
3553
|
self._meridian,
|
|
3511
3554
|
)
|
|
3512
3555
|
# TODO: Once treatment type filtering is added, remove adding
|
|
@@ -3527,7 +3570,9 @@ class Analyzer:
|
|
|
3527
3570
|
(self._meridian.n_geos, n_times, self._meridian.n_media_channels)
|
|
3528
3571
|
)
|
|
3529
3572
|
|
|
3530
|
-
max_freq = np.max(
|
|
3573
|
+
max_freq = max_frequency or np.max(
|
|
3574
|
+
np.array(self._meridian.rf_tensors.frequency)
|
|
3575
|
+
)
|
|
3531
3576
|
if freq_grid is None:
|
|
3532
3577
|
freq_grid = np.arange(1, max_freq, 0.1)
|
|
3533
3578
|
|
|
@@ -3537,8 +3582,8 @@ class Analyzer:
|
|
|
3537
3582
|
metric_grid = np.zeros((len(freq_grid), self._meridian.n_rf_channels, 4))
|
|
3538
3583
|
|
|
3539
3584
|
for i, freq in enumerate(freq_grid):
|
|
3540
|
-
new_frequency = tf.ones_like(filled_data.
|
|
3541
|
-
new_reach = filled_data.
|
|
3585
|
+
new_frequency = tf.ones_like(filled_data.rf_impressions) * freq
|
|
3586
|
+
new_reach = filled_data.rf_impressions / new_frequency
|
|
3542
3587
|
new_roi_data = DataTensors(
|
|
3543
3588
|
reach=new_reach,
|
|
3544
3589
|
frequency=new_frequency,
|
|
@@ -3568,12 +3613,10 @@ class Analyzer:
|
|
|
3568
3613
|
|
|
3569
3614
|
optimal_frequency = [freq_grid[i] for i in optimal_freq_idx]
|
|
3570
3615
|
optimal_frequency_tensor = tf.convert_to_tensor(
|
|
3571
|
-
tf.ones_like(filled_data.
|
|
3616
|
+
tf.ones_like(filled_data.rf_impressions) * optimal_frequency,
|
|
3572
3617
|
tf.float32,
|
|
3573
3618
|
)
|
|
3574
|
-
optimal_reach =
|
|
3575
|
-
filled_data.frequency * filled_data.reach / optimal_frequency_tensor
|
|
3576
|
-
)
|
|
3619
|
+
optimal_reach = filled_data.rf_impressions / optimal_frequency_tensor
|
|
3577
3620
|
|
|
3578
3621
|
new_summary_metrics_data = DataTensors(
|
|
3579
3622
|
reach=optimal_reach,
|