google-meridian 1.4.0__py3-none-any.whl → 1.5.1__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.
- {google_meridian-1.4.0.dist-info → google_meridian-1.5.1.dist-info}/METADATA +14 -11
- {google_meridian-1.4.0.dist-info → google_meridian-1.5.1.dist-info}/RECORD +50 -46
- {google_meridian-1.4.0.dist-info → google_meridian-1.5.1.dist-info}/WHEEL +1 -1
- meridian/analysis/analyzer.py +558 -398
- meridian/analysis/optimizer.py +90 -68
- meridian/analysis/review/checks.py +118 -116
- meridian/analysis/review/constants.py +3 -3
- meridian/analysis/review/results.py +131 -68
- meridian/analysis/review/reviewer.py +8 -23
- meridian/analysis/summarizer.py +6 -1
- meridian/analysis/test_utils.py +2898 -2538
- meridian/analysis/visualizer.py +28 -9
- meridian/backend/__init__.py +106 -0
- meridian/constants.py +1 -0
- meridian/data/input_data.py +30 -52
- meridian/data/input_data_builder.py +2 -9
- meridian/data/test_utils.py +25 -41
- meridian/data/validator.py +48 -0
- meridian/mlflow/autolog.py +19 -9
- meridian/model/adstock_hill.py +3 -5
- meridian/model/context.py +134 -0
- meridian/model/eda/constants.py +334 -4
- meridian/model/eda/eda_engine.py +724 -312
- meridian/model/eda/eda_outcome.py +177 -33
- meridian/model/model.py +159 -110
- meridian/model/model_test_data.py +38 -0
- meridian/model/posterior_sampler.py +103 -62
- meridian/model/prior_sampler.py +114 -94
- meridian/model/spec.py +23 -14
- meridian/templates/card.html.jinja +9 -7
- meridian/templates/chart.html.jinja +1 -6
- meridian/templates/finding.html.jinja +19 -0
- meridian/templates/findings.html.jinja +33 -0
- meridian/templates/formatter.py +41 -5
- meridian/templates/formatter_test.py +127 -0
- meridian/templates/style.css +66 -9
- meridian/templates/style.scss +85 -4
- meridian/templates/table.html.jinja +1 -0
- meridian/version.py +1 -1
- scenarioplanner/linkingapi/constants.py +1 -1
- scenarioplanner/mmm_ui_proto_generator.py +1 -0
- schema/processors/marketing_processor.py +11 -10
- schema/processors/model_processor.py +4 -1
- schema/serde/distribution.py +12 -7
- schema/serde/hyperparameters.py +54 -107
- schema/serde/meridian_serde.py +12 -3
- schema/utils/__init__.py +1 -0
- schema/utils/proto_enum_converter.py +127 -0
- {google_meridian-1.4.0.dist-info → google_meridian-1.5.1.dist-info}/licenses/LICENSE +0 -0
- {google_meridian-1.4.0.dist-info → google_meridian-1.5.1.dist-info}/top_level.txt +0 -0
meridian/analysis/optimizer.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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__(
|
|
1329
|
+
def __init__(
|
|
1330
|
+
self,
|
|
1331
|
+
meridian: model.Meridian,
|
|
1332
|
+
):
|
|
1327
1333
|
self._meridian = meridian
|
|
1328
|
-
self._analyzer = analyzer_module.Analyzer(
|
|
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.
|
|
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(
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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.
|
|
1917
|
-
include_rf=self.
|
|
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
|
-
|
|
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,
|
|
2094
|
+
required_tensors_names=required_tensors,
|
|
2095
|
+
model_context=model_context,
|
|
2080
2096
|
)
|
|
2081
2097
|
selected_times = _expand_selected_times(
|
|
2082
|
-
|
|
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=
|
|
2092
|
-
include_rf=
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
2269
|
+
required_tensors_names=c.PAID_CHANNELS,
|
|
2270
|
+
model_context=model_context,
|
|
2252
2271
|
)
|
|
2253
|
-
if
|
|
2272
|
+
if model_context.n_media_channels > 0:
|
|
2254
2273
|
new_media = (
|
|
2255
2274
|
backend.divide_no_nan(
|
|
2256
|
-
spend[:
|
|
2257
|
-
hist_spend[:
|
|
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
|
|
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[-
|
|
2268
|
-
hist_spend[-
|
|
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
|
-
|
|
2324
|
+
required_tensors_names=c.PAID_DATA + (c.TIME,),
|
|
2325
|
+
model_context=model_context,
|
|
2306
2326
|
)
|
|
2307
2327
|
selected_times = _expand_selected_times(
|
|
2308
|
-
|
|
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
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
2410
|
-
backend.
|
|
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
|
-
|
|
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:
|
|
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,
|
|
2536
|
+
required_tensors_names=c.PAID_DATA,
|
|
2537
|
+
model_context=model_context,
|
|
2520
2538
|
)
|
|
2521
|
-
if
|
|
2539
|
+
if model_context.n_media_channels > 0:
|
|
2522
2540
|
new_media = (
|
|
2523
|
-
multipliers_grid[i, :
|
|
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
|
|
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, -
|
|
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, -
|
|
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(
|
|
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
|
|
2696
|
+
if model_context.n_rf_channels > 0:
|
|
2678
2697
|
incremental_outcome_grid = backend.stabilize_rf_roi_grid(
|
|
2679
|
-
spend_grid, incremental_outcome_grid,
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
3127
|
-
return
|
|
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
|
)
|