google-meridian 1.0.5__py3-none-any.whl → 1.0.6__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.
@@ -20,6 +20,7 @@ import functools
20
20
  import math
21
21
  import os
22
22
  from typing import Any, TypeAlias
23
+ import warnings
23
24
 
24
25
  import altair as alt
25
26
  import jinja2
@@ -45,14 +46,53 @@ alt.data_transformers.disable_max_rows()
45
46
  _SpendConstraint: TypeAlias = float | Sequence[float]
46
47
 
47
48
 
49
+ @dataclasses.dataclass(frozen=True)
50
+ class FixedBudgetScenario:
51
+ """A fixed budget optimization scenario.
52
+
53
+ Attributes:
54
+ total_budget: The total budget for the optimization period. Must be
55
+ non-negative. If unspecified, it represents historical total spend.
56
+ """
57
+
58
+ total_budget: float | None = None
59
+
60
+ def __post_init__(self):
61
+ if self.total_budget is not None and self.total_budget < 0:
62
+ raise ValueError('Total budget must be non-negative.')
63
+
64
+
65
+ @dataclasses.dataclass(frozen=True)
66
+ class FlexibleBudgetScenario:
67
+ """A flexible budget optimization scenario.
68
+
69
+ Attributes:
70
+ target_metric: The target metric to optimize for. This should be ROI or
71
+ mROI.
72
+ target_value: The target value for the above metric. Must be non-negative.
73
+ """
74
+
75
+ target_metric: str
76
+ target_value: float
77
+
78
+ def __post_init__(self):
79
+ if self.target_metric not in (c.ROI, c.MROI):
80
+ raise ValueError(
81
+ f'Unsupported target metric: {self.target_metric} for flexible budget'
82
+ ' scenario.'
83
+ )
84
+ if self.target_value < 0:
85
+ raise ValueError('Target value must be non-negative.')
86
+
87
+
48
88
  @dataclasses.dataclass(frozen=True)
49
89
  class OptimizationGrid:
50
90
  """Optimization grid information.
51
91
 
52
92
  Attributes:
53
- spend: ndarray of shape `(n_paid_channels,)` containing the spend allocation
54
- for spend for all media and RF channels. The order matches
55
- `InputData.get_all_paid_channels`.
93
+ historical_spend: ndarray of shape `(n_paid_channels,)` containing
94
+ aggregated historical spend allocation for spend for all media and RF
95
+ channels. The order matches `InputData.get_all_paid_channels`.
56
96
  use_kpi: Whether using generic KPI or revenue.
57
97
  use_posterior: Whether posterior distributions were used, or prior.
58
98
  use_optimal_frequency: Whether optimal frequency was used.
@@ -67,7 +107,7 @@ class OptimizationGrid:
67
107
 
68
108
  _grid_dataset: xr.Dataset
69
109
 
70
- spend: np.ndarray
110
+ historical_spend: np.ndarray
71
111
  use_kpi: bool
72
112
  use_posterior: bool
73
113
  use_optimal_frequency: bool
@@ -102,6 +142,78 @@ class OptimizationGrid:
102
142
  """The spend step size."""
103
143
  return self.grid_dataset.attrs[c.SPEND_STEP_SIZE]
104
144
 
145
+ # TODO: b/402950014 - Add per-channel constraints parameter.
146
+ def optimize(
147
+ self,
148
+ scenario: FixedBudgetScenario | FlexibleBudgetScenario,
149
+ ) -> np.ndarray:
150
+ """Hill-climbing search algorithm for budget optimization.
151
+
152
+ Args:
153
+ scenario: The optimization scenario with corresponding parameters.
154
+
155
+ Returns:
156
+ optimal_spend: `np.ndarray` with shape `(n_paid_channels,)` containing the
157
+ media spend that maximizes incremental outcome based on spend
158
+ constraints for all media and RF channels.
159
+ """
160
+ if (
161
+ isinstance(scenario, FixedBudgetScenario)
162
+ and scenario.total_budget is None
163
+ ):
164
+ rounded_spend = np.round(self.historical_spend, self.round_factor).astype(
165
+ int
166
+ )
167
+ budget = np.sum(rounded_spend)
168
+ scenario = dataclasses.replace(scenario, total_budget=budget)
169
+
170
+ spend = self.spend_grid[0, :].copy()
171
+ incremental_outcome = self.incremental_outcome_grid[0, :].copy()
172
+ spend_grid = self.spend_grid[1:, :]
173
+ incremental_outcome_grid = self.incremental_outcome_grid[1:, :]
174
+ iterative_roi_grid = np.round(
175
+ tf.math.divide_no_nan(
176
+ incremental_outcome_grid - incremental_outcome, spend_grid - spend
177
+ ),
178
+ decimals=8,
179
+ )
180
+ while True:
181
+ spend_optimal = spend.astype(int)
182
+ # If none of the exit criteria are met roi_grid will eventually be filled
183
+ # with all nans.
184
+ if np.isnan(iterative_roi_grid).all():
185
+ break
186
+ point = np.unravel_index(
187
+ np.nanargmax(iterative_roi_grid), iterative_roi_grid.shape
188
+ )
189
+ row_idx = point[0]
190
+ media_idx = point[1]
191
+ spend[media_idx] = spend_grid[row_idx, media_idx]
192
+ incremental_outcome[media_idx] = incremental_outcome_grid[
193
+ row_idx, media_idx
194
+ ]
195
+ roi_grid_point = iterative_roi_grid[row_idx, media_idx]
196
+ if _exceeds_optimization_constraints(
197
+ spend=spend,
198
+ incremental_outcome=incremental_outcome,
199
+ roi_grid_point=roi_grid_point,
200
+ scenario=scenario,
201
+ ):
202
+ break
203
+
204
+ iterative_roi_grid[0 : row_idx + 1, media_idx] = np.nan
205
+ iterative_roi_grid[row_idx + 1 :, media_idx] = np.round(
206
+ tf.math.divide_no_nan(
207
+ incremental_outcome_grid[row_idx + 1 :, media_idx]
208
+ - incremental_outcome_grid[row_idx, media_idx],
209
+ spend_grid[row_idx + 1 :, media_idx]
210
+ - spend_grid[row_idx, media_idx],
211
+ ),
212
+ decimals=8,
213
+ )
214
+
215
+ return spend_optimal
216
+
105
217
 
106
218
  @dataclasses.dataclass(frozen=True)
107
219
  class OptimizationResults:
@@ -377,7 +489,8 @@ class OptimizationResults:
377
489
  .properties(
378
490
  title=formatter.custom_title_params(
379
491
  summary_text.SPEND_ALLOCATION_CHART_TITLE
380
- )
492
+ ),
493
+ width=c.VEGALITE_FACET_DEFAULT_WIDTH
381
494
  )
382
495
  )
383
496
 
@@ -416,7 +529,7 @@ class OptimizationResults:
416
529
  tooltip=True, size=c.BAR_SIZE, cornerRadiusEnd=c.CORNER_RADIUS
417
530
  ).encode(
418
531
  color=alt.condition(
419
- alt.expr.datum.spend > 0,
532
+ alt.datum.spend > 0,
420
533
  alt.value(c.CYAN_400),
421
534
  alt.value(c.RED_300),
422
535
  ),
@@ -461,7 +574,7 @@ class OptimizationResults:
461
574
  title = summary_text.INC_KPI_LABEL
462
575
  df = self._get_plottable_response_curves_df(n_top_channels=n_top_channels)
463
576
  base = (
464
- alt.Chart(df)
577
+ alt.Chart(df, width=c.VEGALITE_FACET_DEFAULT_WIDTH)
465
578
  .transform_calculate(
466
579
  spend_constraint=(
467
580
  'datum.spend_multiplier >= datum.lower_bound &&'
@@ -1051,6 +1164,9 @@ class BudgetOptimizer:
1051
1164
  include_rf=self._meridian.n_rf_channels > 0,
1052
1165
  ).data
1053
1166
 
1167
+ use_historical_budget = budget is None or round(budget) == round(
1168
+ np.sum(hist_spend)
1169
+ )
1054
1170
  budget = budget or np.sum(hist_spend)
1055
1171
  pct_of_spend = self._validate_pct_of_spend(hist_spend, pct_of_spend)
1056
1172
  spend = budget * pct_of_spend
@@ -1078,7 +1194,7 @@ class BudgetOptimizer:
1078
1194
  )
1079
1195
  )
1080
1196
  optimization_grid = self.create_optimization_grid(
1081
- spend=hist_spend,
1197
+ historical_spend=hist_spend,
1082
1198
  spend_bound_lower=optimization_lower_bound,
1083
1199
  spend_bound_upper=optimization_upper_bound,
1084
1200
  selected_times=selected_time_dims,
@@ -1089,17 +1205,21 @@ class BudgetOptimizer:
1089
1205
  optimal_frequency=optimal_frequency,
1090
1206
  batch_size=batch_size,
1091
1207
  )
1092
- # TODO: b/375644691) - Move grid search to a OptimizationGrid class.
1093
- optimal_spend = self._grid_search(
1094
- spend_grid=optimization_grid.spend_grid,
1095
- incremental_outcome_grid=optimization_grid.incremental_outcome_grid,
1096
- budget=np.sum(rounded_spend),
1097
- fixed_budget=fixed_budget,
1098
- target_mroi=target_mroi,
1099
- target_roi=target_roi,
1100
- )
1101
- use_historical_budget = budget is None or round(budget) == round(
1102
- np.sum(hist_spend)
1208
+
1209
+ if fixed_budget:
1210
+ total_budget = None if use_historical_budget else np.sum(rounded_spend)
1211
+ scenario = FixedBudgetScenario(total_budget=total_budget)
1212
+ elif target_roi:
1213
+ scenario = FlexibleBudgetScenario(
1214
+ target_metric=c.ROI, target_value=target_roi
1215
+ )
1216
+ else:
1217
+ scenario = FlexibleBudgetScenario(
1218
+ target_metric=c.MROI, target_value=target_mroi
1219
+ )
1220
+
1221
+ optimal_spend = optimization_grid.optimize(
1222
+ scenario=scenario,
1103
1223
  )
1104
1224
  nonoptimized_data = self._create_budget_dataset(
1105
1225
  use_posterior=use_posterior,
@@ -1141,6 +1261,14 @@ class BudgetOptimizer:
1141
1261
  batch_size=batch_size,
1142
1262
  use_historical_budget=use_historical_budget,
1143
1263
  )
1264
+
1265
+ if not fixed_budget:
1266
+ self._raise_warning_if_target_constraints_not_met(
1267
+ target_roi=target_roi,
1268
+ target_mroi=target_mroi,
1269
+ optimized_data=optimized_data,
1270
+ )
1271
+
1144
1272
  spend_ratio = np.divide(
1145
1273
  spend,
1146
1274
  hist_spend,
@@ -1159,23 +1287,51 @@ class BudgetOptimizer:
1159
1287
  _optimization_grid=optimization_grid,
1160
1288
  )
1161
1289
 
1290
+ def _raise_warning_if_target_constraints_not_met(
1291
+ self,
1292
+ target_roi: float | None,
1293
+ target_mroi: float | None,
1294
+ optimized_data: xr.Dataset,
1295
+ ) -> None:
1296
+ """Raises a warning if the target constraints are not met."""
1297
+ if target_roi:
1298
+ # Total ROI is a scalar value.
1299
+ optimized_roi = optimized_data.attrs[c.TOTAL_ROI]
1300
+ if optimized_roi < target_roi:
1301
+ warnings.warn(
1302
+ f'Target ROI constraint was not met. The target ROI is {target_roi}'
1303
+ f', but the actual ROI is {optimized_roi}.'
1304
+ )
1305
+ elif target_mroi:
1306
+ # Compare each channel's marginal ROI to the target.
1307
+ # optimized_data[c.MROI] is an array of shape (n_channels, 4), where the
1308
+ # last dimension is [mean, median, ci_lo, ci_hi].
1309
+ optimized_mroi = optimized_data[c.MROI][:, 0]
1310
+ if np.any(optimized_mroi < target_mroi):
1311
+ warnings.warn(
1312
+ 'Target marginal ROI constraint was not met. The target marginal'
1313
+ f' ROI is {target_mroi}, but the actual channel marginal ROIs are'
1314
+ f' {optimized_mroi}.'
1315
+ )
1316
+
1162
1317
  def create_optimization_grid(
1163
1318
  self,
1164
- spend: np.ndarray,
1319
+ historical_spend: np.ndarray,
1165
1320
  spend_bound_lower: np.ndarray,
1166
1321
  spend_bound_upper: np.ndarray,
1167
1322
  selected_times: Sequence[str] | None,
1168
1323
  round_factor: int,
1169
1324
  use_posterior: bool = True,
1170
1325
  use_kpi: bool = False,
1171
- use_optimal_frequency: bool = True,
1326
+ use_optimal_frequency: bool = False,
1172
1327
  optimal_frequency: xr.DataArray | None = None,
1173
1328
  batch_size: int = c.DEFAULT_BATCH_SIZE,
1174
1329
  ) -> OptimizationGrid:
1175
1330
  """Creates a OptimizationGrid for optimization.
1176
1331
 
1177
1332
  Args:
1178
- spend: ndarray of shape `(n_paid_channels,)` with spend per paid channel.
1333
+ historical_spend: ndarray of shape `(n_paid_channels,)` with arrgegated
1334
+ historical spend per paid channel.
1179
1335
  spend_bound_lower: ndarray of dimension `(n_total_channels,)` containing
1180
1336
  the lower constraint spend for each channel.
1181
1337
  spend_bound_upper: ndarray of dimension `(n_total_channels,)` containing
@@ -1208,7 +1364,7 @@ class BudgetOptimizer:
1208
1364
 
1209
1365
  step_size = 10 ** (-round_factor)
1210
1366
  (spend_grid, incremental_outcome_grid) = self._create_grids(
1211
- spend=spend,
1367
+ spend=historical_spend,
1212
1368
  spend_bound_lower=spend_bound_lower,
1213
1369
  spend_bound_upper=spend_bound_upper,
1214
1370
  step_size=step_size,
@@ -1226,7 +1382,7 @@ class BudgetOptimizer:
1226
1382
 
1227
1383
  return OptimizationGrid(
1228
1384
  _grid_dataset=grid_dataset,
1229
- spend=spend,
1385
+ historical_spend=historical_spend,
1230
1386
  use_kpi=use_kpi,
1231
1387
  use_posterior=use_posterior,
1232
1388
  use_optimal_frequency=use_optimal_frequency,
@@ -1810,95 +1966,6 @@ class BudgetOptimizer:
1810
1966
  )
1811
1967
  return (spend_grid, incremental_outcome_grid)
1812
1968
 
1813
- def _grid_search(
1814
- self,
1815
- spend_grid: np.ndarray,
1816
- incremental_outcome_grid: np.ndarray,
1817
- budget: float,
1818
- fixed_budget: bool,
1819
- target_mroi: float | None = None,
1820
- target_roi: float | None = None,
1821
- ) -> np.ndarray:
1822
- """Hill-climbing search algorithm for budget optimization.
1823
-
1824
- Args:
1825
- spend_grid: Discrete grid with dimensions (`grid_length` x
1826
- `n_total_channels`) containing spend by channel for all media and RF
1827
- channels, used in the hill-climbing search algorithm.
1828
- incremental_outcome_grid: Discrete grid with dimensions (`grid_length` x
1829
- `n_total_channels`) containing incremental outcome by channel for all
1830
- media and RF channels, used in the hill-climbing search algorithm.
1831
- budget: Integer indicating the total budget.
1832
- fixed_budget: Bool indicating whether it's a fixed budget optimization or
1833
- flexible budget optimization.
1834
- target_mroi: Optional float indicating the target marginal return on
1835
- investment (mroi) constraint. This can be translated into "How much can
1836
- I spend when I have flexible budget until the mroi of each channel hits
1837
- the target mroi". It's still possible that the mroi of some channels
1838
- will not be equal to the target mroi due to the feasible range of media
1839
- spend. However, the mroi will effectively shrink toward the target mroi.
1840
- target_roi: Optional float indicating the target return on investment
1841
- (roi) constraint. This can be translated into "How much can I spend when
1842
- I have a flexible budget until the roi of total media spend hits the
1843
- target roi".
1844
-
1845
- Returns:
1846
- optimal_spend: np.ndarry of dimension (`n_total_channels`) containing the
1847
- media spend that maximizes incremental outcome based on spend
1848
- constraints for all media and RF channels.
1849
- optimal_inc_outcome: np.ndarry of dimension (`n_total_channels`)
1850
- containing the post optimization incremental outcome per channel for all
1851
- media and RF channels.
1852
- """
1853
- spend = spend_grid[0, :].copy()
1854
- incremental_outcome = incremental_outcome_grid[0, :].copy()
1855
- spend_grid = spend_grid[1:, :]
1856
- incremental_outcome_grid = incremental_outcome_grid[1:, :]
1857
- iterative_roi_grid = np.round(
1858
- tf.math.divide_no_nan(
1859
- incremental_outcome_grid - incremental_outcome, spend_grid - spend
1860
- ),
1861
- decimals=8,
1862
- )
1863
- while True:
1864
- spend_optimal = spend.astype(int)
1865
- # If none of the exit criteria are met roi_grid will eventually be filled
1866
- # with all nans.
1867
- if np.isnan(iterative_roi_grid).all():
1868
- break
1869
- point = np.unravel_index(
1870
- np.nanargmax(iterative_roi_grid), iterative_roi_grid.shape
1871
- )
1872
- row_idx = point[0]
1873
- media_idx = point[1]
1874
- spend[media_idx] = spend_grid[row_idx, media_idx]
1875
- incremental_outcome[media_idx] = incremental_outcome_grid[
1876
- row_idx, media_idx
1877
- ]
1878
- roi_grid_point = iterative_roi_grid[row_idx, media_idx]
1879
- if _exceeds_optimization_constraints(
1880
- fixed_budget,
1881
- budget,
1882
- spend,
1883
- incremental_outcome,
1884
- roi_grid_point,
1885
- target_mroi,
1886
- target_roi,
1887
- ):
1888
- break
1889
-
1890
- iterative_roi_grid[0 : row_idx + 1, media_idx] = np.nan
1891
- iterative_roi_grid[row_idx + 1 :, media_idx] = np.round(
1892
- tf.math.divide_no_nan(
1893
- incremental_outcome_grid[row_idx + 1 :, media_idx]
1894
- - incremental_outcome_grid[row_idx, media_idx],
1895
- spend_grid[row_idx + 1 :, media_idx]
1896
- - spend_grid[row_idx, media_idx],
1897
- ),
1898
- decimals=8,
1899
- )
1900
- return spend_optimal
1901
-
1902
1969
 
1903
1970
  def _validate_budget(
1904
1971
  fixed_budget: bool,
@@ -1958,13 +2025,10 @@ def _get_round_factor(budget: float, gtol: float) -> int:
1958
2025
 
1959
2026
 
1960
2027
  def _exceeds_optimization_constraints(
1961
- fixed_budget: bool,
1962
- budget: float,
1963
2028
  spend: np.ndarray,
1964
2029
  incremental_outcome: np.ndarray,
1965
2030
  roi_grid_point: float,
1966
- target_mroi: float | None = None,
1967
- target_roi: float | None = None,
2031
+ scenario: FixedBudgetScenario | FlexibleBudgetScenario,
1968
2032
  ) -> bool:
1969
2033
  """Checks optimization scenario constraints.
1970
2034
 
@@ -1972,36 +2036,30 @@ def _exceeds_optimization_constraints(
1972
2036
  flexibility, target_roi, and target_mroi.
1973
2037
 
1974
2038
  Args:
1975
- fixed_budget: bool indicating whether it's a fixed budget optimization or
1976
- flexible budget optimization.
1977
- budget: integer indicating the total budget.
1978
2039
  spend: np.ndarray with dimensions (`n_total_channels`) containing spend per
1979
2040
  channel for all media and RF channels.
1980
2041
  incremental_outcome: np.ndarray with dimensions (`n_total_channels`)
1981
2042
  containing incremental outcome per channel for all media and RF channels.
1982
2043
  roi_grid_point: float roi for non-optimized optimation step.
1983
- target_mroi: Optional float indicating the target marginal return on
1984
- investment (mroi) constraint. This can be translated into "How much can I
1985
- spend when I have flexible budget until the mroi of each channel hits the
1986
- target mroi". It's still possible that the mroi of some channels will not
1987
- be equal to the target mroi due to the feasible range of spend. However,
1988
- the mroi will effectively shrink toward the target mroi.
1989
- target_roi: Optional float indicating the target return on investment (roi)
1990
- constraint. This can be translated into "How much can I spend when I have
1991
- a flexible budget until the roi of total spend hits the target roi.
2044
+ scenario: FixedBudgetScenario or FlexibleBudgetScenario.
1992
2045
 
1993
2046
  Returns:
1994
2047
  bool indicating whether optimal spend and incremental outcome have been
1995
2048
  found, given the optimization constraints.
1996
2049
  """
1997
- if fixed_budget:
1998
- return np.sum(spend) > budget
1999
- elif target_roi is not None:
2050
+ if isinstance(scenario, FixedBudgetScenario):
2051
+ # total_budget is guaranteed to be not None.
2052
+ return np.sum(spend) > scenario.total_budget
2053
+ elif (
2054
+ isinstance(scenario, FlexibleBudgetScenario)
2055
+ and scenario.target_metric == c.ROI
2056
+ ):
2000
2057
  cur_total_roi = np.sum(incremental_outcome) / np.sum(spend)
2001
2058
  # In addition to the total roi being less than the target roi, the roi of
2002
2059
  # the current optimization step should also be less than the total roi.
2003
2060
  # Without the second condition, the optimization algorithm may not have
2004
2061
  # found the roi point close to the target roi yet.
2005
- return cur_total_roi < target_roi and roi_grid_point < cur_total_roi
2062
+ target_value = scenario.target_value
2063
+ return cur_total_roi < target_value and roi_grid_point < cur_total_roi
2006
2064
  else:
2007
- return roi_grid_point < target_mroi
2065
+ return roi_grid_point < scenario.target_value
@@ -85,7 +85,7 @@ class Summarizer:
85
85
  filepath: The path to the directory where the file will be saved.
86
86
  start_date: Optional start date selector, *inclusive*, in _yyyy-mm-dd_
87
87
  format.
88
- end_date: Optional end date selector, *exclusive* in _yyyy-mm-dd_ format.
88
+ end_date: Optional end date selector, *inclusive* in _yyyy-mm-dd_ format.
89
89
  """
90
90
  os.makedirs(filepath, exist_ok=True)
91
91
  with open(os.path.join(filepath, filename), 'w') as f:
@@ -128,8 +128,12 @@ class Summarizer:
128
128
  template_env.globals[c.START_DATE] = start_date.strftime(
129
129
  f'%b {start_date.day}, %Y'
130
130
  )
131
- template_env.globals[c.END_DATE] = end_date.strftime(
132
- f'%b {end_date.day}, %Y'
131
+
132
+ interval_days = self._meridian.input_data.time_coordinates.interval_days
133
+ end_date_adjusted = end_date + pd.Timedelta(days=interval_days)
134
+
135
+ template_env.globals[c.END_DATE] = end_date_adjusted.strftime(
136
+ f'%b {end_date_adjusted.day}, %Y'
133
137
  )
134
138
 
135
139
  html_template = template_env.get_template('summary.html.jinja')
@@ -1813,6 +1813,32 @@ MROI_MEDIA_AND_RF_USE_POSTERIOR_BY_REACH = np.array([
1813
1813
  [0.2282, 0.3271, 0.1354, 1.8194, 0.5598],
1814
1814
  ],
1815
1815
  ])
1816
+ MROI_MEDIA_AND_RF_NEW_TIMES_DATA = np.array([
1817
+ [
1818
+ [1.4194793, 1.4562954, 0.42110616, 0.46693847, 2.224705],
1819
+ [1.4079778, 1.4408227, 0.42190424, 0.46729392, 2.2684476],
1820
+ [1.4133729, 1.455379, 0.42965493, 0.46928722, 2.2630167],
1821
+ [1.3786758, 1.4669218, 0.4399548, 0.466466, 2.2499163],
1822
+ [1.3667594, 1.4595027, 0.430115, 0.46791035, 2.2718313],
1823
+ [1.3636469, 1.4838042, 0.43774834, 0.46620503, 2.228661],
1824
+ [1.3479362, 1.5108197, 0.42130333, 0.46699694, 2.2348554],
1825
+ [1.3684787, 1.5252702, 0.4246083, 0.4682973, 2.2124028],
1826
+ [1.3500556, 1.5139565, 0.42371163, 0.46387878, 2.2052019],
1827
+ [1.3509449, 1.5152782, 0.42376328, 0.46421173, 2.2056532],
1828
+ ],
1829
+ [
1830
+ [0.19265468, 0.3131754, 0.11835674, 1.7264867, 0.45867893],
1831
+ [0.19271582, 0.31310934, 0.11833327, 1.7265227, 0.45878735],
1832
+ [0.1927399, 0.3133164, 0.11834618, 1.7260367, 0.45711297],
1833
+ [0.19282141, 0.31354108, 0.11858677, 1.724129, 0.45773327],
1834
+ [0.19184875, 0.31352347, 0.11844476, 1.724147, 0.4579198],
1835
+ [0.19213778, 0.3136204, 0.11846119, 1.7245249, 0.4575424],
1836
+ [0.19298446, 0.3144178, 0.11840369, 1.7243268, 0.45999327],
1837
+ [0.19473709, 0.3146425, 0.1182382, 1.7254068, 0.46479526],
1838
+ [0.19482231, 0.31446627, 0.11806685, 1.7242908, 0.46568453],
1839
+ [0.19479823, 0.31506982, 0.11783329, 1.7254248, 0.46606192],
1840
+ ],
1841
+ ])
1816
1842
  MROI_MEDIA_ONLY_USE_PRIOR = np.array([[
1817
1843
  [1.0740, 1.3019, 0.7984],
1818
1844
  [0.8990, 0.4201, 0.7120],
@@ -1999,6 +2025,32 @@ SAMPLE_ROI_NEW_DATA = np.array([
1999
2025
  [4.11828271, 1.58598067],
2000
2026
  ],
2001
2027
  ])
2028
+ ROI_NEW_TIMES_DATA = np.array([
2029
+ [
2030
+ [1.6587073, 1.6422542, 0.5339541, 0.46695647, 2.2247107],
2031
+ [1.6448834, 1.6238545, 0.5349834, 0.46725115, 2.268456],
2032
+ [1.6512104, 1.6399834, 0.54364294, 0.46939594, 2.2630484],
2033
+ [1.6124083, 1.6505513, 0.55594736, 0.46645072, 2.2499137],
2034
+ [1.597995, 1.6415955, 0.5437353, 0.46789172, 2.2718024],
2035
+ [1.5946435, 1.6684949, 0.5538136, 0.46621037, 2.2286298],
2036
+ [1.5757265, 1.6986614, 0.5317232, 0.4669749, 2.2348094],
2037
+ [1.5993005, 1.7154359, 0.535261, 0.4682574, 2.2124054],
2038
+ [1.5770503, 1.7027655, 0.5336275, 0.4639002, 2.2051802],
2039
+ [1.5781163, 1.704347, 0.53381217, 0.4641966, 2.2056897],
2040
+ ],
2041
+ [
2042
+ [0.22893494, 0.54465544, 0.17262405, 1.726445, 0.4586947],
2043
+ [0.22897178, 0.54465, 0.17261527, 1.7264631, 0.45878458],
2044
+ [0.22901268, 0.5448365, 0.17263621, 1.7260604, 0.45709604],
2045
+ [0.22900137, 0.54496145, 0.17280822, 1.7242719, 0.4577607],
2046
+ [0.22795935, 0.54490256, 0.17272605, 1.7241275, 0.4579781],
2047
+ [0.22818668, 0.5450325, 0.1726518, 1.7245975, 0.45759043],
2048
+ [0.22910665, 0.5459215, 0.17257686, 1.7243998, 0.4600002],
2049
+ [0.23114449, 0.54561883, 0.17233835, 1.7253716, 0.46480042],
2050
+ [0.23121877, 0.5451343, 0.17214388, 1.7242151, 0.46571606],
2051
+ [0.23116706, 0.54571074, 0.17181544, 1.7254194, 0.4660912],
2052
+ ],
2053
+ ])
2002
2054
  SAMPLE_ROI_KPI = np.array([
2003
2055
  [
2004
2056
  [0.4906, 0.3487],
@@ -2073,34 +2125,34 @@ SAMPLE_MROI = np.array([
2073
2125
  ])
2074
2126
  SAMPLE_MROI_NEW_DATA = np.array([
2075
2127
  [
2076
- [0.83164197, 0.86531621],
2077
- [0.52520442, 0.8496893],
2078
- [0.32174, 0.221467],
2079
- [2.23112156, 1.5499284],
2128
+ [0.86685735, 0.9019572],
2129
+ [0.5474438, 0.88566875],
2130
+ [0.33536345, 0.23084486],
2131
+ [2.325597, 1.615559],
2080
2132
  ],
2081
2133
  [
2082
- [1.11224782, 1.26411092],
2083
- [0.89164674, 1.23331833],
2084
- [0.11121775, 0.34618538],
2085
- [2.46114173, 2.2305032],
2134
+ [0.98578817, 1.1203848],
2135
+ [0.79026884, 1.0930933],
2136
+ [0.09857258, 0.306825],
2137
+ [2.1813164, 1.9769008],
2086
2138
  ],
2087
2139
  [
2088
- [1.26143491, 0.32841554],
2089
- [0.7384, 0.32490036],
2090
- [0.48684751, 0.13766211],
2091
- [3.28485963, 0.52965774],
2140
+ [1.1169803, 0.29080662],
2141
+ [0.65436214, 0.28769404],
2142
+ [0.4310956, 0.12189759],
2143
+ [2.9086902, 0.46900335],
2092
2144
  ],
2093
2145
  [
2094
- [0.9377, 0.1185],
2095
- [0.6223, 0.1190],
2096
- [0.2329, 0.0957],
2097
- [2.4082, 0.1392],
2146
+ [0.9376944, 0.11894181],
2147
+ [0.6223348, 0.11948171],
2148
+ [0.23284341, 0.09595118],
2149
+ [2.408213, 0.13983254],
2098
2150
  ],
2099
2151
  [
2100
- [0.5017, 0.1535],
2101
- [0.3321, 0.1530],
2102
- [0.2055, 0.1419],
2103
- [1.0451, 0.1663],
2152
+ [0.501675, 0.15347004],
2153
+ [0.33212814, 0.15300572],
2154
+ [0.20545325, 0.14246973],
2155
+ [1.0451934, 0.16632313],
2104
2156
  ],
2105
2157
  [
2106
2158
  # mROI metric don't make sense for "All Channels".