google-meridian 1.3.2__py3-none-any.whl → 1.5.0__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.
Files changed (78) hide show
  1. {google_meridian-1.3.2.dist-info → google_meridian-1.5.0.dist-info}/METADATA +18 -11
  2. google_meridian-1.5.0.dist-info/RECORD +112 -0
  3. {google_meridian-1.3.2.dist-info → google_meridian-1.5.0.dist-info}/WHEEL +1 -1
  4. {google_meridian-1.3.2.dist-info → google_meridian-1.5.0.dist-info}/top_level.txt +1 -0
  5. meridian/analysis/analyzer.py +558 -398
  6. meridian/analysis/optimizer.py +90 -68
  7. meridian/analysis/review/reviewer.py +4 -1
  8. meridian/analysis/summarizer.py +13 -3
  9. meridian/analysis/test_utils.py +2911 -2102
  10. meridian/analysis/visualizer.py +37 -14
  11. meridian/backend/__init__.py +106 -0
  12. meridian/constants.py +2 -0
  13. meridian/data/input_data.py +30 -52
  14. meridian/data/input_data_builder.py +2 -9
  15. meridian/data/test_utils.py +107 -51
  16. meridian/data/validator.py +48 -0
  17. meridian/mlflow/autolog.py +19 -9
  18. meridian/model/__init__.py +2 -0
  19. meridian/model/adstock_hill.py +3 -5
  20. meridian/model/context.py +1059 -0
  21. meridian/model/eda/constants.py +335 -4
  22. meridian/model/eda/eda_engine.py +723 -312
  23. meridian/model/eda/eda_outcome.py +177 -33
  24. meridian/model/equations.py +418 -0
  25. meridian/model/knots.py +58 -47
  26. meridian/model/model.py +228 -878
  27. meridian/model/model_test_data.py +38 -0
  28. meridian/model/posterior_sampler.py +103 -62
  29. meridian/model/prior_sampler.py +114 -94
  30. meridian/model/spec.py +23 -14
  31. meridian/templates/card.html.jinja +9 -7
  32. meridian/templates/chart.html.jinja +1 -6
  33. meridian/templates/finding.html.jinja +19 -0
  34. meridian/templates/findings.html.jinja +33 -0
  35. meridian/templates/formatter.py +41 -5
  36. meridian/templates/formatter_test.py +127 -0
  37. meridian/templates/style.css +66 -9
  38. meridian/templates/style.scss +85 -4
  39. meridian/templates/table.html.jinja +1 -0
  40. meridian/version.py +1 -1
  41. scenarioplanner/__init__.py +42 -0
  42. scenarioplanner/converters/__init__.py +25 -0
  43. scenarioplanner/converters/dataframe/__init__.py +28 -0
  44. scenarioplanner/converters/dataframe/budget_opt_converters.py +383 -0
  45. scenarioplanner/converters/dataframe/common.py +71 -0
  46. scenarioplanner/converters/dataframe/constants.py +137 -0
  47. scenarioplanner/converters/dataframe/converter.py +42 -0
  48. scenarioplanner/converters/dataframe/dataframe_model_converter.py +70 -0
  49. scenarioplanner/converters/dataframe/marketing_analyses_converters.py +543 -0
  50. scenarioplanner/converters/dataframe/rf_opt_converters.py +314 -0
  51. scenarioplanner/converters/mmm.py +743 -0
  52. scenarioplanner/converters/mmm_converter.py +58 -0
  53. scenarioplanner/converters/sheets.py +156 -0
  54. scenarioplanner/converters/test_data.py +714 -0
  55. scenarioplanner/linkingapi/__init__.py +47 -0
  56. scenarioplanner/linkingapi/constants.py +27 -0
  57. scenarioplanner/linkingapi/url_generator.py +131 -0
  58. scenarioplanner/mmm_ui_proto_generator.py +355 -0
  59. schema/__init__.py +5 -2
  60. schema/mmm_proto_generator.py +71 -0
  61. schema/model_consumer.py +133 -0
  62. schema/processors/__init__.py +77 -0
  63. schema/processors/budget_optimization_processor.py +832 -0
  64. schema/processors/common.py +64 -0
  65. schema/processors/marketing_processor.py +1137 -0
  66. schema/processors/model_fit_processor.py +367 -0
  67. schema/processors/model_kernel_processor.py +117 -0
  68. schema/processors/model_processor.py +415 -0
  69. schema/processors/reach_frequency_optimization_processor.py +584 -0
  70. schema/serde/distribution.py +12 -7
  71. schema/serde/hyperparameters.py +54 -107
  72. schema/serde/meridian_serde.py +6 -1
  73. schema/test_data.py +380 -0
  74. schema/utils/__init__.py +2 -0
  75. schema/utils/date_range_bucketing.py +117 -0
  76. schema/utils/proto_enum_converter.py +127 -0
  77. google_meridian-1.3.2.dist-info/RECORD +0 -76
  78. {google_meridian-1.3.2.dist-info → google_meridian-1.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -29,6 +29,7 @@ from meridian import constants as c
29
29
  from meridian.analysis import analyzer as analyzer_module
30
30
  from meridian.analysis import summary_text
31
31
  from meridian.data import time_coordinates as tc
32
+ from meridian.model import context
32
33
  from meridian.model import model
33
34
  from meridian.templates import formatter
34
35
  import numpy as np
@@ -234,7 +235,7 @@ class OptimizationGrid:
234
235
  spend_constraint_lower = spend_constraint_default
235
236
  if spend_constraint_upper is None:
236
237
  spend_constraint_upper = spend_constraint_default
237
- (optimization_lower_bound, optimization_upper_bound) = (
238
+ optimization_lower_bound, optimization_upper_bound = (
238
239
  get_optimization_bounds(
239
240
  n_channels=len(self.channels),
240
241
  spend=spend,
@@ -252,7 +253,7 @@ class OptimizationGrid:
252
253
  ' It is only a problem when you use a much smaller budget, '
253
254
  ' for which the intended step size is smaller. '
254
255
  )
255
- (spend_grid, incremental_outcome_grid) = self.trim_grids(
256
+ spend_grid, incremental_outcome_grid = self.trim_grids(
256
257
  spend_bound_lower=optimization_lower_bound,
257
258
  spend_bound_upper=optimization_upper_bound,
258
259
  )
@@ -924,7 +925,7 @@ class OptimizationResults:
924
925
  """
925
926
  channels = self.optimized_data.channel.values
926
927
  selected_times = _expand_selected_times(
927
- meridian=self.meridian,
928
+ model_context=self.analyzer.model_context,
928
929
  start_date=self.optimized_data.start_date,
929
930
  end_date=self.optimized_data.end_date,
930
931
  new_data=self.new_data,
@@ -1064,7 +1065,9 @@ class OptimizationResults:
1064
1065
  self.template_env.globals[c.START_DATE] = start_date.strftime(
1065
1066
  f'%b {start_date.day}, %Y'
1066
1067
  )
1067
- interval_days = self.meridian.input_data.time_coordinates.interval_days
1068
+ interval_days = (
1069
+ self.analyzer.model_context.input_data.time_coordinates.interval_days
1070
+ )
1068
1071
  end_date = tc.normalize_date(self.optimized_data.end_date)
1069
1072
  end_date_adjusted = end_date + pd.Timedelta(days=interval_days)
1070
1073
  self.template_env.globals[c.END_DATE] = end_date_adjusted.strftime(
@@ -1323,14 +1326,20 @@ class BudgetOptimizer:
1323
1326
  results can be viewed as plots and as an HTML summary output page.
1324
1327
  """
1325
1328
 
1326
- def __init__(self, meridian: model.Meridian):
1329
+ def __init__(
1330
+ self,
1331
+ meridian: model.Meridian,
1332
+ ):
1327
1333
  self._meridian = meridian
1328
- self._analyzer = analyzer_module.Analyzer(self._meridian)
1334
+ self._analyzer = analyzer_module.Analyzer(
1335
+ model_context=meridian.model_context,
1336
+ inference_data=meridian.inference_data,
1337
+ )
1329
1338
 
1330
1339
  def _validate_model_fit(self, use_posterior: bool):
1331
1340
  """Validates that the model is fit."""
1332
1341
  dist_type = c.POSTERIOR if use_posterior else c.PRIOR
1333
- if dist_type not in self._meridian.inference_data.groups():
1342
+ if dist_type not in self._analyzer.inference_data.groups():
1334
1343
  raise model.NotFittedModelError(
1335
1344
  'Running budget optimization scenarios requires fitting the model.'
1336
1345
  )
@@ -1654,7 +1663,9 @@ class BudgetOptimizer:
1654
1663
  out=np.zeros_like(optimization_grid.historical_spend, dtype=float),
1655
1664
  where=optimization_grid.historical_spend != 0,
1656
1665
  )
1657
- n_paid_channels = len(self._meridian.input_data.get_all_paid_channels())
1666
+ n_paid_channels = len(
1667
+ self._analyzer.model_context.input_data.get_all_paid_channels()
1668
+ )
1658
1669
  spend_bounds = _get_spend_bounds(
1659
1670
  n_channels=n_paid_channels,
1660
1671
  spend_constraint_lower=spend_constraint_lower,
@@ -1753,7 +1764,7 @@ class BudgetOptimizer:
1753
1764
  `frequency`, `media_spend`, `rf_spend`, `revenue_per_kpi`, and `time`.
1754
1765
  """
1755
1766
  n_times = time.shape[0] if isinstance(time, backend.Tensor) else len(time)
1756
- n_geos = self._meridian.n_geos
1767
+ n_geos = self._analyzer.model_context.n_geos
1757
1768
  self._validate_optimization_tensors(
1758
1769
  expected_n_geos=n_geos,
1759
1770
  expected_n_times=n_times,
@@ -1880,9 +1891,12 @@ class BudgetOptimizer:
1880
1891
  new_data = analyzer_module.DataTensors()
1881
1892
  required_tensors = c.PERFORMANCE_DATA + (c.TIME,)
1882
1893
  filled_data = new_data.validate_and_fill_missing_data(
1883
- required_tensors_names=required_tensors, meridian=self._meridian
1894
+ required_tensors_names=required_tensors,
1895
+ model_context=self._analyzer.model_context,
1896
+ )
1897
+ paid_channels = (
1898
+ self._analyzer.model_context.input_data.get_all_paid_channels()
1884
1899
  )
1885
- paid_channels = self._meridian.input_data.get_all_paid_channels()
1886
1900
  if not np.array_equal(paid_channels, optimization_grid.channels):
1887
1901
  warnings.warn(
1888
1902
  'Given optimization grid was created with `channels` ='
@@ -1905,7 +1919,7 @@ class BudgetOptimizer:
1905
1919
 
1906
1920
  n_channels = len(optimization_grid.channels)
1907
1921
  selected_times = _expand_selected_times(
1908
- meridian=self._meridian,
1922
+ model_context=self._analyzer.model_context,
1909
1923
  start_date=start_date,
1910
1924
  end_date=end_date,
1911
1925
  new_data=new_data,
@@ -1913,8 +1927,8 @@ class BudgetOptimizer:
1913
1927
  hist_spend = self._analyzer.get_aggregated_spend(
1914
1928
  new_data=filled_data.filter_fields(c.PAID_CHANNELS + c.SPEND_DATA),
1915
1929
  selected_times=selected_times,
1916
- include_media=self._meridian.n_media_channels > 0,
1917
- include_rf=self._meridian.n_rf_channels > 0,
1930
+ include_media=self._analyzer.model_context.n_media_channels > 0,
1931
+ include_rf=self._analyzer.model_context.n_rf_channels > 0,
1918
1932
  ).data
1919
1933
  budget = budget or np.sum(hist_spend)
1920
1934
  valid_pct_of_spend = _validate_pct_of_spend(
@@ -1923,7 +1937,7 @@ class BudgetOptimizer:
1923
1937
  pct_of_spend=pct_of_spend,
1924
1938
  )
1925
1939
  spend = budget * valid_pct_of_spend
1926
- (optimization_lower_bound, optimization_upper_bound) = (
1940
+ optimization_lower_bound, optimization_upper_bound = (
1927
1941
  get_optimization_bounds(
1928
1942
  n_channels=n_channels,
1929
1943
  spend=spend,
@@ -2075,11 +2089,13 @@ class BudgetOptimizer:
2075
2089
  end_date = end_date or deprecated_end_date
2076
2090
 
2077
2091
  required_tensors = c.PERFORMANCE_DATA + (c.TIME,)
2092
+ model_context = self._analyzer.model_context
2078
2093
  filled_data = new_data.validate_and_fill_missing_data(
2079
- required_tensors_names=required_tensors, meridian=self._meridian
2094
+ required_tensors_names=required_tensors,
2095
+ model_context=model_context,
2080
2096
  )
2081
2097
  selected_times = _expand_selected_times(
2082
- meridian=self._meridian,
2098
+ model_context=model_context,
2083
2099
  start_date=start_date,
2084
2100
  end_date=end_date,
2085
2101
  new_data=filled_data,
@@ -2088,10 +2104,10 @@ class BudgetOptimizer:
2088
2104
  new_data=filled_data.filter_fields(c.PAID_CHANNELS + c.SPEND_DATA),
2089
2105
  selected_geos=selected_geos,
2090
2106
  selected_times=selected_times,
2091
- include_media=self._meridian.n_media_channels > 0,
2092
- include_rf=self._meridian.n_rf_channels > 0,
2107
+ include_media=model_context.n_media_channels > 0,
2108
+ include_rf=model_context.n_rf_channels > 0,
2093
2109
  ).data
2094
- n_paid_channels = len(self._meridian.input_data.get_all_paid_channels())
2110
+ n_paid_channels = len(model_context.input_data.get_all_paid_channels())
2095
2111
  budget = budget or np.sum(hist_spend)
2096
2112
  valid_pct_of_spend = _validate_pct_of_spend(
2097
2113
  n_channels=n_paid_channels,
@@ -2100,7 +2116,7 @@ class BudgetOptimizer:
2100
2116
  )
2101
2117
  spend = budget * valid_pct_of_spend
2102
2118
  round_factor = get_round_factor(budget, gtol)
2103
- (optimization_lower_bound, optimization_upper_bound) = (
2119
+ optimization_lower_bound, optimization_upper_bound = (
2104
2120
  get_optimization_bounds(
2105
2121
  n_channels=n_paid_channels,
2106
2122
  spend=spend,
@@ -2109,7 +2125,7 @@ class BudgetOptimizer:
2109
2125
  spend_constraint_upper=spend_constraint_upper,
2110
2126
  )
2111
2127
  )
2112
- if self._meridian.n_rf_channels > 0 and use_optimal_frequency:
2128
+ if model_context.n_rf_channels > 0 and use_optimal_frequency:
2113
2129
  opt_freq_data = analyzer_module.DataTensors(
2114
2130
  rf_impressions=filled_data.reach * filled_data.frequency,
2115
2131
  rf_spend=filled_data.rf_spend,
@@ -2130,7 +2146,7 @@ class BudgetOptimizer:
2130
2146
  optimal_frequency = None
2131
2147
 
2132
2148
  step_size = 10 ** (-round_factor)
2133
- (spend_grid, incremental_outcome_grid) = self._create_grids(
2149
+ spend_grid, incremental_outcome_grid = self._create_grids(
2134
2150
  spend=hist_spend,
2135
2151
  spend_bound_lower=optimization_lower_bound,
2136
2152
  spend_bound_upper=optimization_upper_bound,
@@ -2200,7 +2216,9 @@ class BudgetOptimizer:
2200
2216
  data_vars=data_vars,
2201
2217
  coords={
2202
2218
  c.GRID_SPEND_INDEX: np.arange(0, len(spend_grid)),
2203
- c.CHANNEL: self._meridian.input_data.get_all_paid_channels(),
2219
+ c.CHANNEL: (
2220
+ self._analyzer.model_context.input_data.get_all_paid_channels()
2221
+ ),
2204
2222
  },
2205
2223
  attrs={c.SPEND_STEP_SIZE: spend_step_size},
2206
2224
  )
@@ -2246,26 +2264,27 @@ class BudgetOptimizer:
2246
2264
  Tuple of backend.tensors (new_media, new_reach, new_frequency).
2247
2265
  """
2248
2266
  new_data = new_data or analyzer_module.DataTensors()
2267
+ model_context = self._analyzer.model_context
2249
2268
  filled_data = new_data.validate_and_fill_missing_data(
2250
- c.PAID_CHANNELS,
2251
- self._meridian,
2269
+ required_tensors_names=c.PAID_CHANNELS,
2270
+ model_context=model_context,
2252
2271
  )
2253
- if self._meridian.n_media_channels > 0:
2272
+ if model_context.n_media_channels > 0:
2254
2273
  new_media = (
2255
2274
  backend.divide_no_nan(
2256
- spend[: self._meridian.n_media_channels],
2257
- hist_spend[: self._meridian.n_media_channels],
2275
+ spend[: model_context.n_media_channels],
2276
+ hist_spend[: model_context.n_media_channels],
2258
2277
  )
2259
2278
  * filled_data.media
2260
2279
  )
2261
2280
  else:
2262
2281
  new_media = None
2263
- if self._meridian.n_rf_channels > 0:
2282
+ if model_context.n_rf_channels > 0:
2264
2283
  rf_impressions = filled_data.reach * filled_data.frequency
2265
2284
  new_rf_impressions = (
2266
2285
  backend.divide_no_nan(
2267
- spend[-self._meridian.n_rf_channels :],
2268
- hist_spend[-self._meridian.n_rf_channels :],
2286
+ spend[-model_context.n_rf_channels :],
2287
+ hist_spend[-model_context.n_rf_channels :],
2269
2288
  )
2270
2289
  * rf_impressions
2271
2290
  )
@@ -2299,26 +2318,25 @@ class BudgetOptimizer:
2299
2318
  use_historical_budget: bool = True,
2300
2319
  ) -> xr.Dataset:
2301
2320
  """Creates the budget dataset."""
2321
+ model_context = self._analyzer.model_context
2302
2322
  new_data = new_data or analyzer_module.DataTensors()
2303
2323
  filled_data = new_data.validate_and_fill_missing_data(
2304
- c.PAID_DATA + (c.TIME,),
2305
- self._meridian,
2324
+ required_tensors_names=c.PAID_DATA + (c.TIME,),
2325
+ model_context=model_context,
2306
2326
  )
2307
2327
  selected_times = _expand_selected_times(
2308
- meridian=self._meridian,
2328
+ model_context=self._analyzer.model_context,
2309
2329
  start_date=start_date,
2310
2330
  end_date=end_date,
2311
2331
  new_data=new_data,
2312
2332
  )
2313
2333
  spend_tensor = backend.to_tensor(spend, dtype=backend.float32)
2314
2334
  hist_spend = backend.to_tensor(hist_spend, dtype=backend.float32)
2315
- (new_media, new_reach, new_frequency) = (
2316
- self._get_incremental_outcome_tensors(
2317
- hist_spend,
2318
- spend_tensor,
2319
- new_data=filled_data.filter_fields(c.PAID_CHANNELS),
2320
- optimal_frequency=optimal_frequency,
2321
- )
2335
+ new_media, new_reach, new_frequency = self._get_incremental_outcome_tensors(
2336
+ hist_spend,
2337
+ spend_tensor,
2338
+ new_data=filled_data.filter_fields(c.PAID_CHANNELS),
2339
+ optimal_frequency=optimal_frequency,
2322
2340
  )
2323
2341
  budget = np.sum(spend_tensor)
2324
2342
  inc_outcome_data = analyzer_module.DataTensors(
@@ -2379,21 +2397,19 @@ class BudgetOptimizer:
2379
2397
  )
2380
2398
  effectiveness_with_mean_median_and_ci = (
2381
2399
  analyzer_module.get_central_tendency_and_ci(
2382
- data=backend.divide_no_nan(
2383
- incremental_outcome, aggregated_impressions
2384
- ),
2400
+ data=backend.divide(incremental_outcome, aggregated_impressions),
2385
2401
  confidence_level=confidence_level,
2386
2402
  include_median=True,
2387
2403
  )
2388
2404
  )
2389
2405
 
2390
2406
  roi = analyzer_module.get_central_tendency_and_ci(
2391
- data=backend.divide_no_nan(incremental_outcome, spend_tensor),
2407
+ data=backend.divide(incremental_outcome, spend_tensor),
2392
2408
  confidence_level=confidence_level,
2393
2409
  include_median=True,
2394
2410
  )
2395
2411
  marginal_roi = analyzer_module.get_central_tendency_and_ci(
2396
- data=backend.divide_no_nan(
2412
+ data=backend.divide(
2397
2413
  mroi_numerator, spend_tensor * incremental_increase
2398
2414
  ),
2399
2415
  confidence_level=confidence_level,
@@ -2401,13 +2417,13 @@ class BudgetOptimizer:
2401
2417
  )
2402
2418
 
2403
2419
  cpik = analyzer_module.get_central_tendency_and_ci(
2404
- data=backend.divide_no_nan(spend_tensor, incremental_outcome),
2420
+ data=backend.divide(spend_tensor, incremental_outcome),
2405
2421
  confidence_level=confidence_level,
2406
2422
  include_median=True,
2407
2423
  )
2408
2424
  total_inc_outcome = backend.reduce_sum(incremental_outcome, -1)
2409
- total_cpik = backend.reduce_mean(
2410
- backend.divide_no_nan(budget, total_inc_outcome),
2425
+ total_cpik = backend.nanmean(
2426
+ backend.divide(budget, total_inc_outcome),
2411
2427
  axis=(0, 1),
2412
2428
  )
2413
2429
 
@@ -2448,7 +2464,7 @@ class BudgetOptimizer:
2448
2464
  c.TOTAL_ROI: total_incremental_outcome / budget,
2449
2465
  c.TOTAL_CPIK: total_cpik,
2450
2466
  c.IS_REVENUE_KPI: (
2451
- self._meridian.input_data.kpi_type == c.REVENUE or not use_kpi
2467
+ model_context.input_data.kpi_type == c.REVENUE or not use_kpi
2452
2468
  ),
2453
2469
  c.CONFIDENCE_LEVEL: confidence_level,
2454
2470
  c.USE_HISTORICAL_BUDGET: use_historical_budget,
@@ -2457,7 +2473,7 @@ class BudgetOptimizer:
2457
2473
  return xr.Dataset(
2458
2474
  data_vars=data_vars,
2459
2475
  coords={
2460
- c.CHANNEL: self._meridian.input_data.get_all_paid_channels(),
2476
+ c.CHANNEL: model_context.input_data.get_all_paid_channels(),
2461
2477
  c.METRIC: [c.MEAN, c.MEDIAN, c.CI_LO, c.CI_HI],
2462
2478
  },
2463
2479
  attrs=attributes | (attrs or {}),
@@ -2514,19 +2530,21 @@ class BudgetOptimizer:
2514
2530
  reducing `batch_size`. The calculation will generally be faster with
2515
2531
  larger `batch_size` values.
2516
2532
  """
2533
+ model_context = self._analyzer.model_context
2517
2534
  new_data = new_data or analyzer_module.DataTensors()
2518
2535
  filled_data = new_data.validate_and_fill_missing_data(
2519
- c.PAID_DATA, self._meridian
2536
+ required_tensors_names=c.PAID_DATA,
2537
+ model_context=model_context,
2520
2538
  )
2521
- if self._meridian.n_media_channels > 0:
2539
+ if model_context.n_media_channels > 0:
2522
2540
  new_media = (
2523
- multipliers_grid[i, : self._meridian.n_media_channels]
2541
+ multipliers_grid[i, : model_context.n_media_channels]
2524
2542
  * filled_data.media
2525
2543
  )
2526
2544
  else:
2527
2545
  new_media = None
2528
2546
 
2529
- if self._meridian.n_rf_channels == 0:
2547
+ if model_context.n_rf_channels == 0:
2530
2548
  new_frequency = None
2531
2549
  new_reach = None
2532
2550
  elif optimal_frequency is not None:
@@ -2534,7 +2552,7 @@ class BudgetOptimizer:
2534
2552
  backend.ones_like(filled_data.frequency) * optimal_frequency
2535
2553
  )
2536
2554
  new_reach = backend.divide_no_nan(
2537
- multipliers_grid[i, -self._meridian.n_rf_channels :]
2555
+ multipliers_grid[i, -model_context.n_rf_channels :]
2538
2556
  * filled_data.reach
2539
2557
  * filled_data.frequency,
2540
2558
  new_frequency,
@@ -2542,7 +2560,7 @@ class BudgetOptimizer:
2542
2560
  else:
2543
2561
  new_frequency = filled_data.frequency
2544
2562
  new_reach = (
2545
- multipliers_grid[i, -self._meridian.n_rf_channels :]
2563
+ multipliers_grid[i, -model_context.n_rf_channels :]
2546
2564
  * filled_data.reach
2547
2565
  )
2548
2566
 
@@ -2634,11 +2652,12 @@ class BudgetOptimizer:
2634
2652
  number of columns is equal to the number of total channels, containing
2635
2653
  incremental outcome by channel.
2636
2654
  """
2655
+ model_context = self._analyzer.model_context
2637
2656
  n_grid_rows = int(
2638
2657
  (np.max(np.subtract(spend_bound_upper, spend_bound_lower)) // step_size)
2639
2658
  + 1
2640
2659
  )
2641
- n_grid_columns = len(self._meridian.input_data.get_all_paid_channels())
2660
+ n_grid_columns = len(model_context.input_data.get_all_paid_channels())
2642
2661
  spend_grid = np.full([n_grid_rows, n_grid_columns], np.nan)
2643
2662
  for i in range(n_grid_columns):
2644
2663
  spend_grid_m = np.arange(
@@ -2674,9 +2693,9 @@ class BudgetOptimizer:
2674
2693
  # np.unravel_index(np.nanargmax(iROAS_grid), iROAS_grid.shape). Therefore
2675
2694
  # we use the following code to fix it, and ensure incremental_outcome/spend
2676
2695
  # is always same for RF channels.
2677
- if self._meridian.n_rf_channels > 0:
2696
+ if model_context.n_rf_channels > 0:
2678
2697
  incremental_outcome_grid = backend.stabilize_rf_roi_grid(
2679
- spend_grid, incremental_outcome_grid, self._meridian.n_rf_channels
2698
+ spend_grid, incremental_outcome_grid, model_context.n_rf_channels
2680
2699
  )
2681
2700
  return (spend_grid, incremental_outcome_grid)
2682
2701
 
@@ -2794,7 +2813,7 @@ class BudgetOptimizer:
2794
2813
  f'{tensor.ndim} dimensions.'
2795
2814
  )
2796
2815
 
2797
- population = self._meridian.population
2816
+ population = self._analyzer.model_context.input_data.population
2798
2817
  normalized_population = population / backend.reduce_sum(population)
2799
2818
  if tensor.ndim == 1:
2800
2819
  reshaped_population = normalized_population[:, backend.newaxis]
@@ -2948,7 +2967,7 @@ def _get_spend_bounds(
2948
2967
  spend_bounds: tuple of np.ndarray of size `n_total_channels` containing
2949
2968
  the untreated lower and upper bound spend for each media and RF channel.
2950
2969
  """
2951
- (spend_const_lower, spend_const_upper) = _validate_spend_constraints(
2970
+ spend_const_lower, spend_const_upper = _validate_spend_constraints(
2952
2971
  n_channels,
2953
2972
  spend_constraint_lower,
2954
2973
  spend_constraint_upper,
@@ -3053,7 +3072,10 @@ def _raise_warning_if_target_constraints_not_met(
3053
3072
  # optimized_data[c.MROI] is an array of shape (n_channels, 4), where the
3054
3073
  # last dimension is [mean, median, ci_lo, ci_hi].
3055
3074
  optimized_mroi = optimized_data[c.MROI][:, 0]
3056
- if np.any(optimized_mroi < target_mroi):
3075
+ # Replace NaN with -np.inf so it's treated as failing the constraint.
3076
+ # +/-inf will be converted to large-magnitude finite numbers.
3077
+ compare_mroi = np.nan_to_num(optimized_mroi, nan=-np.inf)
3078
+ if np.any(compare_mroi < target_mroi):
3057
3079
  warnings.warn(
3058
3080
  'Target marginal ROI constraint was not met. The target marginal'
3059
3081
  f' ROI is {target_mroi}, but the actual channel marginal ROIs are'
@@ -3088,7 +3110,7 @@ def _expand_tensor(tensor: backend.Tensor, required_shape: tuple[int, ...]):
3088
3110
 
3089
3111
 
3090
3112
  def _expand_selected_times(
3091
- meridian: model.Meridian,
3113
+ model_context: context.ModelContext,
3092
3114
  start_date: tc.Date,
3093
3115
  end_date: tc.Date,
3094
3116
  new_data: analyzer_module.DataTensors | None,
@@ -3104,7 +3126,7 @@ def _expand_selected_times(
3104
3126
  and the function returns a list of booleans.
3105
3127
 
3106
3128
  Args:
3107
- meridian: The `Meridian` object with original data.
3129
+ model_context: The `ModelContext` object with original data.
3108
3130
  start_date: Start date of the selected time period.
3109
3131
  end_date: End date of the selected time period.
3110
3132
  new_data: The optional `DataTensors` object. If times are modified in
@@ -3123,8 +3145,8 @@ def _expand_selected_times(
3123
3145
  return None
3124
3146
 
3125
3147
  new_data = new_data or analyzer_module.DataTensors()
3126
- if new_data.get_modified_times(meridian) is None:
3127
- return meridian.expand_selected_time_dims(
3148
+ if new_data.get_modified_times(model_context=model_context) is None:
3149
+ return model_context.expand_selected_time_dims(
3128
3150
  start_date=start_date,
3129
3151
  end_date=end_date,
3130
3152
  )
@@ -75,7 +75,10 @@ class ModelReviewer:
75
75
  ):
76
76
  self._meridian = meridian
77
77
  self._results: list[results.CheckResult] = []
78
- self._analyzer = analyzer_module.Analyzer(meridian)
78
+ self._analyzer = analyzer_module.Analyzer(
79
+ model_context=meridian.model_context,
80
+ inference_data=meridian.inference_data,
81
+ )
79
82
  self._post_convergence_checks = post_convergence_checks
80
83
 
81
84
  def _run_and_handle(self, check_class, config):
@@ -60,10 +60,15 @@ RESPONSE_CURVES_CARD_SPEC = formatter.CardSpec(
60
60
  class Summarizer:
61
61
  """Generates HTML summary visualizations from the model fitting."""
62
62
 
63
+ # TODO: Switch to model_context, model_equations, and
64
+ # inference_data.
63
65
  def __init__(self, meridian: model.Meridian, use_kpi: bool = False):
64
66
  """Initialize the visualizer classes that are not time-dependent."""
65
67
  self._meridian = meridian
66
- self._use_kpi = analyzer.Analyzer(meridian)._use_kpi(use_kpi)
68
+ self._use_kpi = analyzer.Analyzer(
69
+ model_context=meridian.model_context,
70
+ inference_data=meridian.inference_data,
71
+ )._use_kpi(use_kpi)
67
72
 
68
73
  @functools.cached_property
69
74
  def _model_fit(self):
@@ -318,7 +323,7 @@ class Summarizer:
318
323
  chart_json=media_summary.plot_contribution_waterfall_chart().to_json(),
319
324
  )
320
325
  lead_channels = self._get_sorted_posterior_mean_metrics_df(
321
- media_summary, [c.INCREMENTAL_OUTCOME]
326
+ media_summary, [c.INCREMENTAL_OUTCOME], include_non_paid_channels=True
322
327
  )[c.CHANNEL][:2]
323
328
  formatted_channels = [channel.title() for channel in lead_channels]
324
329
 
@@ -358,9 +363,14 @@ class Summarizer:
358
363
  media_summary: visualizer.MediaSummary,
359
364
  metrics: Sequence[str],
360
365
  ascending: bool = False,
366
+ include_non_paid_channels: bool = False,
361
367
  ) -> pd.DataFrame:
368
+ if include_non_paid_channels:
369
+ summary_metrics = media_summary.get_all_summary_metrics()
370
+ else:
371
+ summary_metrics = media_summary.get_paid_summary_metrics()
362
372
  return (
363
- media_summary.get_paid_summary_metrics()[metrics]
373
+ summary_metrics[metrics]
364
374
  .sel(distribution=c.POSTERIOR, metric=c.MEAN)
365
375
  .drop_sel(channel=c.ALL_CHANNELS)
366
376
  .to_dataframe()