google-meridian 1.1.3__tar.gz → 1.1.5__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.3/google_meridian.egg-info → google_meridian-1.1.5}/PKG-INFO +2 -2
- {google_meridian-1.1.3 → google_meridian-1.1.5}/README.md +1 -1
- {google_meridian-1.1.3 → google_meridian-1.1.5/google_meridian.egg-info}/PKG-INFO +2 -2
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/analysis/analyzer.py +18 -11
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/analysis/optimizer.py +292 -47
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/constants.py +6 -4
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/data/data_frame_input_data_builder.py +222 -61
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/data/input_data_builder.py +3 -1
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/data/load.py +210 -350
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/model/model.py +3 -10
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/model/prior_distribution.py +7 -4
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/model/prior_sampler.py +2 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/version.py +1 -1
- {google_meridian-1.1.3 → google_meridian-1.1.5}/LICENSE +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/MANIFEST.in +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/google_meridian.egg-info/SOURCES.txt +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/google_meridian.egg-info/dependency_links.txt +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/google_meridian.egg-info/requires.txt +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/google_meridian.egg-info/top_level.txt +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/__init__.py +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/analysis/__init__.py +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/analysis/formatter.py +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/analysis/summarizer.py +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/analysis/summary_text.py +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/analysis/templates/card.html.jinja +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/analysis/templates/chart.html.jinja +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/analysis/templates/chips.html.jinja +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/analysis/templates/insights.html.jinja +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/analysis/templates/stats.html.jinja +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/analysis/templates/style.scss +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/analysis/templates/summary.html.jinja +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/analysis/templates/table.html.jinja +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/analysis/test_utils.py +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/analysis/visualizer.py +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/data/__init__.py +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/data/arg_builder.py +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/data/input_data.py +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/data/nd_array_input_data_builder.py +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/data/test_utils.py +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/data/time_coordinates.py +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/mlflow/__init__.py +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/mlflow/autolog.py +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/model/__init__.py +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/model/adstock_hill.py +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/model/knots.py +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/model/media.py +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/model/model_test_data.py +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/model/posterior_sampler.py +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/model/spec.py +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/meridian/model/transformers.py +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/pyproject.toml +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/setup.cfg +0 -0
- {google_meridian-1.1.3 → google_meridian-1.1.5}/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.5
|
|
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.5},
|
|
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.5},
|
|
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.5
|
|
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.5},
|
|
401
401
|
year = {2025},
|
|
402
402
|
}
|
|
403
403
|
```
|
|
@@ -59,16 +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
70
|
rf_impressions: Optional tensor with dimensions `(n_geos, T, n_rf_channels)`
|
|
69
71
|
for any time dimension `T`.
|
|
70
|
-
rf_spend: Optional tensor with dimensions `(n_geos, T,
|
|
71
|
-
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.
|
|
72
76
|
organic_media: Optional tensor with dimensions `(n_geos, T,
|
|
73
77
|
n_organic_media_channels)` for any time dimension `T`.
|
|
74
78
|
organic_reach: Optional tensor with dimensions `(n_geos, T,
|
|
@@ -406,13 +410,6 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
406
410
|
|
|
407
411
|
if old_tensor is None:
|
|
408
412
|
continue
|
|
409
|
-
# Skip spend data with only 1 dimension of (n_channels).
|
|
410
|
-
if (
|
|
411
|
-
var_name in [constants.MEDIA_SPEND, constants.RF_SPEND]
|
|
412
|
-
and new_tensor is not None
|
|
413
|
-
and new_tensor.ndim == 1
|
|
414
|
-
):
|
|
415
|
-
continue
|
|
416
413
|
|
|
417
414
|
if new_tensor is None:
|
|
418
415
|
missing_params.append(var_name)
|
|
@@ -424,6 +421,16 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
424
421
|
"time periods, which does not match the modified number of time "
|
|
425
422
|
f"periods, {new_n_times}.",
|
|
426
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
|
+
)
|
|
427
434
|
elif new_tensor.ndim > 1 and new_tensor.shape[1] != new_n_times:
|
|
428
435
|
raise ValueError(
|
|
429
436
|
"If the time dimension of any variable in `new_data` is "
|
|
@@ -1368,7 +1368,10 @@ class BudgetOptimizer:
|
|
|
1368
1368
|
versions of all the remaining tensors. If any of the tensors in
|
|
1369
1369
|
`new_data` is provided with a different number of time periods than in
|
|
1370
1370
|
`InputData`, then all tensors must be provided with the same number of
|
|
1371
|
-
time periods and the `time` tensor must be provided.
|
|
1371
|
+
time periods and the `time` tensor must be provided. In this case, spend
|
|
1372
|
+
tensors must be provided with `geo` and `time` granularity. If
|
|
1373
|
+
`use_optimal_frequency` is `True`, `new_data.frequency` does not need to
|
|
1374
|
+
be provided and is ignored. The optimal frequency is used instead.
|
|
1372
1375
|
use_posterior: Boolean. If `True`, then the budget is optimized based on
|
|
1373
1376
|
the posterior distribution of the model. Otherwise, the prior
|
|
1374
1377
|
distribution is used.
|
|
@@ -1427,7 +1430,7 @@ class BudgetOptimizer:
|
|
|
1427
1430
|
or equal to `(budget * gtol)`. `gtol` must be less than 1.
|
|
1428
1431
|
use_optimal_frequency: If `True`, uses `optimal_frequency` calculated by
|
|
1429
1432
|
trained Meridian model for optimization. If `False`, uses historical
|
|
1430
|
-
frequency.
|
|
1433
|
+
frequency or `new_data.frequency` if provided.
|
|
1431
1434
|
use_kpi: If `True`, runs the optimization on KPI. Defaults to revenue.
|
|
1432
1435
|
confidence_level: The threshold for computing the confidence intervals.
|
|
1433
1436
|
batch_size: Maximum draws per chain in each batch. The calculation is run
|
|
@@ -1596,6 +1599,142 @@ class BudgetOptimizer:
|
|
|
1596
1599
|
_optimization_grid=optimization_grid,
|
|
1597
1600
|
)
|
|
1598
1601
|
|
|
1602
|
+
def create_optimization_tensors(
|
|
1603
|
+
self,
|
|
1604
|
+
time: Sequence[str] | tf.Tensor,
|
|
1605
|
+
cpmu: tf.Tensor | None = None,
|
|
1606
|
+
media: tf.Tensor | None = None,
|
|
1607
|
+
media_spend: tf.Tensor | None = None,
|
|
1608
|
+
cprf: tf.Tensor | None = None,
|
|
1609
|
+
rf_impressions: tf.Tensor | None = None,
|
|
1610
|
+
frequency: tf.Tensor | None = None,
|
|
1611
|
+
rf_spend: tf.Tensor | None = None,
|
|
1612
|
+
revenue_per_kpi: tf.Tensor | None = None,
|
|
1613
|
+
use_optimal_frequency: bool = True,
|
|
1614
|
+
) -> analyzer.DataTensors:
|
|
1615
|
+
"""Creates a `DataTensors` for optimizations from CPM and flighting data.
|
|
1616
|
+
|
|
1617
|
+
CPM is broken down into cost per media unit, `cpmu`, for the media channels
|
|
1618
|
+
and cost per impression (reach * frequency), `cprf`, for the reach and
|
|
1619
|
+
frequency channels.
|
|
1620
|
+
|
|
1621
|
+
The flighting pattern can be specified as the spend flighting or the media
|
|
1622
|
+
units flighting pattern at the time or geo and time granularity. If data is
|
|
1623
|
+
passed without a geo dimension, then the values are interpreted as
|
|
1624
|
+
national-level totals. If the model is a geo-level model, then the values
|
|
1625
|
+
are allocated across geos based on the population used in the model.
|
|
1626
|
+
|
|
1627
|
+
Below are the different combinations of tensors that can be provided:
|
|
1628
|
+
For media:
|
|
1629
|
+
1) `media`, `cpmu` (media units flighting pattern)
|
|
1630
|
+
2) `media_spend`, `cpmu` (spend flighting pattern)
|
|
1631
|
+
|
|
1632
|
+
For R&F:
|
|
1633
|
+
If `use_optimal_frequency=True`, `frequency` should not be provided.
|
|
1634
|
+
Frequency input is not required for the optimization, so the new
|
|
1635
|
+
`DataTensors` object will be created with `frequuency` arbitrarily set to
|
|
1636
|
+
1 and `reach=rf_impressions`.
|
|
1637
|
+
1) `rf_impressions`, `cprf` (impressions flighting pattern)
|
|
1638
|
+
2) `rf_spend`, `cprf` (spend flighting pattern)
|
|
1639
|
+
|
|
1640
|
+
If `use_optimal_frequency=False`:
|
|
1641
|
+
1) `rf_impressions`, `frequency`, `cprf` (impressions flighting pattern)
|
|
1642
|
+
2) `rf_spend`, `frequency`, `cprf` (spend flighting pattern)
|
|
1643
|
+
|
|
1644
|
+
|
|
1645
|
+
Args:
|
|
1646
|
+
time: A sequence or tensor of time coordinates in the "YYYY-mm-dd" string
|
|
1647
|
+
format.
|
|
1648
|
+
cpmu: A tensor of cost per media unit with dimensions `(n_media_channels),
|
|
1649
|
+
`(T, n_media_channels)` or `(n_geos, T, n_media_channels)` for any time
|
|
1650
|
+
dimension `T`.
|
|
1651
|
+
media: An optional tensor of media unit values with dimensions `(T,
|
|
1652
|
+
n_media_channels)` or `(n_geos, T, n_media_channels)` for any time
|
|
1653
|
+
dimension `T`.
|
|
1654
|
+
media_spend: A tensor of media spend values with dimensions `(T,
|
|
1655
|
+
n_media_channels)` or `(n_geos, T, n_media_channels)` for any time
|
|
1656
|
+
dimension `T`.
|
|
1657
|
+
cprf: A tensor of cost per impression (reach * frequency) with dimensions
|
|
1658
|
+
`(n_rf_channels), `(T, n_rf_channels)` or `(n_geos, T, n_rf_channels)`
|
|
1659
|
+
for any time dimension `T`.
|
|
1660
|
+
rf_impressions: A tensor of impressions (reach * frequency) values with
|
|
1661
|
+
dimensions `(T, n_rf_channels)` or `(n_geos, T, n_rf_channels)` for any
|
|
1662
|
+
time dimension `T`.
|
|
1663
|
+
frequency: A tensor of frequency values with dimensions `(n_rf_channels)`,
|
|
1664
|
+
`(T, n_rf_channels)` or `(n_geos, T, n_rf_channels)` for any time
|
|
1665
|
+
dimension `T`. If `use_optimal_frequency=True`, then this tensor should
|
|
1666
|
+
not be provided and the optimal frequency will be calculated and used.
|
|
1667
|
+
rf_spend: A tensor of rf spend values with dimensions `(T, n_rf_channels)`
|
|
1668
|
+
or `(n_geos, T, n_rf_channels)` for any time dimension `T`.
|
|
1669
|
+
revenue_per_kpi: A tensor of revenue per KPI values with dimensions `()`,
|
|
1670
|
+
`(T)`, or `(n_geos, T)` for any time dimension `T`.
|
|
1671
|
+
use_optimal_frequency: Boolean. If `True`, the optiaml frequency will be
|
|
1672
|
+
used in the optimization and a frequency value should not be provided.
|
|
1673
|
+
In this case, `reach=rf_impressions` and `frequency=1` (by arbitrary
|
|
1674
|
+
convention) in the new data. If `False`, the frequency value must be
|
|
1675
|
+
provided.
|
|
1676
|
+
|
|
1677
|
+
Returns:
|
|
1678
|
+
A `DataTensors` object with optional tensors `media`, `reach`,
|
|
1679
|
+
`frequency`, `media_spend`, `rf_spend`, `revenue_per_kpi`, and `time`.
|
|
1680
|
+
"""
|
|
1681
|
+
self._validate_optimization_tensors(
|
|
1682
|
+
cpmu=cpmu,
|
|
1683
|
+
cprf=cprf,
|
|
1684
|
+
media=media,
|
|
1685
|
+
rf_impressions=rf_impressions,
|
|
1686
|
+
frequency=frequency,
|
|
1687
|
+
media_spend=media_spend,
|
|
1688
|
+
rf_spend=rf_spend,
|
|
1689
|
+
revenue_per_kpi=revenue_per_kpi,
|
|
1690
|
+
use_optimal_frequency=use_optimal_frequency,
|
|
1691
|
+
)
|
|
1692
|
+
n_times = time.shape[0] if isinstance(time, tf.Tensor) else len(time)
|
|
1693
|
+
n_geos = self._meridian.n_geos
|
|
1694
|
+
revenue_per_kpi = (
|
|
1695
|
+
_expand_tensor(revenue_per_kpi, (n_geos, n_times))
|
|
1696
|
+
if revenue_per_kpi is not None
|
|
1697
|
+
else None
|
|
1698
|
+
)
|
|
1699
|
+
|
|
1700
|
+
tensors = {}
|
|
1701
|
+
if media is not None:
|
|
1702
|
+
cpmu = _expand_tensor(cpmu, (n_geos, n_times, media.shape[-1]))
|
|
1703
|
+
tensors[c.MEDIA] = self._allocate_tensor_by_population(media)
|
|
1704
|
+
tensors[c.MEDIA_SPEND] = tensors[c.MEDIA] * cpmu
|
|
1705
|
+
if media_spend is not None:
|
|
1706
|
+
cpmu = _expand_tensor(cpmu, (n_geos, n_times, media_spend.shape[-1]))
|
|
1707
|
+
tensors[c.MEDIA_SPEND] = self._allocate_tensor_by_population(media_spend)
|
|
1708
|
+
tensors[c.MEDIA] = tensors[c.MEDIA_SPEND] / cpmu
|
|
1709
|
+
if rf_impressions is not None:
|
|
1710
|
+
shape = (n_geos, n_times, rf_impressions.shape[-1])
|
|
1711
|
+
cprf = _expand_tensor(cprf, shape)
|
|
1712
|
+
allocated_impressions = self._allocate_tensor_by_population(
|
|
1713
|
+
rf_impressions
|
|
1714
|
+
)
|
|
1715
|
+
tensors[c.RF_SPEND] = allocated_impressions * cprf
|
|
1716
|
+
if use_optimal_frequency:
|
|
1717
|
+
frequency = tf.ones_like(allocated_impressions)
|
|
1718
|
+
tensors[c.FREQUENCY] = _expand_tensor(frequency, shape)
|
|
1719
|
+
tensors[c.REACH] = tf.math.divide_no_nan(
|
|
1720
|
+
allocated_impressions, tensors[c.FREQUENCY]
|
|
1721
|
+
)
|
|
1722
|
+
if rf_spend is not None:
|
|
1723
|
+
shape = (n_geos, n_times, rf_spend.shape[-1])
|
|
1724
|
+
cprf = _expand_tensor(cprf, shape)
|
|
1725
|
+
tensors[c.RF_SPEND] = self._allocate_tensor_by_population(rf_spend)
|
|
1726
|
+
impressions = tf.math.divide_no_nan(tensors[c.RF_SPEND], cprf)
|
|
1727
|
+
if use_optimal_frequency:
|
|
1728
|
+
frequency = tf.ones_like(impressions)
|
|
1729
|
+
tensors[c.FREQUENCY] = _expand_tensor(frequency, shape)
|
|
1730
|
+
tensors[c.REACH] = tf.math.divide_no_nan(
|
|
1731
|
+
impressions, tensors[c.FREQUENCY]
|
|
1732
|
+
)
|
|
1733
|
+
if revenue_per_kpi is not None:
|
|
1734
|
+
tensors[c.REVENUE_PER_KPI] = revenue_per_kpi
|
|
1735
|
+
tensors[c.TIME] = tf.convert_to_tensor(time)
|
|
1736
|
+
return analyzer.DataTensors(**tensors)
|
|
1737
|
+
|
|
1599
1738
|
def _validate_grid(
|
|
1600
1739
|
self,
|
|
1601
1740
|
new_data: analyzer.DataTensors | None,
|
|
@@ -1990,8 +2129,6 @@ class BudgetOptimizer:
|
|
|
1990
2129
|
tf.Tensor | None,
|
|
1991
2130
|
tf.Tensor | None,
|
|
1992
2131
|
tf.Tensor | None,
|
|
1993
|
-
tf.Tensor | None,
|
|
1994
|
-
tf.Tensor | None,
|
|
1995
2132
|
]:
|
|
1996
2133
|
"""Gets the tensors for incremental outcome, based on spend data.
|
|
1997
2134
|
|
|
@@ -1999,12 +2136,11 @@ class BudgetOptimizer:
|
|
|
1999
2136
|
incremental_outcome() for creating budget data. new_media is calculated
|
|
2000
2137
|
assuming a constant cpm between historical spend and optimization spend.
|
|
2001
2138
|
new_reach and new_frequency are calculated by first multiplying them
|
|
2002
|
-
together and getting
|
|
2003
|
-
|
|
2004
|
-
optimal_frequency if optimal_frequency is not
|
|
2005
|
-
self._meridian.rf_tensors.frequency otherwise. new_reach is calculated
|
|
2006
|
-
|
|
2007
|
-
their respective indexes in spend.
|
|
2139
|
+
together and getting `rf_impressions`, and then calculating
|
|
2140
|
+
`new_rf_impressions` given the same formula for `new_media`. `new_frequency`
|
|
2141
|
+
is `optimal_frequency` if `optimal_frequency` is not None, and
|
|
2142
|
+
`self._meridian.rf_tensors.frequency` otherwise. `new_reach` is calculated
|
|
2143
|
+
using `new_rf_impressions / new_frequency`.
|
|
2008
2144
|
|
|
2009
2145
|
Args:
|
|
2010
2146
|
hist_spend: historical spend data.
|
|
@@ -2021,8 +2157,7 @@ class BudgetOptimizer:
|
|
|
2021
2157
|
frequency is used for the optimization scenario.
|
|
2022
2158
|
|
|
2023
2159
|
Returns:
|
|
2024
|
-
Tuple of tf.tensors (new_media,
|
|
2025
|
-
new_rf_spend).
|
|
2160
|
+
Tuple of tf.tensors (new_media, new_reach, new_frequency).
|
|
2026
2161
|
"""
|
|
2027
2162
|
new_data = new_data or analyzer.DataTensors()
|
|
2028
2163
|
filled_data = new_data.validate_and_fill_missing_data(
|
|
@@ -2037,37 +2172,29 @@ class BudgetOptimizer:
|
|
|
2037
2172
|
)
|
|
2038
2173
|
* filled_data.media
|
|
2039
2174
|
)
|
|
2040
|
-
new_media_spend = tf.convert_to_tensor(
|
|
2041
|
-
spend[: self._meridian.n_media_channels]
|
|
2042
|
-
)
|
|
2043
2175
|
else:
|
|
2044
2176
|
new_media = None
|
|
2045
|
-
new_media_spend = None
|
|
2046
2177
|
if self._meridian.n_rf_channels > 0:
|
|
2047
|
-
|
|
2048
|
-
|
|
2178
|
+
rf_impressions = filled_data.reach * filled_data.frequency
|
|
2179
|
+
new_rf_impressions = (
|
|
2049
2180
|
tf.math.divide_no_nan(
|
|
2050
2181
|
spend[-self._meridian.n_rf_channels :],
|
|
2051
2182
|
hist_spend[-self._meridian.n_rf_channels :],
|
|
2052
2183
|
)
|
|
2053
|
-
*
|
|
2184
|
+
* rf_impressions
|
|
2054
2185
|
)
|
|
2055
2186
|
frequency = (
|
|
2056
2187
|
filled_data.frequency
|
|
2057
2188
|
if optimal_frequency is None
|
|
2058
2189
|
else optimal_frequency
|
|
2059
2190
|
)
|
|
2060
|
-
new_reach = tf.math.divide_no_nan(
|
|
2061
|
-
new_frequency = tf.math.divide_no_nan(
|
|
2062
|
-
new_rf_spend = tf.convert_to_tensor(
|
|
2063
|
-
spend[-self._meridian.n_rf_channels :]
|
|
2064
|
-
)
|
|
2191
|
+
new_reach = tf.math.divide_no_nan(new_rf_impressions, frequency)
|
|
2192
|
+
new_frequency = tf.math.divide_no_nan(new_rf_impressions, new_reach)
|
|
2065
2193
|
else:
|
|
2066
2194
|
new_reach = None
|
|
2067
2195
|
new_frequency = None
|
|
2068
|
-
new_rf_spend = None
|
|
2069
2196
|
|
|
2070
|
-
return (new_media,
|
|
2197
|
+
return (new_media, new_reach, new_frequency)
|
|
2071
2198
|
|
|
2072
2199
|
def _create_budget_dataset(
|
|
2073
2200
|
self,
|
|
@@ -2091,7 +2218,7 @@ class BudgetOptimizer:
|
|
|
2091
2218
|
)
|
|
2092
2219
|
spend_tensor = tf.convert_to_tensor(spend, dtype=tf.float32)
|
|
2093
2220
|
hist_spend = tf.convert_to_tensor(hist_spend, dtype=tf.float32)
|
|
2094
|
-
(new_media,
|
|
2221
|
+
(new_media, new_reach, new_frequency) = (
|
|
2095
2222
|
self._get_incremental_outcome_tensors(
|
|
2096
2223
|
hist_spend,
|
|
2097
2224
|
spend_tensor,
|
|
@@ -2100,18 +2227,30 @@ class BudgetOptimizer:
|
|
|
2100
2227
|
)
|
|
2101
2228
|
)
|
|
2102
2229
|
budget = np.sum(spend_tensor)
|
|
2230
|
+
inc_outcome_data = analyzer.DataTensors(
|
|
2231
|
+
media=new_media,
|
|
2232
|
+
reach=new_reach,
|
|
2233
|
+
frequency=new_frequency,
|
|
2234
|
+
revenue_per_kpi=filled_data.revenue_per_kpi,
|
|
2235
|
+
)
|
|
2103
2236
|
|
|
2104
2237
|
# incremental_outcome here is a tensor with the shape
|
|
2105
2238
|
# (n_chains, n_draws, n_channels)
|
|
2106
2239
|
incremental_outcome = self._analyzer.incremental_outcome(
|
|
2107
2240
|
use_posterior=use_posterior,
|
|
2108
|
-
new_data=
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2241
|
+
new_data=inc_outcome_data,
|
|
2242
|
+
selected_times=selected_times,
|
|
2243
|
+
use_kpi=use_kpi,
|
|
2244
|
+
batch_size=batch_size,
|
|
2245
|
+
include_non_paid_channels=False,
|
|
2246
|
+
)
|
|
2247
|
+
incremental_increase = 0.01
|
|
2248
|
+
mroi_numerator = self._analyzer.incremental_outcome(
|
|
2249
|
+
new_data=inc_outcome_data,
|
|
2114
2250
|
selected_times=selected_times,
|
|
2251
|
+
scaling_factor0=1.0,
|
|
2252
|
+
scaling_factor1=1 + incremental_increase,
|
|
2253
|
+
use_posterior=use_posterior,
|
|
2115
2254
|
use_kpi=use_kpi,
|
|
2116
2255
|
batch_size=batch_size,
|
|
2117
2256
|
include_non_paid_channels=False,
|
|
@@ -2158,20 +2297,8 @@ class BudgetOptimizer:
|
|
|
2158
2297
|
include_median=True,
|
|
2159
2298
|
)
|
|
2160
2299
|
marginal_roi = analyzer.get_central_tendency_and_ci(
|
|
2161
|
-
data=
|
|
2162
|
-
|
|
2163
|
-
new_data=analyzer.DataTensors(
|
|
2164
|
-
media=new_media,
|
|
2165
|
-
reach=new_reach,
|
|
2166
|
-
frequency=new_frequency,
|
|
2167
|
-
media_spend=new_media_spend,
|
|
2168
|
-
rf_spend=new_rf_spend,
|
|
2169
|
-
revenue_per_kpi=filled_data.revenue_per_kpi,
|
|
2170
|
-
),
|
|
2171
|
-
selected_times=selected_times,
|
|
2172
|
-
batch_size=batch_size,
|
|
2173
|
-
by_reach=True,
|
|
2174
|
-
use_kpi=use_kpi,
|
|
2300
|
+
data=tf.math.divide_no_nan(
|
|
2301
|
+
mroi_numerator, spend_tensor * incremental_increase
|
|
2175
2302
|
),
|
|
2176
2303
|
confidence_level=confidence_level,
|
|
2177
2304
|
include_median=True,
|
|
@@ -2452,6 +2579,100 @@ class BudgetOptimizer:
|
|
|
2452
2579
|
)
|
|
2453
2580
|
return (spend_grid, incremental_outcome_grid)
|
|
2454
2581
|
|
|
2582
|
+
def _validate_optimization_tensors(
|
|
2583
|
+
self,
|
|
2584
|
+
cpmu: tf.Tensor | None = None,
|
|
2585
|
+
cprf: tf.Tensor | None = None,
|
|
2586
|
+
media: tf.Tensor | None = None,
|
|
2587
|
+
rf_impressions: tf.Tensor | None = None,
|
|
2588
|
+
frequency: tf.Tensor | None = None,
|
|
2589
|
+
media_spend: tf.Tensor | None = None,
|
|
2590
|
+
rf_spend: tf.Tensor | None = None,
|
|
2591
|
+
revenue_per_kpi: tf.Tensor | None = None,
|
|
2592
|
+
use_optimal_frequency: bool = True,
|
|
2593
|
+
):
|
|
2594
|
+
"""Validates the tensors needed for optimization."""
|
|
2595
|
+
if (media is not None or media_spend is not None) and cpmu is None:
|
|
2596
|
+
raise ValueError(
|
|
2597
|
+
'If `media` or `media_spend` is provided, then `cpmu` must also be'
|
|
2598
|
+
' provided.'
|
|
2599
|
+
)
|
|
2600
|
+
if (rf_impressions is not None or rf_spend is not None) and cprf is None:
|
|
2601
|
+
raise ValueError(
|
|
2602
|
+
'If `reach` and `frequency` or `rf_spend` is provided, then `cprf`'
|
|
2603
|
+
' must also be provided.'
|
|
2604
|
+
)
|
|
2605
|
+
if media is not None and media_spend is not None:
|
|
2606
|
+
raise ValueError('Only one of `media` or `media_spend` can be provided.')
|
|
2607
|
+
if rf_impressions is not None and rf_spend is not None:
|
|
2608
|
+
raise ValueError(
|
|
2609
|
+
'Only one of `rf_impressions` or `rf_spend` can be provided.'
|
|
2610
|
+
)
|
|
2611
|
+
if use_optimal_frequency and frequency is not None:
|
|
2612
|
+
raise ValueError(
|
|
2613
|
+
'If `use_optimal_frequency` is `True`, then `frequency` must not be'
|
|
2614
|
+
' provided.'
|
|
2615
|
+
)
|
|
2616
|
+
if not use_optimal_frequency and frequency is None:
|
|
2617
|
+
if rf_impressions is not None or rf_spend is not None:
|
|
2618
|
+
raise ValueError(
|
|
2619
|
+
'If `use_optimal_frequency` is `False`, then `frequency` must be'
|
|
2620
|
+
' provided.'
|
|
2621
|
+
)
|
|
2622
|
+
|
|
2623
|
+
n_geos = [
|
|
2624
|
+
t.shape[0]
|
|
2625
|
+
for t in [
|
|
2626
|
+
cpmu,
|
|
2627
|
+
cprf,
|
|
2628
|
+
media,
|
|
2629
|
+
rf_impressions,
|
|
2630
|
+
frequency,
|
|
2631
|
+
media_spend,
|
|
2632
|
+
rf_spend,
|
|
2633
|
+
]
|
|
2634
|
+
if t is not None and t.ndim == 3
|
|
2635
|
+
]
|
|
2636
|
+
if revenue_per_kpi is not None and revenue_per_kpi.ndim == 2:
|
|
2637
|
+
n_geos.append(revenue_per_kpi.shape[0])
|
|
2638
|
+
if any(n_geo != self._meridian.n_geos for n_geo in n_geos):
|
|
2639
|
+
raise ValueError(
|
|
2640
|
+
'All tensors with a geo dimension must have the same number of geos'
|
|
2641
|
+
' as in `meridian.InputData`.'
|
|
2642
|
+
)
|
|
2643
|
+
|
|
2644
|
+
def _allocate_tensor_by_population(
|
|
2645
|
+
self, tensor: tf.Tensor, required_ndim: int = 3
|
|
2646
|
+
):
|
|
2647
|
+
"""Allocates a tensor of shape (time,) or (time, channel) by the population.
|
|
2648
|
+
|
|
2649
|
+
Args:
|
|
2650
|
+
tensor: A tensor of shape (time,) or (time, channel).
|
|
2651
|
+
required_ndim: The required number of dimensions for the tensor.
|
|
2652
|
+
|
|
2653
|
+
Returns:
|
|
2654
|
+
The scaled tensor of shape (geo, time) or (geo, time, channel).
|
|
2655
|
+
"""
|
|
2656
|
+
if tensor.ndim == required_ndim:
|
|
2657
|
+
return tensor
|
|
2658
|
+
|
|
2659
|
+
if tensor.ndim != required_ndim - 1:
|
|
2660
|
+
raise ValueError(
|
|
2661
|
+
'Tensor must have 1 less than the required number of dimensions, '
|
|
2662
|
+
f'{required_ndim}, in order to be allocated by population. Found '
|
|
2663
|
+
f'{tensor.ndim} dimensions.'
|
|
2664
|
+
)
|
|
2665
|
+
|
|
2666
|
+
population = self._meridian.population
|
|
2667
|
+
normalized_population = population / tf.reduce_sum(population)
|
|
2668
|
+
if tensor.ndim == 1:
|
|
2669
|
+
reshaped_population = normalized_population[:, tf.newaxis]
|
|
2670
|
+
reshaped_tensor = tensor[tf.newaxis, :]
|
|
2671
|
+
else:
|
|
2672
|
+
reshaped_population = normalized_population[:, tf.newaxis, tf.newaxis]
|
|
2673
|
+
reshaped_tensor = tensor[tf.newaxis, :, :]
|
|
2674
|
+
return reshaped_tensor * reshaped_population
|
|
2675
|
+
|
|
2455
2676
|
|
|
2456
2677
|
def _validate_pct_of_spend(
|
|
2457
2678
|
n_channels: int,
|
|
@@ -2705,3 +2926,27 @@ def _raise_warning_if_target_constraints_not_met(
|
|
|
2705
2926
|
f' ROI is {target_mroi}, but the actual channel marginal ROIs are'
|
|
2706
2927
|
f' {optimized_mroi}.'
|
|
2707
2928
|
)
|
|
2929
|
+
|
|
2930
|
+
|
|
2931
|
+
def _expand_tensor(tensor: tf.Tensor, required_shape: tuple[int, ...]):
|
|
2932
|
+
"""Expands a tensor to the required number of dimensions."""
|
|
2933
|
+
if tensor.shape == required_shape:
|
|
2934
|
+
return tensor
|
|
2935
|
+
if tensor.ndim == 0:
|
|
2936
|
+
return tf.fill(required_shape, tensor)
|
|
2937
|
+
|
|
2938
|
+
# Tensor must be less than or equal to the required number of dimensions and
|
|
2939
|
+
# the shape must match the required shape excluding the difference in number
|
|
2940
|
+
# of dims.
|
|
2941
|
+
if tensor.ndim <= len(required_shape) and list(tensor.shape) == list(
|
|
2942
|
+
required_shape[-tensor.ndim :]
|
|
2943
|
+
):
|
|
2944
|
+
n_tile_dims = len(required_shape) - tensor.ndim
|
|
2945
|
+
repeats = list(required_shape[:n_tile_dims]) + [1] * tensor.ndim
|
|
2946
|
+
reshaped_tensor = tf.reshape(tensor, [1] * n_tile_dims + list(tensor.shape))
|
|
2947
|
+
return tf.tile(reshaped_tensor, repeats)
|
|
2948
|
+
|
|
2949
|
+
raise ValueError(
|
|
2950
|
+
f'Cannot expand tensor with shape {tensor.shape} to target'
|
|
2951
|
+
f' {required_shape}.'
|
|
2952
|
+
)
|
|
@@ -71,6 +71,8 @@ ORGANIC_FREQUENCY = 'organic_frequency'
|
|
|
71
71
|
NON_MEDIA_TREATMENTS = 'non_media_treatments'
|
|
72
72
|
REVENUE = 'revenue'
|
|
73
73
|
NON_REVENUE = 'non_revenue'
|
|
74
|
+
CPMU = 'cpmu'
|
|
75
|
+
CPRF = 'cprf'
|
|
74
76
|
REQUIRED_INPUT_DATA_ARRAY_NAMES = (
|
|
75
77
|
KPI,
|
|
76
78
|
POPULATION,
|
|
@@ -216,9 +218,10 @@ PAID_MEDIA_ROI_PRIOR_TYPES = frozenset(
|
|
|
216
218
|
# Represents a 1% increase in spend.
|
|
217
219
|
MROI_FACTOR = 1.01
|
|
218
220
|
|
|
219
|
-
NATIONAL_MODEL_SPEC_ARGS = immutabledict.immutabledict(
|
|
220
|
-
|
|
221
|
-
|
|
221
|
+
NATIONAL_MODEL_SPEC_ARGS = immutabledict.immutabledict({
|
|
222
|
+
MEDIA_EFFECTS_DIST: MEDIA_EFFECTS_NORMAL,
|
|
223
|
+
UNIQUE_SIGMA_FOR_EACH_GEO: False,
|
|
224
|
+
})
|
|
222
225
|
|
|
223
226
|
NATIONAL_ANALYZER_PARAMETERS_DEFAULTS = immutabledict.immutabledict(
|
|
224
227
|
{'aggregate_geos': True, 'geos_to_include': None}
|
|
@@ -229,7 +232,6 @@ NATIONAL_ANALYZER_PARAMETERS_DEFAULTS = immutabledict.immutabledict(
|
|
|
229
232
|
CHAIN = 'chain'
|
|
230
233
|
DRAW = 'draw'
|
|
231
234
|
KNOTS = 'knots'
|
|
232
|
-
SIGMA_DIM = 'sigma_dim'
|
|
233
235
|
|
|
234
236
|
|
|
235
237
|
# Model parameters.
|