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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-meridian
3
- Version: 1.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.1},
400
+ version = {1.1.3},
397
401
  year = {2025},
398
402
  }
399
403
  ```
@@ -1,14 +1,15 @@
1
- google_meridian-1.1.1.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
2
- meridian/__init__.py,sha256=v7cNJABthU3UGBjqzcBs5J7MInPPxRkCUZChVo2pw3M,714
3
- meridian/constants.py,sha256=AWhDEP9VcyQtPCbZhM6cPXHeWuz19wjaqB5lGz6qBsw,17161
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=VBEQYP28G23F2EXoEOqGrWJRmCr_ez-qWD3brQlqZI4,204098
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=ZmO05reNjlFOy8i3E8M9dDMYCIzNnQjLdH99zSorkqw,106122
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=VHgbvGnRmloawilU_I7FPsqZcAYpZq5ODl3cHy2eiDo,93728
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=4F6_dCnDOic08yMw6_nIDR03B9cF_4STDFb430XvZR4,774
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/load.py,sha256=iFdNq9J89qlmOIrvMER1ci8LzZD87gHl6NTW49h7ZFE,55260
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=R0LnMUNTuGzXD2lzNRRORA4-p21xpdhkVVsvFaWtEK0,13819
31
- meridian/model/model.py,sha256=JXHCcxpUDXqJQ9hI0YkY5PfGbpt8d3jAKR1TbCP08PI,61110
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=jjLqcYEAorVJ_2nmhpkVUjCGAyNUZYPTEXVTDHufbqA,27727
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=jSaxFmJzyN2OKqKyU059Ar4Yr565w4zlInPl4zxjGZk,23212
36
- meridian/model/spec.py,sha256=b6nYj39L-Yy5j2i2IHdZHY2trRvjEA-9i_c3b__63A8,17239
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.1.dist-info/METADATA,sha256=5yywzNt-Pe3h9GLYo-0MfmOku5tHg2J5XrcJtUTp3Gk,22055
39
- google_meridian-1.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
40
- google_meridian-1.1.1.dist-info/top_level.txt,sha256=nwaCebZvvU34EopTKZsjK0OMTFjVnkf4FfnBN_TAc0g,9
41
- google_meridian-1.1.1.dist-info/RECORD,,
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
@@ -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
- old_tensor = getattr(meridian.input_data, field.name)
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 getattr(meridian.input_data, field.name) is None:
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
- old_tensor = getattr(meridian.input_data, var_name)
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
- old_tensor = getattr(meridian.input_data, var_name)
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
- old_tensor = getattr(meridian.input_data, var_name)
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 `reach`, `frequency`,
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
- constants.RF_DATA,
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(np.array(filled_data.frequency))
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.frequency) * freq
3541
- new_reach = filled_data.frequency * filled_data.reach / new_frequency
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.frequency) * optimal_frequency,
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 at the national-level, assuming the
3965
- historical flighting pattern across geos and time periods for each media
3966
- channel. A list of multipliers is applied to each media channel's total
3967
- historical spend to obtain the `x-values` at which the channel's response
3968
- curve is calculated.
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
@@ -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
- _get_optimization_bounds(
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
- Optimization depends on the following:
1311
- 1. Flighting pattern (the relative allocation of a channels' media units
1312
- across geos and time periods, which is held fixed for each channel)
1313
- 2. Cost per media unit (This is assumed to be constant for each channel, and
1314
- can optionally vary by geo and/or time period)
1315
- 3. `pct_of_spend` (center of the spend box constraint for each channel)
1316
- 4. `budget` (total budget used for fixed budget scenarios)
1317
-
1318
- By default, these values are assigned based on the historical data. The
1319
- `pct_of_spend` and `budget` are optimization arguments that can be
1320
- overridden directly. Passing `new_data.media` (or `new_data.reach` or
1321
- `new_data.frequency`) will override both the flighting pattern and cost per
1322
- media unit. Passing `new_data.spend` (or `new_data.rf_spend) will only
1323
- override the cost per media unit.
1324
-
1325
- If `new_data` is passed with a different number of time periods than the
1326
- historical data, then all of the optimization parameters will be inferred
1327
- from it. Default values for `pct_of_spend` and `budget` (if
1328
- `fixed_budget=True`) will be inferred from the `new_data`, but can be
1329
- overridden using the `pct_of_spend` and `budget` arguments.
1330
-
1331
- If `start_date` or `end_date` is specified, then the default values are
1332
- inferred based on the subset of time periods specified. Both start and end
1333
- time selectors should align with the Meridian time dimension coordinates in
1334
- the underlying model if optimizing the original data. If `new_data` is
1335
- provided with a different number of time periods than in `InputData`, then
1336
- the start and end time coordinates must match the time dimensions in
1337
- `new_data.time`. By default, all times periods are used. Either start or
1338
- end time component can be `None` to represent the first or the last time
1339
- coordinate, respectively.
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 `None`, i.e. the first time period.
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 `None`, i.e. the last time period.
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
- _get_optimization_bounds(
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
- _get_optimization_bounds(
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=filled_data.filter_fields(c.RF_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
- spend = tf.convert_to_tensor(spend, dtype=tf.float32)
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
- spend,
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(spend)
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, spend),
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(spend, incremental_outcome),
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], spend / total_spend),
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 _get_optimization_bounds(
2544
+ def get_optimization_bounds(
2514
2545
  n_channels: int,
2515
2546
  spend: np.ndarray,
2516
2547
  round_factor: int,
@@ -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 of a subset of time dimensions to include.
880
- By default, all times are included. Times should match the time
881
- dimensions from `meridian.InputData`.
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 times to include. By
976
- default, all time periods are included.
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
@@ -63,6 +63,7 @@ CONTROLS = 'controls'
63
63
  POPULATION = 'population'
64
64
  REACH = 'reach'
65
65
  FREQUENCY = 'frequency'
66
+ RF_IMPRESSIONS = 'rf_impressions'
66
67
  RF_SPEND = 'rf_spend'
67
68
  ORGANIC_MEDIA = 'organic_media'
68
69
  ORGANIC_REACH = 'organic_reach'
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