google-meridian 1.1.3__py3-none-any.whl → 1.1.5__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-meridian
3
- Version: 1.1.3
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.3},
400
+ version = {1.1.5},
401
401
  year = {2025},
402
402
  }
403
403
  ```
@@ -1,11 +1,11 @@
1
- google_meridian-1.1.3.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
1
+ google_meridian-1.1.5.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
2
2
  meridian/__init__.py,sha256=XROKwHNVQvEa371QCXAHik5wN_YKObOdJQX9bJ2c4M4,832
3
- meridian/constants.py,sha256=VAVHyGfm9FyDd0dWomfqK5XYDUt9qJx7SAM4rzDh3RQ,17195
4
- meridian/version.py,sha256=CUTXDDaOfXFTukX_ywPK6Q3PiK9hMyJbmJRBeb5ez7c,644
3
+ meridian/constants.py,sha256=YE5h3qKH8e2lI3d9vWxc5TsSHUm5bHcz1Lq-2LurJnw,17204
4
+ meridian/version.py,sha256=PWF9Cv3V4ldFs0FOhaSlKgPdbBYCdiiaNLeQLulJrpE,644
5
5
  meridian/analysis/__init__.py,sha256=nGBYz7k9FVdadO_WVGMKJcfq7Yy_TuuP8zgee4i9pSA,836
6
- meridian/analysis/analyzer.py,sha256=FY_SvnkmEqqCIS37UXB3bvaQi-U3BwLcSWhH1puTzdQ,206003
6
+ meridian/analysis/analyzer.py,sha256=L7XyCTd4e_Bqfi8a0bW1WaXjH2ZvSVTPs0VP12a209c,206559
7
7
  meridian/analysis/formatter.py,sha256=ENIdR1CRiaVqIGEXx1HcnsA4ewgDD_nhsYCweJAThaw,7270
8
- meridian/analysis/optimizer.py,sha256=P4uMcV9ByqMapqa1TEqcnu-3NyTH9fR8QLszdKxRAFc,107801
8
+ meridian/analysis/optimizer.py,sha256=6L_FniWmqVgJlsFX-t3i8z_AO6mMftirtg_ZoOCaw8Y,118291
9
9
  meridian/analysis/summarizer.py,sha256=IthOUTMufGvAvbxiDhaKwe7uYCyiTyiQ8vgdmUtdevs,18855
10
10
  meridian/analysis/summary_text.py,sha256=I_smDkZJYp2j77ea-9AIbgeraDa7-qUYyb-IthP2qO4,12438
11
11
  meridian/analysis/test_utils.py,sha256=ES1r1akhRjD4pf2oTaGqzDfGNu9weAcLv6UZRuIkfEc,77699
@@ -21,10 +21,10 @@ meridian/analysis/templates/summary.html.jinja,sha256=LuENVDHYIpNo4pzloYaCR2K9XN
21
21
  meridian/analysis/templates/table.html.jinja,sha256=mvLMZx92RcD2JAS2w2eZtfYG-6WdfwYVo7pM8TbHp4g,1176
22
22
  meridian/data/__init__.py,sha256=StIe-wfYnnbfUbKtZHwnAQcRQUS8XCZk_PCaEzw90Ww,929
23
23
  meridian/data/arg_builder.py,sha256=Kqlt88bOqFj6D3xNwvWo4MBwNwcDFHzd-wMfEOmLoPU,3741
24
- meridian/data/data_frame_input_data_builder.py,sha256=3m6wrcC0psmD2ijsXk3R4uByA0Tu2gJxZBGaTS6Z7Io,22040
24
+ meridian/data/data_frame_input_data_builder.py,sha256=_hexZMFAuAowgo6FaOGElHSFHqhGnHQwEEBcwnT3zUE,27295
25
25
  meridian/data/input_data.py,sha256=teJPKTBfW-AzBWgf_fEO_S_Z1J_veqQkCvctINaid6I,39749
26
- meridian/data/input_data_builder.py,sha256=08E_MZLrCzwfjvjPWFVs7o_094vVJ5o6VmbTfrg4NUM,25602
27
- meridian/data/load.py,sha256=B-12fBhsghN7wj0A9IWyT7BVogIXjuUDDvR34JJFwPM,45157
26
+ meridian/data/input_data_builder.py,sha256=tbZjVXPDfmtndVyJA0fmzGzZwZb0RCEjXOTXb-ga8Nc,25648
27
+ meridian/data/load.py,sha256=X2nmYCC-7A0RUgmdolTqCt0TD3NEZabQ5oGv-TugE00,40129
28
28
  meridian/data/nd_array_input_data_builder.py,sha256=lfpmnENGuSGKyUd7bDGAwoLqHqteOKmHdKl0VI2wCQA,16341
29
29
  meridian/data/test_utils.py,sha256=6GJrPmeaF4uzMxxRgzERGv4g1XMUHwI0s7qDVMZUjuI,55565
30
30
  meridian/data/time_coordinates.py,sha256=C5A5fscSLjPH6G9YT8OspgIlCrkMY7y8dMFEt3tNSnE,9874
@@ -34,14 +34,14 @@ meridian/model/__init__.py,sha256=9NFfqUE5WgFc-9lQMkbfkwwV-bQIz0tsQ_3Jyq0A4SU,98
34
34
  meridian/model/adstock_hill.py,sha256=20A_6rbDUAADEkkHspB7JpCm5tYfYS1FQ6hJMLu21Pk,9283
35
35
  meridian/model/knots.py,sha256=KPEgnb-UdQQ4QBugOYEke-zBgEghgTmeCMoeiJ30meY,8054
36
36
  meridian/model/media.py,sha256=3BaPX8xYAFMEvf0mz3mBSCIDWViIs7M218nrCklc6Fk,14099
37
- meridian/model/model.py,sha256=BlLPyskHrEx5D71mUZFbNxS2VjkQgaiaE6hLKvQ5D3A,61489
37
+ meridian/model/model.py,sha256=XxVJaJtfUnCWI6gM7hWC6yC64yXECi91r1LHP2B23SQ,61216
38
38
  meridian/model/model_test_data.py,sha256=hDDTEzm72LknW9c5E_dNsy4Mm4Tfs6AirhGf_QxykFs,15552
39
39
  meridian/model/posterior_sampler.py,sha256=K49zWTTelME2rL1JLeFAdMPzL0OwrBvyAXA3oR-kgSI,27801
40
- meridian/model/prior_distribution.py,sha256=IEDU1rabcmKNY8lxwbbO4OUAlMHPIMa7flM_zsu3DLM,42417
41
- meridian/model/prior_sampler.py,sha256=cmu6jG-bSEkYDkjVUxl3iSxrL7r-LN7a77cb2Vc0LoA,23218
40
+ meridian/model/prior_distribution.py,sha256=1Qh7jQ2py7tdhLPDyeQzZ0doU6NhQRVaA0lGZNnOVZA,42554
41
+ meridian/model/prior_sampler.py,sha256=by41y2g56jEeJ1cxJi_s45uaUBySgf7wtL5u7-GpVE8,23325
42
42
  meridian/model/spec.py,sha256=0HNiMQUWQpYvWYOZr1_fj2ah8tH-bEyfEjoqgBZ9Lc0,18049
43
43
  meridian/model/transformers.py,sha256=nRjzq1fQG0ypldxboM7Gqok6WSAXAS1witRXoAzeH9Q,7763
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,,
44
+ google_meridian-1.1.5.dist-info/METADATA,sha256=FfS9XdL_j8tmV1xBvGavFjNJTdivXFGnQy23JLuTcuY,22201
45
+ google_meridian-1.1.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
46
+ google_meridian-1.1.5.dist-info/top_level.txt,sha256=nwaCebZvvU34EopTKZsjK0OMTFjVnkf4FfnBN_TAc0g,9
47
+ google_meridian-1.1.5.dist-info/RECORD,,
@@ -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 `(n_geos, T, n_media_channels)`
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, n_rf_channels)` for
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 rf_media(impressions), and then calculating
2003
- new_rf_media given the same formula for new_media. new_frequency is
2004
- optimal_frequency if optimal_frequency is not none, and
2005
- self._meridian.rf_tensors.frequency otherwise. new_reach is calculated using
2006
- (new_rf_media / new_frequency). new_spend and new_rf_spend are taken from
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, new_media_spend, new_reach, new_frequency,
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
- rf_media = filled_data.reach * filled_data.frequency
2048
- new_rf_media = (
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
- * rf_media
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(new_rf_media, frequency)
2061
- new_frequency = tf.math.divide_no_nan(new_rf_media, new_reach)
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, new_media_spend, new_reach, new_frequency, new_rf_spend)
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, new_media_spend, new_reach, new_frequency, new_rf_spend) = (
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=analyzer.DataTensors(
2109
- media=new_media,
2110
- reach=new_reach,
2111
- frequency=new_frequency,
2112
- revenue_per_kpi=filled_data.revenue_per_kpi,
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=self._analyzer.marginal_roi(
2162
- use_posterior=use_posterior,
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
+ )
meridian/constants.py CHANGED
@@ -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
- {MEDIA_EFFECTS_DIST: MEDIA_EFFECTS_NORMAL, UNIQUE_SIGMA_FOR_EACH_GEO: False}
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.