google-meridian 1.0.8__tar.gz → 1.0.9__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.
Files changed (47) hide show
  1. {google_meridian-1.0.8/google_meridian.egg-info → google_meridian-1.0.9}/PKG-INFO +2 -2
  2. {google_meridian-1.0.8 → google_meridian-1.0.9}/README.md +1 -1
  3. {google_meridian-1.0.8 → google_meridian-1.0.9/google_meridian.egg-info}/PKG-INFO +2 -2
  4. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/__init__.py +1 -1
  5. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/analyzer.py +108 -18
  6. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/optimizer.py +196 -45
  7. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/summarizer.py +21 -3
  8. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/visualizer.py +69 -23
  9. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/constants.py +12 -11
  10. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/model/model.py +15 -0
  11. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/model/prior_distribution.py +22 -1
  12. {google_meridian-1.0.8 → google_meridian-1.0.9}/LICENSE +0 -0
  13. {google_meridian-1.0.8 → google_meridian-1.0.9}/MANIFEST.in +0 -0
  14. {google_meridian-1.0.8 → google_meridian-1.0.9}/google_meridian.egg-info/SOURCES.txt +0 -0
  15. {google_meridian-1.0.8 → google_meridian-1.0.9}/google_meridian.egg-info/dependency_links.txt +0 -0
  16. {google_meridian-1.0.8 → google_meridian-1.0.9}/google_meridian.egg-info/requires.txt +0 -0
  17. {google_meridian-1.0.8 → google_meridian-1.0.9}/google_meridian.egg-info/top_level.txt +0 -0
  18. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/__init__.py +0 -0
  19. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/formatter.py +0 -0
  20. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/summary_text.py +0 -0
  21. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/templates/card.html.jinja +0 -0
  22. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/templates/chart.html.jinja +0 -0
  23. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/templates/chips.html.jinja +0 -0
  24. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/templates/insights.html.jinja +0 -0
  25. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/templates/stats.html.jinja +0 -0
  26. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/templates/style.scss +0 -0
  27. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/templates/summary.html.jinja +0 -0
  28. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/templates/table.html.jinja +0 -0
  29. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/test_utils.py +0 -0
  30. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/data/__init__.py +0 -0
  31. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/data/arg_builder.py +0 -0
  32. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/data/input_data.py +0 -0
  33. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/data/load.py +0 -0
  34. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/data/test_utils.py +0 -0
  35. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/data/time_coordinates.py +0 -0
  36. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/model/__init__.py +0 -0
  37. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/model/adstock_hill.py +0 -0
  38. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/model/knots.py +0 -0
  39. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/model/media.py +0 -0
  40. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/model/model_test_data.py +0 -0
  41. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/model/posterior_sampler.py +0 -0
  42. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/model/prior_sampler.py +0 -0
  43. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/model/spec.py +0 -0
  44. {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/model/transformers.py +0 -0
  45. {google_meridian-1.0.8 → google_meridian-1.0.9}/pyproject.toml +0 -0
  46. {google_meridian-1.0.8 → google_meridian-1.0.9}/setup.cfg +0 -0
  47. {google_meridian-1.0.8 → google_meridian-1.0.9}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-meridian
3
- Version: 1.0.8
3
+ Version: 1.0.9
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:
@@ -393,7 +393,7 @@ To cite this repository:
393
393
  author = {Google Meridian Marketing Mix Modeling Team},
394
394
  title = {Meridian: Marketing Mix Modeling},
395
395
  url = {https://github.com/google/meridian},
396
- version = {1.0.8},
396
+ version = {1.0.9},
397
397
  year = {2025},
398
398
  }
399
399
  ```
@@ -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.0.8},
154
+ version = {1.0.9},
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.0.8
3
+ Version: 1.0.9
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:
@@ -393,7 +393,7 @@ To cite this repository:
393
393
  author = {Google Meridian Marketing Mix Modeling Team},
394
394
  title = {Meridian: Marketing Mix Modeling},
395
395
  url = {https://github.com/google/meridian},
396
- version = {1.0.8},
396
+ version = {1.0.9},
397
397
  year = {2025},
398
398
  }
399
399
  ```
@@ -14,7 +14,7 @@
14
14
 
15
15
  """Meridian API."""
16
16
 
17
- __version__ = "1.0.8"
17
+ __version__ = "1.0.9"
18
18
 
19
19
 
20
20
  from meridian import analysis
@@ -63,6 +63,8 @@ class DataTensors(tf.experimental.ExtensionType):
63
63
  controls: Optional tensor with dimensions `(n_geos, n_times, n_controls)`.
64
64
  revenue_per_kpi: Optional tensor with dimensions `(n_geos, T)` for any time
65
65
  dimension `T`.
66
+ time: Optional tensor of time coordinates in the "YYYY-mm-dd" string format
67
+ for time dimension `T`.
66
68
  """
67
69
 
68
70
  media: Optional[tf.Tensor]
@@ -76,6 +78,7 @@ class DataTensors(tf.experimental.ExtensionType):
76
78
  non_media_treatments: Optional[tf.Tensor]
77
79
  controls: Optional[tf.Tensor]
78
80
  revenue_per_kpi: Optional[tf.Tensor]
81
+ time: Optional[tf.Tensor]
79
82
 
80
83
  def __init__(
81
84
  self,
@@ -90,6 +93,7 @@ class DataTensors(tf.experimental.ExtensionType):
90
93
  non_media_treatments: Optional[tf.Tensor] = None,
91
94
  controls: Optional[tf.Tensor] = None,
92
95
  revenue_per_kpi: Optional[tf.Tensor] = None,
96
+ time: Optional[Sequence[str] | tf.Tensor] = None,
93
97
  ):
94
98
  self.media = tf.cast(media, tf.float32) if media is not None else None
95
99
  self.media_spend = (
@@ -130,6 +134,7 @@ class DataTensors(tf.experimental.ExtensionType):
130
134
  if revenue_per_kpi is not None
131
135
  else None
132
136
  )
137
+ self.time = tf.cast(time, tf.string) if time is not None else None
133
138
 
134
139
  def __validate__(self):
135
140
  self._validate_n_dims()
@@ -241,6 +246,8 @@ class DataTensors(tf.experimental.ExtensionType):
241
246
  f"New `{field.name}` must have 1 or 3 dimensions. Found"
242
247
  f" {tensor.ndim} dimensions."
243
248
  )
249
+ elif field.name == constants.TIME:
250
+ _check_n_dims(tensor, field.name, 1)
244
251
  else:
245
252
  _check_n_dims(tensor, field.name, 3)
246
253
 
@@ -283,7 +290,7 @@ class DataTensors(tf.experimental.ExtensionType):
283
290
  for var_name in required_fields:
284
291
  new_tensor = getattr(self, var_name)
285
292
  if new_tensor is not None and new_tensor.shape[0] != meridian.n_geos:
286
- # Skip spend data with only 1 dimension of (n_channels).
293
+ # Skip spend and time data with only 1 dimension.
287
294
  if new_tensor.ndim == 1:
288
295
  continue
289
296
  raise ValueError(
@@ -296,7 +303,7 @@ class DataTensors(tf.experimental.ExtensionType):
296
303
  ):
297
304
  """Validates the channel dimension of the specified data variables."""
298
305
  for var_name in required_fields:
299
- if var_name == constants.REVENUE_PER_KPI:
306
+ if var_name in [constants.REVENUE_PER_KPI, constants.TIME]:
300
307
  continue
301
308
  new_tensor = getattr(self, var_name)
302
309
  old_tensor = getattr(meridian.input_data, var_name)
@@ -317,12 +324,24 @@ class DataTensors(tf.experimental.ExtensionType):
317
324
  old_tensor = getattr(meridian.input_data, var_name)
318
325
 
319
326
  # Skip spend data with only 1 dimension of (n_channels).
320
- if new_tensor is not None and new_tensor.ndim == 1:
327
+ if (
328
+ var_name in [constants.MEDIA_SPEND, constants.RF_SPEND]
329
+ and new_tensor is not None
330
+ and new_tensor.ndim == 1
331
+ ):
321
332
  continue
322
333
 
323
334
  if new_tensor is not None:
324
335
  assert old_tensor is not None
325
- if new_tensor.shape[1] != old_tensor.shape[1]:
336
+ if (
337
+ var_name == constants.TIME
338
+ and new_tensor.shape[0] != old_tensor.shape[0]
339
+ ):
340
+ raise ValueError(
341
+ f"New `{var_name}` is expected to have {old_tensor.shape[0]}"
342
+ f" time periods. Found {new_tensor.shape[0]} time periods."
343
+ )
344
+ elif new_tensor.ndim > 1 and new_tensor.shape[1] != old_tensor.shape[1]:
326
345
  raise ValueError(
327
346
  f"New `{var_name}` is expected to have {old_tensor.shape[1]}"
328
347
  f" time periods. Found {new_tensor.shape[1]} time periods."
@@ -345,12 +364,24 @@ class DataTensors(tf.experimental.ExtensionType):
345
364
  if old_tensor is None:
346
365
  continue
347
366
  # Skip spend data with only 1 dimension of (n_channels).
348
- if new_tensor is not None and new_tensor.ndim == 1:
367
+ if (
368
+ var_name in [constants.MEDIA_SPEND, constants.RF_SPEND]
369
+ and new_tensor is not None
370
+ and new_tensor.ndim == 1
371
+ ):
349
372
  continue
350
373
 
351
374
  if new_tensor is None:
352
375
  missing_params.append(var_name)
353
- elif new_tensor.shape[1] != new_n_times:
376
+ elif var_name == constants.TIME and new_tensor.shape[0] != new_n_times:
377
+ raise ValueError(
378
+ "If the time dimension of any variable in `new_data` is "
379
+ "modified, then all variables must be provided with the same "
380
+ f"number of time periods. `{var_name}` has {new_tensor.shape[1]} "
381
+ "time periods, which does not match the modified number of time "
382
+ f"periods, {new_n_times}.",
383
+ )
384
+ elif new_tensor.ndim > 1 and new_tensor.shape[1] != new_n_times:
354
385
  raise ValueError(
355
386
  "If the time dimension of any variable in `new_data` is "
356
387
  "modified, then all variables must be provided with the same "
@@ -390,6 +421,10 @@ class DataTensors(tf.experimental.ExtensionType):
390
421
  old_tensor = meridian.controls
391
422
  elif var_name == constants.REVENUE_PER_KPI:
392
423
  old_tensor = meridian.revenue_per_kpi
424
+ elif var_name == constants.TIME:
425
+ old_tensor = tf.convert_to_tensor(
426
+ meridian.input_data.time.values.tolist(), dtype=tf.string
427
+ )
393
428
  else:
394
429
  continue
395
430
 
@@ -4663,11 +4698,11 @@ class Analyzer:
4663
4698
 
4664
4699
  def get_historical_spend(
4665
4700
  self,
4666
- selected_times: Sequence[str] | None,
4701
+ selected_times: Sequence[str] | None = None,
4667
4702
  include_media: bool = True,
4668
4703
  include_rf: bool = True,
4669
4704
  ) -> xr.DataArray:
4670
- """Gets the aggregated historical spend based on the time period.
4705
+ """Deprecated. Gets the aggregated historical spend based on the time.
4671
4706
 
4672
4707
  Args:
4673
4708
  selected_times: The time period to get the historical spends. If None, the
@@ -4681,6 +4716,51 @@ class Analyzer:
4681
4716
  An `xr.DataArray` with the coordinate `channel` and contains the data
4682
4717
  variable `spend`.
4683
4718
 
4719
+ Raises:
4720
+ ValueError: A ValueError is raised when `include_media` and `include_rf`
4721
+ are both False.
4722
+ """
4723
+ warnings.warn(
4724
+ "`get_historical_spend` is deprecated. Please use "
4725
+ "`get_aggregated_spend` with `new_data=None` instead.",
4726
+ DeprecationWarning,
4727
+ stacklevel=2,
4728
+ )
4729
+ return self.get_aggregated_spend(
4730
+ selected_times=selected_times,
4731
+ include_media=include_media,
4732
+ include_rf=include_rf,
4733
+ )
4734
+
4735
+ def get_aggregated_spend(
4736
+ self,
4737
+ new_data: DataTensors | None = None,
4738
+ selected_times: Sequence[str] | Sequence[bool] | None = None,
4739
+ include_media: bool = True,
4740
+ include_rf: bool = True,
4741
+ ) -> xr.DataArray:
4742
+ """Gets the aggregated spend based on the selected time.
4743
+
4744
+ Args:
4745
+ new_data: An optional `DataTensors` object containing the new `media`,
4746
+ `media_spend`, `reach`, `frequency`, `rf_spend` tensors. If `None`, the
4747
+ existing tensors from the Meridian object are used. If `new_data`
4748
+ argument is used, then the aggregated spend is computed using the values
4749
+ of the tensors passed in the `new_data` argument and the original values
4750
+ of all the remaining tensors. If any of the tensors in `new_data` is
4751
+ provided with a different number of time periods than in `InputData`,
4752
+ then all tensors must be provided with the same number of time periods.
4753
+ selected_times: The time period to get the aggregated spends. If None, the
4754
+ spend will be aggregated over all time periods.
4755
+ include_media: Whether to include spends for paid media channels that do
4756
+ not have R&F data.
4757
+ include_rf: Whether to include spends for paid media channels with R&F
4758
+ data.
4759
+
4760
+ Returns:
4761
+ An `xr.DataArray` with the coordinate `channel` and contains the data
4762
+ variable `spend`.
4763
+
4684
4764
  Raises:
4685
4765
  ValueError: A ValueError is raised when `include_media` and `include_rf`
4686
4766
  are both False.
@@ -4689,6 +4769,11 @@ class Analyzer:
4689
4769
  raise ValueError(
4690
4770
  "At least one of include_media or include_rf must be True."
4691
4771
  )
4772
+ new_data = new_data or DataTensors()
4773
+ required_tensors_names = constants.PAID_CHANNELS + constants.SPEND_DATA
4774
+ filled_data = new_data.validate_and_fill_missing_data(
4775
+ required_tensors_names, self._meridian
4776
+ )
4692
4777
 
4693
4778
  empty_da = xr.DataArray(
4694
4779
  dims=[constants.CHANNEL], coords={constants.CHANNEL: []}
@@ -4709,8 +4794,8 @@ class Analyzer:
4709
4794
  else:
4710
4795
  aggregated_media_spend = self._impute_and_aggregate_spend(
4711
4796
  selected_times,
4712
- self._meridian.media_tensors.media,
4713
- self._meridian.media_tensors.media_spend,
4797
+ filled_data.media,
4798
+ filled_data.media_spend,
4714
4799
  list(self._meridian.input_data.media_channel.values),
4715
4800
  )
4716
4801
 
@@ -4723,18 +4808,16 @@ class Analyzer:
4723
4808
  or self._meridian.rf_tensors.rf_spend is None
4724
4809
  ):
4725
4810
  warnings.warn(
4726
- "Requested spends for paid media channels with R&F data, but but the"
4811
+ "Requested spends for paid media channels with R&F data, but the"
4727
4812
  " channels are not available.",
4728
4813
  )
4729
4814
  aggregated_rf_spend = empty_da
4730
4815
  else:
4731
- rf_execution_values = (
4732
- self._meridian.rf_tensors.reach * self._meridian.rf_tensors.frequency
4733
- )
4816
+ rf_execution_values = filled_data.reach * filled_data.frequency
4734
4817
  aggregated_rf_spend = self._impute_and_aggregate_spend(
4735
4818
  selected_times,
4736
4819
  rf_execution_values,
4737
- self._meridian.rf_tensors.rf_spend,
4820
+ filled_data.rf_spend,
4738
4821
  list(self._meridian.input_data.rf_channel.values),
4739
4822
  )
4740
4823
 
@@ -4744,7 +4827,7 @@ class Analyzer:
4744
4827
 
4745
4828
  def _impute_and_aggregate_spend(
4746
4829
  self,
4747
- selected_times: Sequence[str] | None,
4830
+ selected_times: Sequence[str] | Sequence[bool] | None,
4748
4831
  media_execution_values: tf.Tensor,
4749
4832
  channel_spend: tf.Tensor,
4750
4833
  channel_names: Sequence[str],
@@ -4759,7 +4842,7 @@ class Analyzer:
4759
4842
  argument, its values only affect the output when imputation is required.
4760
4843
 
4761
4844
  Args:
4762
- selected_times: The time period to get the historical spend.
4845
+ selected_times: The time period to get the aggregated spend.
4763
4846
  media_execution_values: The media execution values over all time points.
4764
4847
  channel_spend: The spend over all time points. Its shape can be `(n_geos,
4765
4848
  n_times, n_media_channels)` or `(n_media_channels,)` if the data is
@@ -4775,17 +4858,24 @@ class Analyzer:
4775
4858
  "selected_times": selected_times,
4776
4859
  "aggregate_geos": True,
4777
4860
  "aggregate_times": True,
4861
+ "flexible_time_dim": True,
4778
4862
  }
4779
4863
 
4780
4864
  if channel_spend.ndim == 3:
4781
4865
  aggregated_spend = self.filter_and_aggregate_geos_and_times(
4782
4866
  channel_spend,
4867
+ has_media_dim=True,
4783
4868
  **dim_kwargs,
4784
4869
  ).numpy()
4785
4870
  # channel_spend.ndim can only be 3 or 1.
4786
4871
  else:
4787
4872
  # media spend can have more time points than the model time points
4788
- media_exe_values = media_execution_values[:, -self._meridian.n_times :, :]
4873
+ if media_execution_values.shape[1] == self._meridian.n_media_times:
4874
+ media_exe_values = media_execution_values[
4875
+ :, -self._meridian.n_times :, :
4876
+ ]
4877
+ else:
4878
+ media_exe_values = media_execution_values
4789
4879
  # Calculates CPM over all times and geos if the spend does not have time
4790
4880
  # and geo dimensions.
4791
4881
  target_media_exe_values = self.filter_and_aggregate_geos_and_times(