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.Model que devuelve un PyMC

    modelo donde existe una variable llamada channel_data con dimensiones que incluyen "date" y todas las dimensiones de presupuesto (por ejemplo, ("channel", "geo")). El optimizador reemplaza channel_data con la variable de optimización internamente.

  • Posterior debe contener una variable de respuesta (predeterminada: "total_contribution") o cualquier response_variable personalizada 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 de fecha que 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_optimize no se proporciona, el optimizador detecta automáticamente las celdas con información histórica utilizando idata.posterior.channel_contribution.mean(("chain","draw","date")).astype(bool).

  • Los límites predeterminados son [0, total_budget] en cada celda optimizada.

  • Establezca callback=True en allocate_budget(...) para recibir diagnósticos por iteración (objetivo, gradiente, restricciones) para su monitoreo.

Funciones

optimizer_xarray_builder(valor, **kwargs)

Cree un xarray.DataArray con dimensiones y coordenadas flexibles.

Clases

BudgetOptimizer(*, num_periods, ...)

Una clase para optimizar la asignación de presupuesto en un modelo de mezcla de marketing.

BuildMergedModel(modelos[, prefijos, ...])

Fusionar múltiples modelos compatibles con el optimizador en un solo modelo.

CustomModelWrapper(base_model, idata, channels)

Envoltorio para el BudgetOptimizer para manejar modelos PyMC personalizados.

OptimizerCompatibleModelWrapper(*args, **kwargs)

Protocolo para envolturas del modelo de mezcla de marketing compatibles con el BudgetOptimizer.

Excepciones

MinimizeException(mensaje)

Excepción personalizada para fallo de optimización.