google-meridian 1.1.1__py3-none-any.whl → 1.1.3__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.1.1.dist-info → google_meridian-1.1.3.dist-info}/METADATA +6 -2
- {google_meridian-1.1.1.dist-info → google_meridian-1.1.3.dist-info}/RECORD +23 -17
- meridian/__init__.py +6 -4
- meridian/analysis/analyzer.py +61 -19
- meridian/analysis/optimizer.py +75 -44
- meridian/analysis/visualizer.py +15 -5
- meridian/constants.py +1 -0
- meridian/data/__init__.py +3 -0
- meridian/data/data_frame_input_data_builder.py +614 -0
- meridian/data/input_data_builder.py +823 -0
- meridian/data/load.py +138 -402
- meridian/data/nd_array_input_data_builder.py +509 -0
- meridian/mlflow/__init__.py +17 -0
- meridian/mlflow/autolog.py +206 -0
- meridian/model/media.py +7 -0
- meridian/model/model.py +32 -26
- meridian/model/posterior_sampler.py +13 -9
- meridian/model/prior_sampler.py +4 -6
- meridian/model/spec.py +17 -7
- meridian/version.py +17 -0
- {google_meridian-1.1.1.dist-info → google_meridian-1.1.3.dist-info}/WHEEL +0 -0
- {google_meridian-1.1.1.dist-info → google_meridian-1.1.3.dist-info}/licenses/LICENSE +0 -0
- {google_meridian-1.1.1.dist-info → google_meridian-1.1.3.dist-info}/top_level.txt +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.3
|
|
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:
|
|
@@ -222,6 +222,7 @@ Requires-Dist: arviz
|
|
|
222
222
|
Requires-Dist: altair>=5
|
|
223
223
|
Requires-Dist: immutabledict
|
|
224
224
|
Requires-Dist: joblib
|
|
225
|
+
Requires-Dist: natsort<8,>=7.1.1
|
|
225
226
|
Requires-Dist: numpy<3,>=2.0.2
|
|
226
227
|
Requires-Dist: pandas<3,>=2.2.2
|
|
227
228
|
Requires-Dist: scipy<2,>=1.13.1
|
|
@@ -236,8 +237,11 @@ Requires-Dist: pylint>=2.6.0; extra == "dev"
|
|
|
236
237
|
Requires-Dist: pyink; extra == "dev"
|
|
237
238
|
Provides-Extra: colab
|
|
238
239
|
Requires-Dist: psutil; extra == "colab"
|
|
240
|
+
Requires-Dist: python-calamine; extra == "colab"
|
|
239
241
|
Provides-Extra: and-cuda
|
|
240
242
|
Requires-Dist: tensorflow[and-cuda]<2.19,>=2.18; extra == "and-cuda"
|
|
243
|
+
Provides-Extra: mlflow
|
|
244
|
+
Requires-Dist: mlflow; extra == "mlflow"
|
|
241
245
|
Dynamic: license-file
|
|
242
246
|
|
|
243
247
|
# About Meridian
|
|
@@ -393,7 +397,7 @@ To cite this repository:
|
|
|
393
397
|
author = {Google Meridian Marketing Mix Modeling Team},
|
|
394
398
|
title = {Meridian: Marketing Mix Modeling},
|
|
395
399
|
url = {https://github.com/google/meridian},
|
|
396
|
-
version = {1.1.
|
|
400
|
+
version = {1.1.3},
|
|
397
401
|
year = {2025},
|
|
398
402
|
}
|
|
399
403
|
```
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
google_meridian-1.1.
|
|
2
|
-
meridian/__init__.py,sha256=
|
|
3
|
-
meridian/constants.py,sha256=
|
|
1
|
+
google_meridian-1.1.3.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
|
|
2
|
+
meridian/__init__.py,sha256=XROKwHNVQvEa371QCXAHik5wN_YKObOdJQX9bJ2c4M4,832
|
|
3
|
+
meridian/constants.py,sha256=VAVHyGfm9FyDd0dWomfqK5XYDUt9qJx7SAM4rzDh3RQ,17195
|
|
4
|
+
meridian/version.py,sha256=CUTXDDaOfXFTukX_ywPK6Q3PiK9hMyJbmJRBeb5ez7c,644
|
|
4
5
|
meridian/analysis/__init__.py,sha256=nGBYz7k9FVdadO_WVGMKJcfq7Yy_TuuP8zgee4i9pSA,836
|
|
5
|
-
meridian/analysis/analyzer.py,sha256=
|
|
6
|
+
meridian/analysis/analyzer.py,sha256=FY_SvnkmEqqCIS37UXB3bvaQi-U3BwLcSWhH1puTzdQ,206003
|
|
6
7
|
meridian/analysis/formatter.py,sha256=ENIdR1CRiaVqIGEXx1HcnsA4ewgDD_nhsYCweJAThaw,7270
|
|
7
|
-
meridian/analysis/optimizer.py,sha256=
|
|
8
|
+
meridian/analysis/optimizer.py,sha256=P4uMcV9ByqMapqa1TEqcnu-3NyTH9fR8QLszdKxRAFc,107801
|
|
8
9
|
meridian/analysis/summarizer.py,sha256=IthOUTMufGvAvbxiDhaKwe7uYCyiTyiQ8vgdmUtdevs,18855
|
|
9
10
|
meridian/analysis/summary_text.py,sha256=I_smDkZJYp2j77ea-9AIbgeraDa7-qUYyb-IthP2qO4,12438
|
|
10
11
|
meridian/analysis/test_utils.py,sha256=ES1r1akhRjD4pf2oTaGqzDfGNu9weAcLv6UZRuIkfEc,77699
|
|
11
|
-
meridian/analysis/visualizer.py,sha256=
|
|
12
|
+
meridian/analysis/visualizer.py,sha256=hVY0JxDZSgK7ekav3jTYBfxXXn-J0g7uQWMtEj3obx4,94512
|
|
12
13
|
meridian/analysis/templates/card.html.jinja,sha256=pv4MVbQ25CcvtZY-LH7bFW0OSeHobkeEkAleB1sfQ14,1284
|
|
13
14
|
meridian/analysis/templates/chart.html.jinja,sha256=87i0xnXHRBoLLxBpKv2i960TLToWq4r1aVQZqaXIeMQ,1086
|
|
14
15
|
meridian/analysis/templates/chips.html.jinja,sha256=Az0tQwF_-b03JDLyOzpeH-8fb-6jgJgbNfnUUSm-q6E,645
|
|
@@ -18,24 +19,29 @@ meridian/analysis/templates/style.css,sha256=RODTWc2pXcG9zW3q9SEJpVXgeD-WwQgzLpm
|
|
|
18
19
|
meridian/analysis/templates/style.scss,sha256=nSrZOpcIrVyiL4eC9jLUlxIZtAKZ0Rt8pwfk4H1nMrs,5076
|
|
19
20
|
meridian/analysis/templates/summary.html.jinja,sha256=LuENVDHYIpNo4pzloYaCR2K9XN1Ow6_9oQOcOwD9nGg,1707
|
|
20
21
|
meridian/analysis/templates/table.html.jinja,sha256=mvLMZx92RcD2JAS2w2eZtfYG-6WdfwYVo7pM8TbHp4g,1176
|
|
21
|
-
meridian/data/__init__.py,sha256=
|
|
22
|
+
meridian/data/__init__.py,sha256=StIe-wfYnnbfUbKtZHwnAQcRQUS8XCZk_PCaEzw90Ww,929
|
|
22
23
|
meridian/data/arg_builder.py,sha256=Kqlt88bOqFj6D3xNwvWo4MBwNwcDFHzd-wMfEOmLoPU,3741
|
|
24
|
+
meridian/data/data_frame_input_data_builder.py,sha256=3m6wrcC0psmD2ijsXk3R4uByA0Tu2gJxZBGaTS6Z7Io,22040
|
|
23
25
|
meridian/data/input_data.py,sha256=teJPKTBfW-AzBWgf_fEO_S_Z1J_veqQkCvctINaid6I,39749
|
|
24
|
-
meridian/data/
|
|
26
|
+
meridian/data/input_data_builder.py,sha256=08E_MZLrCzwfjvjPWFVs7o_094vVJ5o6VmbTfrg4NUM,25602
|
|
27
|
+
meridian/data/load.py,sha256=B-12fBhsghN7wj0A9IWyT7BVogIXjuUDDvR34JJFwPM,45157
|
|
28
|
+
meridian/data/nd_array_input_data_builder.py,sha256=lfpmnENGuSGKyUd7bDGAwoLqHqteOKmHdKl0VI2wCQA,16341
|
|
25
29
|
meridian/data/test_utils.py,sha256=6GJrPmeaF4uzMxxRgzERGv4g1XMUHwI0s7qDVMZUjuI,55565
|
|
26
30
|
meridian/data/time_coordinates.py,sha256=C5A5fscSLjPH6G9YT8OspgIlCrkMY7y8dMFEt3tNSnE,9874
|
|
31
|
+
meridian/mlflow/__init__.py,sha256=elwXUqPQYi7VF9PYjelU1tydfcUrmtuoq6eJCOnV9bk,693
|
|
32
|
+
meridian/mlflow/autolog.py,sha256=s240eLGAurzaNsulwRlyM1ZdBLvUzyr2eOMYgOyWAzk,6393
|
|
27
33
|
meridian/model/__init__.py,sha256=9NFfqUE5WgFc-9lQMkbfkwwV-bQIz0tsQ_3Jyq0A4SU,982
|
|
28
34
|
meridian/model/adstock_hill.py,sha256=20A_6rbDUAADEkkHspB7JpCm5tYfYS1FQ6hJMLu21Pk,9283
|
|
29
35
|
meridian/model/knots.py,sha256=KPEgnb-UdQQ4QBugOYEke-zBgEghgTmeCMoeiJ30meY,8054
|
|
30
|
-
meridian/model/media.py,sha256=
|
|
31
|
-
meridian/model/model.py,sha256=
|
|
36
|
+
meridian/model/media.py,sha256=3BaPX8xYAFMEvf0mz3mBSCIDWViIs7M218nrCklc6Fk,14099
|
|
37
|
+
meridian/model/model.py,sha256=BlLPyskHrEx5D71mUZFbNxS2VjkQgaiaE6hLKvQ5D3A,61489
|
|
32
38
|
meridian/model/model_test_data.py,sha256=hDDTEzm72LknW9c5E_dNsy4Mm4Tfs6AirhGf_QxykFs,15552
|
|
33
|
-
meridian/model/posterior_sampler.py,sha256=
|
|
39
|
+
meridian/model/posterior_sampler.py,sha256=K49zWTTelME2rL1JLeFAdMPzL0OwrBvyAXA3oR-kgSI,27801
|
|
34
40
|
meridian/model/prior_distribution.py,sha256=IEDU1rabcmKNY8lxwbbO4OUAlMHPIMa7flM_zsu3DLM,42417
|
|
35
|
-
meridian/model/prior_sampler.py,sha256=
|
|
36
|
-
meridian/model/spec.py,sha256=
|
|
41
|
+
meridian/model/prior_sampler.py,sha256=cmu6jG-bSEkYDkjVUxl3iSxrL7r-LN7a77cb2Vc0LoA,23218
|
|
42
|
+
meridian/model/spec.py,sha256=0HNiMQUWQpYvWYOZr1_fj2ah8tH-bEyfEjoqgBZ9Lc0,18049
|
|
37
43
|
meridian/model/transformers.py,sha256=nRjzq1fQG0ypldxboM7Gqok6WSAXAS1witRXoAzeH9Q,7763
|
|
38
|
-
google_meridian-1.1.
|
|
39
|
-
google_meridian-1.1.
|
|
40
|
-
google_meridian-1.1.
|
|
41
|
-
google_meridian-1.1.
|
|
44
|
+
google_meridian-1.1.3.dist-info/METADATA,sha256=5W_XWui7q5gH68OC3Z-PXbDOeBftDbWuhqznNv7fOAk,22201
|
|
45
|
+
google_meridian-1.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
46
|
+
google_meridian-1.1.3.dist-info/top_level.txt,sha256=nwaCebZvvU34EopTKZsjK0OMTFjVnkf4FfnBN_TAc0g,9
|
|
47
|
+
google_meridian-1.1.3.dist-info/RECORD,,
|
meridian/__init__.py
CHANGED
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
"""Meridian API."""
|
|
16
|
-
|
|
17
|
-
__version__ = "1.1.1"
|
|
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
|
meridian/analysis/analyzer.py
CHANGED
|
@@ -65,6 +65,8 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
65
65
|
time dimension `T`.
|
|
66
66
|
frequency: Optional tensor with dimensions `(n_geos, T, n_rf_channels)` for
|
|
67
67
|
any time dimension `T`.
|
|
68
|
+
rf_impressions: Optional tensor with dimensions `(n_geos, T, n_rf_channels)`
|
|
69
|
+
for any time dimension `T`.
|
|
68
70
|
rf_spend: Optional tensor with dimensions `(n_geos, T, n_rf_channels)` for
|
|
69
71
|
any time dimension `T`.
|
|
70
72
|
organic_media: Optional tensor with dimensions `(n_geos, T,
|
|
@@ -86,6 +88,7 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
86
88
|
media_spend: Optional[tf.Tensor]
|
|
87
89
|
reach: Optional[tf.Tensor]
|
|
88
90
|
frequency: Optional[tf.Tensor]
|
|
91
|
+
rf_impressions: Optional[tf.Tensor]
|
|
89
92
|
rf_spend: Optional[tf.Tensor]
|
|
90
93
|
organic_media: Optional[tf.Tensor]
|
|
91
94
|
organic_reach: Optional[tf.Tensor]
|
|
@@ -101,6 +104,7 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
101
104
|
media_spend: Optional[tf.Tensor] = None,
|
|
102
105
|
reach: Optional[tf.Tensor] = None,
|
|
103
106
|
frequency: Optional[tf.Tensor] = None,
|
|
107
|
+
rf_impressions: Optional[tf.Tensor] = None,
|
|
104
108
|
rf_spend: Optional[tf.Tensor] = None,
|
|
105
109
|
organic_media: Optional[tf.Tensor] = None,
|
|
106
110
|
organic_reach: Optional[tf.Tensor] = None,
|
|
@@ -118,6 +122,11 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
118
122
|
self.frequency = (
|
|
119
123
|
tf.cast(frequency, tf.float32) if frequency is not None else None
|
|
120
124
|
)
|
|
125
|
+
self.rf_impressions = (
|
|
126
|
+
tf.cast(rf_impressions, tf.float32)
|
|
127
|
+
if rf_impressions is not None
|
|
128
|
+
else None
|
|
129
|
+
)
|
|
121
130
|
self.rf_spend = (
|
|
122
131
|
tf.cast(rf_spend, tf.float32) if rf_spend is not None else None
|
|
123
132
|
)
|
|
@@ -189,7 +198,10 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
189
198
|
"""
|
|
190
199
|
for field in self._tf_extension_type_fields():
|
|
191
200
|
new_tensor = getattr(self, field.name)
|
|
192
|
-
|
|
201
|
+
if field.name == constants.RF_IMPRESSIONS:
|
|
202
|
+
old_tensor = getattr(meridian.rf_tensors, field.name)
|
|
203
|
+
else:
|
|
204
|
+
old_tensor = getattr(meridian.input_data, field.name)
|
|
193
205
|
# The time dimension is always the second dimension, except for when spend
|
|
194
206
|
# data is provided with only one dimension of (n_channels).
|
|
195
207
|
if (
|
|
@@ -293,7 +305,13 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
293
305
|
"This is not supported and will be ignored."
|
|
294
306
|
)
|
|
295
307
|
if field.name in required_variables:
|
|
296
|
-
if
|
|
308
|
+
if field.name == constants.RF_IMPRESSIONS:
|
|
309
|
+
if meridian.n_rf_channels == 0:
|
|
310
|
+
raise ValueError(
|
|
311
|
+
"New `rf_impressions` is not allowed because there are no R&F"
|
|
312
|
+
" channels in the Meridian model."
|
|
313
|
+
)
|
|
314
|
+
elif getattr(meridian.input_data, field.name) is None:
|
|
297
315
|
raise ValueError(
|
|
298
316
|
f"New `{field.name}` is not allowed because the input data to the"
|
|
299
317
|
f" Meridian model does not contain `{field.name}`."
|
|
@@ -322,7 +340,10 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
322
340
|
if var_name in [constants.REVENUE_PER_KPI, constants.TIME]:
|
|
323
341
|
continue
|
|
324
342
|
new_tensor = getattr(self, var_name)
|
|
325
|
-
|
|
343
|
+
if var_name == constants.RF_IMPRESSIONS:
|
|
344
|
+
old_tensor = getattr(meridian.rf_tensors, var_name)
|
|
345
|
+
else:
|
|
346
|
+
old_tensor = getattr(meridian.input_data, var_name)
|
|
326
347
|
if new_tensor is not None:
|
|
327
348
|
assert old_tensor is not None
|
|
328
349
|
if new_tensor.shape[-1] != old_tensor.shape[-1]:
|
|
@@ -337,7 +358,10 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
337
358
|
"""Validates the time dimension of the specified data variables."""
|
|
338
359
|
for var_name in required_fields:
|
|
339
360
|
new_tensor = getattr(self, var_name)
|
|
340
|
-
|
|
361
|
+
if var_name == constants.RF_IMPRESSIONS:
|
|
362
|
+
old_tensor = getattr(meridian.rf_tensors, var_name)
|
|
363
|
+
else:
|
|
364
|
+
old_tensor = getattr(meridian.input_data, var_name)
|
|
341
365
|
|
|
342
366
|
# Skip spend data with only 1 dimension of (n_channels).
|
|
343
367
|
if (
|
|
@@ -375,7 +399,10 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
375
399
|
missing_params = []
|
|
376
400
|
for var_name in required_fields:
|
|
377
401
|
new_tensor = getattr(self, var_name)
|
|
378
|
-
|
|
402
|
+
if var_name == constants.RF_IMPRESSIONS:
|
|
403
|
+
old_tensor = getattr(meridian.rf_tensors, var_name)
|
|
404
|
+
else:
|
|
405
|
+
old_tensor = getattr(meridian.input_data, var_name)
|
|
379
406
|
|
|
380
407
|
if old_tensor is None:
|
|
381
408
|
continue
|
|
@@ -3415,6 +3442,7 @@ class Analyzer:
|
|
|
3415
3442
|
def optimal_freq(
|
|
3416
3443
|
self,
|
|
3417
3444
|
new_data: DataTensors | None = None,
|
|
3445
|
+
max_frequency: float | None = None,
|
|
3418
3446
|
freq_grid: Sequence[float] | None = None,
|
|
3419
3447
|
use_posterior: bool = True,
|
|
3420
3448
|
use_kpi: bool = False,
|
|
@@ -3443,7 +3471,7 @@ class Analyzer:
|
|
|
3443
3471
|
ROI numerator is KPI units.
|
|
3444
3472
|
|
|
3445
3473
|
Args:
|
|
3446
|
-
new_data: Optional `DataTensors` object containing `
|
|
3474
|
+
new_data: Optional `DataTensors` object containing `rf_impressions`,
|
|
3447
3475
|
`rf_spend`, and `revenue_per_kpi`. If provided, the optimal frequency is
|
|
3448
3476
|
calculated using the values of the tensors passed in `new_data` and the
|
|
3449
3477
|
original values of all the remaining tensors. If `None`, the historical
|
|
@@ -3451,6 +3479,10 @@ class Analyzer:
|
|
|
3451
3479
|
tensors in `new_data` is provided with a different number of time
|
|
3452
3480
|
periods than in `InputData`, then all tensors must be provided with the
|
|
3453
3481
|
same number of time periods.
|
|
3482
|
+
max_frequency: Maximum frequency value used to calculate the frequency
|
|
3483
|
+
grid. If `None`, the maximum frequency value is calculated from the
|
|
3484
|
+
historic frequency (maximum value of Meridian.input_data, not
|
|
3485
|
+
`new_data`). If `freq_grid` is provided, this argument has no effect.
|
|
3454
3486
|
freq_grid: List of frequency values. The ROI of each channel is calculated
|
|
3455
3487
|
for each frequency value in the list. By default, the list includes
|
|
3456
3488
|
numbers from `1.0` to the maximum frequency in increments of `0.1`.
|
|
@@ -3506,7 +3538,11 @@ class Analyzer:
|
|
|
3506
3538
|
)
|
|
3507
3539
|
|
|
3508
3540
|
filled_data = new_data.validate_and_fill_missing_data(
|
|
3509
|
-
|
|
3541
|
+
[
|
|
3542
|
+
constants.RF_IMPRESSIONS,
|
|
3543
|
+
constants.RF_SPEND,
|
|
3544
|
+
constants.REVENUE_PER_KPI,
|
|
3545
|
+
],
|
|
3510
3546
|
self._meridian,
|
|
3511
3547
|
)
|
|
3512
3548
|
# TODO: Once treatment type filtering is added, remove adding
|
|
@@ -3527,7 +3563,9 @@ class Analyzer:
|
|
|
3527
3563
|
(self._meridian.n_geos, n_times, self._meridian.n_media_channels)
|
|
3528
3564
|
)
|
|
3529
3565
|
|
|
3530
|
-
max_freq = np.max(
|
|
3566
|
+
max_freq = max_frequency or np.max(
|
|
3567
|
+
np.array(self._meridian.rf_tensors.frequency)
|
|
3568
|
+
)
|
|
3531
3569
|
if freq_grid is None:
|
|
3532
3570
|
freq_grid = np.arange(1, max_freq, 0.1)
|
|
3533
3571
|
|
|
@@ -3537,8 +3575,8 @@ class Analyzer:
|
|
|
3537
3575
|
metric_grid = np.zeros((len(freq_grid), self._meridian.n_rf_channels, 4))
|
|
3538
3576
|
|
|
3539
3577
|
for i, freq in enumerate(freq_grid):
|
|
3540
|
-
new_frequency = tf.ones_like(filled_data.
|
|
3541
|
-
new_reach = filled_data.
|
|
3578
|
+
new_frequency = tf.ones_like(filled_data.rf_impressions) * freq
|
|
3579
|
+
new_reach = filled_data.rf_impressions / new_frequency
|
|
3542
3580
|
new_roi_data = DataTensors(
|
|
3543
3581
|
reach=new_reach,
|
|
3544
3582
|
frequency=new_frequency,
|
|
@@ -3568,12 +3606,10 @@ class Analyzer:
|
|
|
3568
3606
|
|
|
3569
3607
|
optimal_frequency = [freq_grid[i] for i in optimal_freq_idx]
|
|
3570
3608
|
optimal_frequency_tensor = tf.convert_to_tensor(
|
|
3571
|
-
tf.ones_like(filled_data.
|
|
3609
|
+
tf.ones_like(filled_data.rf_impressions) * optimal_frequency,
|
|
3572
3610
|
tf.float32,
|
|
3573
3611
|
)
|
|
3574
|
-
optimal_reach =
|
|
3575
|
-
filled_data.frequency * filled_data.reach / optimal_frequency_tensor
|
|
3576
|
-
)
|
|
3612
|
+
optimal_reach = filled_data.rf_impressions / optimal_frequency_tensor
|
|
3577
3613
|
|
|
3578
3614
|
new_summary_metrics_data = DataTensors(
|
|
3579
3615
|
reach=optimal_reach,
|
|
@@ -3961,11 +3997,17 @@ class Analyzer:
|
|
|
3961
3997
|
) -> xr.Dataset:
|
|
3962
3998
|
"""Method to generate a response curves xarray.Dataset.
|
|
3963
3999
|
|
|
3964
|
-
Response curves are calculated
|
|
3965
|
-
historical flighting pattern across geos and time periods for
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
4000
|
+
Response curves are calculated in aggregate across geos and time periods,
|
|
4001
|
+
assuming the historical flighting pattern across geos and time periods for
|
|
4002
|
+
each media channel.
|
|
4003
|
+
|
|
4004
|
+
A list of multipliers is applied to each media channel's total historical
|
|
4005
|
+
spend within `selected_geos` and `selected_times` to obtain the x-axis
|
|
4006
|
+
values. The y-axis values are the incremental ouctcome generated by each
|
|
4007
|
+
channel within `selected_geos` and `selected_times` under the counterfactual
|
|
4008
|
+
where media units in each geo and time period are scaled by the
|
|
4009
|
+
corresponding multiplier. (Media units for time periods prior to
|
|
4010
|
+
`selected_times` are also scaled by the multiplier.)
|
|
3969
4011
|
|
|
3970
4012
|
Args:
|
|
3971
4013
|
spend_multipliers: List of multipliers. Each channel's total spend is
|
meridian/analysis/optimizer.py
CHANGED
|
@@ -223,7 +223,7 @@ class OptimizationGrid:
|
|
|
223
223
|
if spend_constraint_upper is None:
|
|
224
224
|
spend_constraint_upper = spend_constraint_default
|
|
225
225
|
(optimization_lower_bound, optimization_upper_bound) = (
|
|
226
|
-
|
|
226
|
+
get_optimization_bounds(
|
|
227
227
|
n_channels=len(self.channels),
|
|
228
228
|
spend=spend,
|
|
229
229
|
round_factor=self.round_factor,
|
|
@@ -1307,36 +1307,57 @@ class BudgetOptimizer:
|
|
|
1307
1307
|
) -> OptimizationResults:
|
|
1308
1308
|
"""Finds the optimal budget allocation that maximizes outcome.
|
|
1309
1309
|
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
`
|
|
1320
|
-
|
|
1321
|
-
`
|
|
1322
|
-
media
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1310
|
+
Define B to be the historical spend of a channel within `selected_geos` and
|
|
1311
|
+
between `start_date` and `end_date`. When the optimization assigns a new
|
|
1312
|
+
budget N to this channel, the historical media units for each geo and time
|
|
1313
|
+
period are assumed to scale by the ratio N / B. Media units prior to
|
|
1314
|
+
`selected_times` are also scaled by N / B. The incremental outcome of each
|
|
1315
|
+
channel is aggregated over `selected_geos` and between `start_date` and
|
|
1316
|
+
`end_date`.
|
|
1317
|
+
|
|
1318
|
+
The incremental outcome includes the (lagged) amount generated between
|
|
1319
|
+
`start_date` and `end_date` by media executed prior to `start_date`, but it
|
|
1320
|
+
excludes the (lagged) amount generated after `end_date` by media executed
|
|
1321
|
+
between `start_date` and `end_date`. This definition does not require any
|
|
1322
|
+
assumptions about media execution levels, media costs, or revenue per kpi
|
|
1323
|
+
for time periods after `end_date`.
|
|
1324
|
+
|
|
1325
|
+
These assumptions are equivalent to assuming that for each channel, neither
|
|
1326
|
+
the flighting pattern nor the cost per media unit depend on the overall
|
|
1327
|
+
budget assigned to that channel.
|
|
1328
|
+
|
|
1329
|
+
The following optimization parameters are assigned default values based on
|
|
1330
|
+
the model input data:
|
|
1331
|
+
1. Flighting pattern. This is the relative allocation of a channel's media
|
|
1332
|
+
units across geos and time periods. By default, the historical flighting
|
|
1333
|
+
pattern is used. The default can be overridden by passing
|
|
1334
|
+
`new_data.media`. The flighting pattern is held constant during
|
|
1335
|
+
optimization and does not depend on the overall budget assigned to the
|
|
1336
|
+
channel.
|
|
1337
|
+
2. Cost per media unit. By default, the historical spend divided by
|
|
1338
|
+
historical media units is used. This can optionally vary by geo or time
|
|
1339
|
+
period or both depending on whether the spend data has geo and time
|
|
1340
|
+
dimensions. The default can be overridden by passing `new_data.spend`.
|
|
1341
|
+
The cost per media unit is held constant during optimization and does not
|
|
1342
|
+
depend on the overall budget assigned to the channel.
|
|
1343
|
+
3. Center of the spend box constraint for each channel. By default, the
|
|
1344
|
+
historical percentage of spend within `selected_geos` and between
|
|
1345
|
+
`start_date` and `end_date` is used. This can be overridden by passing
|
|
1346
|
+
`pct_of_spend`.
|
|
1347
|
+
4. Total budget to be allocated (for fixed budget scenarios only). By
|
|
1348
|
+
default, the historical spend within `selected_geos` and between
|
|
1349
|
+
`start_date` and `end_date` is used. This can be overridden by passing
|
|
1350
|
+
`budget`.
|
|
1351
|
+
|
|
1352
|
+
Passing `new_data.media` (or `new_data.reach` or `new_data.frequency`) will
|
|
1353
|
+
override both the flighting pattern and cost per media unit. Passing
|
|
1354
|
+
`new_data.spend` (or `new_data.rf_spend) will only override the cost per
|
|
1355
|
+
media unit.
|
|
1356
|
+
|
|
1357
|
+
If `start_date` or `end_date` is specified, these values must be selected
|
|
1358
|
+
from `new_data.time` (if provided) or from `Meridian.n_times` (if
|
|
1359
|
+
`new_data.time` is not provided). The `start_date` and `end_date` default to
|
|
1360
|
+
the first and last time periods, respectively.
|
|
1340
1361
|
|
|
1341
1362
|
Args:
|
|
1342
1363
|
new_data: An optional `DataTensors` container with optional tensors:
|
|
@@ -1355,9 +1376,13 @@ class BudgetOptimizer:
|
|
|
1355
1376
|
dimension coordinates for the duration to run the optimization on.
|
|
1356
1377
|
Please Use `start_date` and `end_date` instead.
|
|
1357
1378
|
start_date: Optional start date selector, *inclusive*, in _yyyy-mm-dd_
|
|
1358
|
-
format. Default is
|
|
1379
|
+
format. Default is the first time period of `Meridian.InputData.time` if
|
|
1380
|
+
`new_data` is not provided; otherwise it is the first time period of
|
|
1381
|
+
`new_data.time`.
|
|
1359
1382
|
end_date: Optional end date selector, *inclusive* in _yyyy-mm-dd_ format.
|
|
1360
|
-
Default is
|
|
1383
|
+
Default is the last time period of `Meridian.InputData.time` if
|
|
1384
|
+
`new_data` is not provided; otherwise it is the last time period of
|
|
1385
|
+
`new_data.time`.
|
|
1361
1386
|
fixed_budget: Boolean indicating whether it's a fixed budget optimization
|
|
1362
1387
|
or flexible budget optimization. Defaults to `True`. If `False`, must
|
|
1363
1388
|
specify either `target_roi` or `target_mroi`.
|
|
@@ -1664,7 +1689,7 @@ class BudgetOptimizer:
|
|
|
1664
1689
|
)
|
|
1665
1690
|
spend = budget * valid_pct_of_spend
|
|
1666
1691
|
(optimization_lower_bound, optimization_upper_bound) = (
|
|
1667
|
-
|
|
1692
|
+
get_optimization_bounds(
|
|
1668
1693
|
n_channels=n_channels,
|
|
1669
1694
|
spend=spend,
|
|
1670
1695
|
round_factor=optimization_grid.round_factor,
|
|
@@ -1829,7 +1854,7 @@ class BudgetOptimizer:
|
|
|
1829
1854
|
spend = budget * valid_pct_of_spend
|
|
1830
1855
|
round_factor = _get_round_factor(budget, gtol)
|
|
1831
1856
|
(optimization_lower_bound, optimization_upper_bound) = (
|
|
1832
|
-
|
|
1857
|
+
get_optimization_bounds(
|
|
1833
1858
|
n_channels=n_paid_channels,
|
|
1834
1859
|
spend=spend,
|
|
1835
1860
|
round_factor=round_factor,
|
|
@@ -1838,9 +1863,14 @@ class BudgetOptimizer:
|
|
|
1838
1863
|
)
|
|
1839
1864
|
)
|
|
1840
1865
|
if self._meridian.n_rf_channels > 0 and use_optimal_frequency:
|
|
1866
|
+
opt_freq_data = analyzer.DataTensors(
|
|
1867
|
+
rf_impressions=filled_data.reach * filled_data.frequency,
|
|
1868
|
+
rf_spend=filled_data.rf_spend,
|
|
1869
|
+
revenue_per_kpi=filled_data.revenue_per_kpi,
|
|
1870
|
+
)
|
|
1841
1871
|
optimal_frequency = tf.convert_to_tensor(
|
|
1842
1872
|
self._analyzer.optimal_freq(
|
|
1843
|
-
new_data=
|
|
1873
|
+
new_data=opt_freq_data,
|
|
1844
1874
|
use_posterior=use_posterior,
|
|
1845
1875
|
selected_times=selected_times,
|
|
1846
1876
|
use_kpi=use_kpi,
|
|
@@ -2059,17 +2089,17 @@ class BudgetOptimizer:
|
|
|
2059
2089
|
c.PAID_DATA + (c.TIME,),
|
|
2060
2090
|
self._meridian,
|
|
2061
2091
|
)
|
|
2062
|
-
|
|
2092
|
+
spend_tensor = tf.convert_to_tensor(spend, dtype=tf.float32)
|
|
2063
2093
|
hist_spend = tf.convert_to_tensor(hist_spend, dtype=tf.float32)
|
|
2064
2094
|
(new_media, new_media_spend, new_reach, new_frequency, new_rf_spend) = (
|
|
2065
2095
|
self._get_incremental_outcome_tensors(
|
|
2066
2096
|
hist_spend,
|
|
2067
|
-
|
|
2097
|
+
spend_tensor,
|
|
2068
2098
|
new_data=filled_data.filter_fields(c.PAID_CHANNELS),
|
|
2069
2099
|
optimal_frequency=optimal_frequency,
|
|
2070
2100
|
)
|
|
2071
2101
|
)
|
|
2072
|
-
budget = np.sum(
|
|
2102
|
+
budget = np.sum(spend_tensor)
|
|
2073
2103
|
|
|
2074
2104
|
# incremental_outcome here is a tensor with the shape
|
|
2075
2105
|
# (n_chains, n_draws, n_channels)
|
|
@@ -2123,7 +2153,7 @@ class BudgetOptimizer:
|
|
|
2123
2153
|
)
|
|
2124
2154
|
|
|
2125
2155
|
roi = analyzer.get_central_tendency_and_ci(
|
|
2126
|
-
data=tf.math.divide_no_nan(incremental_outcome,
|
|
2156
|
+
data=tf.math.divide_no_nan(incremental_outcome, spend_tensor),
|
|
2127
2157
|
confidence_level=confidence_level,
|
|
2128
2158
|
include_median=True,
|
|
2129
2159
|
)
|
|
@@ -2148,7 +2178,7 @@ class BudgetOptimizer:
|
|
|
2148
2178
|
)
|
|
2149
2179
|
|
|
2150
2180
|
cpik = analyzer.get_central_tendency_and_ci(
|
|
2151
|
-
data=tf.math.divide_no_nan(
|
|
2181
|
+
data=tf.math.divide_no_nan(spend_tensor, incremental_outcome),
|
|
2152
2182
|
confidence_level=confidence_level,
|
|
2153
2183
|
include_median=True,
|
|
2154
2184
|
)
|
|
@@ -2159,9 +2189,10 @@ class BudgetOptimizer:
|
|
|
2159
2189
|
)
|
|
2160
2190
|
|
|
2161
2191
|
total_spend = np.sum(spend) if np.sum(spend) > 0 else 1
|
|
2192
|
+
pct_of_spend = spend / total_spend
|
|
2162
2193
|
data_vars = {
|
|
2163
|
-
c.SPEND: ([c.CHANNEL], spend),
|
|
2164
|
-
c.PCT_OF_SPEND: ([c.CHANNEL],
|
|
2194
|
+
c.SPEND: ([c.CHANNEL], spend.data),
|
|
2195
|
+
c.PCT_OF_SPEND: ([c.CHANNEL], pct_of_spend.data),
|
|
2165
2196
|
c.INCREMENTAL_OUTCOME: (
|
|
2166
2197
|
[c.CHANNEL, c.METRIC],
|
|
2167
2198
|
incremental_outcome_with_mean_median_and_ci,
|
|
@@ -2510,7 +2541,7 @@ def _get_spend_bounds(
|
|
|
2510
2541
|
return spend_bounds
|
|
2511
2542
|
|
|
2512
2543
|
|
|
2513
|
-
def
|
|
2544
|
+
def get_optimization_bounds(
|
|
2514
2545
|
n_channels: int,
|
|
2515
2546
|
spend: np.ndarray,
|
|
2516
2547
|
round_factor: int,
|
meridian/analysis/visualizer.py
CHANGED
|
@@ -876,9 +876,14 @@ class MediaEffects:
|
|
|
876
876
|
Args:
|
|
877
877
|
confidence_level: Confidence level for modeled response credible
|
|
878
878
|
intervals, represented as a value between zero and one. Default is 0.9.
|
|
879
|
-
selected_times: Optional list
|
|
880
|
-
|
|
881
|
-
|
|
879
|
+
selected_times: Optional list containing a subset of time dimensions to
|
|
880
|
+
include. The x-axis corresponds to spend within these time periods. The
|
|
881
|
+
y-axis corresponds to the incremental outcome generated within these
|
|
882
|
+
time periods under the counterfactual where media units in each geo and
|
|
883
|
+
time period are scaled by the ratio of x-axis spend to historical spend.
|
|
884
|
+
(Media units for time periods prior to to `selected_times` are also
|
|
885
|
+
scaled by this ratio). By default, all times are included. Times should
|
|
886
|
+
match the time dimensions from `meridian.InputData`.
|
|
882
887
|
by_reach: For the channel w/ reach and frequency, return the response
|
|
883
888
|
curves by reach given fixed frequency if true; return the response
|
|
884
889
|
curves by frequency given fixed reach if false.
|
|
@@ -972,8 +977,13 @@ class MediaEffects:
|
|
|
972
977
|
Args:
|
|
973
978
|
confidence_level: Confidence level to update to for the response curve
|
|
974
979
|
credible intervals, represented as a value between zero and one.
|
|
975
|
-
selected_times: Optional list containing a subset of
|
|
976
|
-
|
|
980
|
+
selected_times: Optional list containing a subset of time dimensions to
|
|
981
|
+
include. The x-axis corresponds to spend within these time periods. The
|
|
982
|
+
y-axis corresponds to the incremental outcome generated within these
|
|
983
|
+
time periods under the counterfactual where media units in each geo and
|
|
984
|
+
time period are multiplied by the corresponding multiplier (including
|
|
985
|
+
time periods prior to to `selected_times`). By default, all time periods
|
|
986
|
+
are included.
|
|
977
987
|
by_reach: For the channel w/ reach and frequency, return the response
|
|
978
988
|
curves by reach given fixed frequency if true; return the response
|
|
979
989
|
curves by frequency given fixed reach if false.
|
meridian/constants.py
CHANGED
meridian/data/__init__.py
CHANGED
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
"""Data handling API for Meridian."""
|
|
16
16
|
|
|
17
17
|
from meridian.data import arg_builder
|
|
18
|
+
from meridian.data import data_frame_input_data_builder
|
|
18
19
|
from meridian.data import input_data
|
|
20
|
+
from meridian.data import input_data_builder
|
|
19
21
|
from meridian.data import load
|
|
22
|
+
from meridian.data import nd_array_input_data_builder
|
|
20
23
|
from meridian.data import time_coordinates
|