optimizador_de_presupuesto#
Módulo de optimización de presupuesto.
Resumen#
Optimizar la forma de asignar un presupuesto total entre canales (y dimensiones adicionales opcionales) para maximizar una respuesta esperada derivada de un posterior de MMM ajustado.
Inicio rápido (MMM multidimensional)#
import numpy as np
import pandas as pd
import xarray as xr
from pymc_marketing.mmm import GeometricAdstock, LogisticSaturation
from pymc_marketing.mmm.multidimensional import (
MMM,
MultiDimensionalBudgetOptimizerWrapper,
)
# 1) Fit a model (toy example)
X = pd.DataFrame(
{
"date": pd.date_range("2025-01-01", periods=30, freq="W-MON"),
"geo": np.random.choice(["A", "B"], size=30),
"C1": np.random.rand(30),
"C2": np.random.rand(30),
}
)
y = pd.Series(np.random.rand(30), name="y")
mmm = MMM(
date_column="date",
dims=("geo",),
channel_columns=["C1", "C2"],
target_column="y",
adstock=GeometricAdstock(l_max=4),
saturation=LogisticSaturation(),
)
mmm.fit(X, y)
# 2) Wrap the fitted model for allocation over a future window
wrapper = MultiDimensionalBudgetOptimizerWrapper(
model=mmm,
start_date=X["date"].max() + pd.Timedelta(weeks=1),
end_date=X["date"].max() + pd.Timedelta(weeks=8),
)
# Optional: choose which (channel, geo) cells to optimize
budgets_to_optimize = xr.DataArray(
np.array([[True, False], [True, True]]),
dims=["channel", "geo"],
coords={"channel": ["C1", "C2"], "geo": ["A", "B"]},
)
# Optional: distribute each cell's budget over the time window (must sum to 1 along date)
dates = pd.date_range(wrapper.start_date, wrapper.end_date, freq="W-MON")
factors = xr.DataArray(
np.vstack(
[
np.full(len(dates), 1 / len(dates)), # C1: uniform
np.linspace(0.7, 0.3, len(dates)), # C2: front‑to‑back taper
]
),
dims=["channel", "date"],
coords={"channel": ["C1", "C2"], "date": np.arange(len(dates))},
)
# 3) Optimize
optimal, res = wrapper.optimize_budget(
budget=100.0,
budgets_to_optimize=budgets_to_optimize,
budget_distribution_over_period=factors,
response_variable="total_media_contribution_original_scale",
)
# `optimal` is an xr.DataArray with dims (channel, geo)
Using cost_per_unit (non-monetary channels)#
When channels are measured in non-monetary units (impressions, clicks, GRPs),
pass cost_per_unit so the optimizer converts dollar budgets into the
model’s native units internally. All user-facing inputs and outputs remain
in monetary units.
# cost_per_unit DataFrame (xr.DataArray) for the optimisation window.
# Rows = dates in the future window; columns = channels with $/unit rates.
# Channels absent from the DataFrame default to 1.0 (already in spend units).
cpu_df = pd.DataFrame(
{
"date": pd.date_range("2025-03-03", periods=8, freq="W-MON"),
"C1": [0.05] * 8, # $0.05 per impression
"C2": [1.20] * 8, # $1.20 per click
}
)
optimal, res = wrapper.optimize_budget(
budget=100.0,
cost_per_unit=cpu_df,
)
# `optimal` budgets are in dollars. The optimizer divided by
# cost_per_unit internally before feeding into the model.
Utilice un modelo pymc personalizado con cualquier dimensionalidad.#
import numpy as np
import pandas as pd
import pymc as pm
import xarray as xr
from pymc.model.fgraph import clone_model
from pymc_marketing.mmm.budget_optimizer import (
BudgetOptimizer,
optimizer_xarray_builder,
)
# 1) Build and fit any PyMC model that exposes:
# - a variable named 'channel_data' with dims ("date", "channel", ...)
# - a deterministic named 'total_contribution' with dim "date"
# - optionally a deterministic named 'channel_contribution' with dims ("date", "channel", ...)
# so the optimizer can auto-detect optimizable cells; otherwise pass budgets_to_optimize.
rng = np.random.default_rng(0)
dates = pd.date_range("2025-01-01", periods=30, freq="W-MON")
channels = ["C1", "C2", "C3"]
X = rng.uniform(0.0, 1.0, size=(len(dates), len(channels)))
true_beta = np.array([0.8, 0.4, 0.2])
y = (X @ true_beta) + rng.normal(0.0, 0.1, size=len(dates))
coords = {"date": dates, "channel": channels}
with pm.Model(coords=coords) as train_model:
pm.Data("channel_data", X, dims=("date", "channel"))
beta = pm.Normal("beta", 0.0, 1.0, dims="channel")
channel_contrib = train_model["channel_data"] * beta
mu = channel_contrib.sum(axis=-1) # sum over channel axis
# Per-period contribution
pm.Deterministic("total_contribution_per_period", mu, dims="date")
# For optimization: sum over all dimensions to get a scalar
pm.Deterministic("total_contribution", mu.sum(), dims=())
pm.Deterministic(
"channel_contribution",
channel_contrib,
dims=("date", "channel"),
)
sigma = pm.HalfNormal("sigma", 0.2)
pm.Normal("y", mu=mu, sigma=sigma, observed=y, dims="date")
idata = pm.sample(100, tune=100, chains=2, random_seed=1)
# 2) Create a minimal wrapper satisfying OptimizerCompatibleModelWrapper
wrapper = CustomModelWrapper(base_model=train_model, idata=idata, channels=channels)
# 3) Optimize N future periods with optional bounds and/or masks
optimizer = BudgetOptimizer(model=wrapper, num_periods=8)
# Optional: bounds per channel (single budget dim, using dict)
bounds = {"C1": (0.0, 50.0), "C2": (0.0, 40.0), "C3": (0.0, 60.0)}
# Or as an xarray when you have multiple budget dims, e.g. (channel, geo):
# bounds = optimizer_xarray_builder(
# value=np.array([[0.0, 50.0], [0.0, 40.0], [0.0, 60.0]]),
# channel=channels,
# bound=["lower", "upper"],
# )
allocation, result = optimizer.allocate_budget(
total_budget=100.0, budget_bounds=bounds
)
# allocation is an xr.DataArray with dims inferred from your model's channel_data dims (excluding date)
Requisitos#
El optimizador funciona con cualquier envoltura que satisfaga
OptimizerCompatibleModelWrapper: - Atributos:adstock,_channel_scales,idata(arviz.InferenceData con posterior) - Método:_set_predictors_for_optimization(num_periods) -> pm.Modelque devuelve un PyMCmodelo donde existe una variable llamada
channel_datacon dimensiones que incluyen"date"y todas las dimensiones de presupuesto (por ejemplo,("channel", "geo")). El optimizador reemplazachannel_datacon la variable de optimización internamente.Posterior debe contener una variable de respuesta (predeterminada:
"total_contribution") o cualquierresponse_variablepersonalizada que proporciones, y los determinísticos MMM requeridos (por ejemplo,channel_contribution).Para la distribución del tiempo: pase un DataArray con dimensiones
("fecha", *dimensiones_presupuesto)y valores a lo largo defechaque sumen 1 para cada celda de presupuesto.Los límites pueden ser un diccionario solo para presupuestos unidimensionales; de lo contrario, utilice un xarray.DataArray (utilice
optimizer_xarray_builder(...)).
Notas#
Si
budgets_to_optimizeno se proporciona, el optimizador detecta automáticamente las celdas con información histórica utilizandoidata.posterior.channel_contribution.mean(("chain","draw","date")).astype(bool).Los límites predeterminados son
[0, total_budget]en cada celda optimizada.Establezca
callback=Trueenallocate_budget(...)para recibir diagnósticos por iteración (objetivo, gradiente, restricciones) para su monitoreo.
Funciones
|
Cree un xarray.DataArray con dimensiones y coordenadas flexibles. |
Clases
|
Una clase para optimizar la asignación de presupuesto en un modelo de mezcla de marketing. |
|
Fusionar múltiples modelos compatibles con el optimizador en un solo modelo. |
|
Envoltorio para el BudgetOptimizer para manejar modelos PyMC personalizados. |
|
Protocolo para envolturas del modelo de mezcla de marketing compatibles con el BudgetOptimizer. |
Excepciones
|
Excepción personalizada para fallo de optimización. |