Guía de inicio rápido de MMM#
Bienvenido a PyMC-Marketing. Esta biblioteca ofrece poderosas herramientas de modelado bayesiano para analítica de marketing. PyMC-Marketing está construido sobre PyMC, una biblioteca de programación probabilística que habilita la inferencia bayesiana. En esta guía rápida, recorreremos cómo ajustar un modelo básico de Mezcla de Medios (MMM) en PyMC-Marketing.
¿Qué es el Modelado de Mezcla de Medios?#
El Modelado de Mezcla de Medios (MMM) ayuda a los especialistas en marketing a comprender cómo distintos canales de publicidad contribuyen a los resultados del negocio (como ventas o conversiones). MMM responde preguntas clave:
¿Qué canales impulsan más las ventas?
¿Cuál es el Retorno de la Inversión Publicitaria (ROAS) de cada canal?
¿Cómo debería asignar mi presupuesto de marketing?
Conceptos clave#
MMM tiene en cuenta dos fenómenos importantes en la publicidad:
Adstock (efecto de arrastre): El impacto de la publicidad no ocurre de forma instantánea; se acumula con el tiempo y se atenúa gradualmente.
Saturación: Los rendimientos disminuyen a medida que aumentas el gasto; el primer dólar gastado es más efectivo que el millonésimo.
Veamos cómo ajustar un modelo MMM básico para entender estos efectos y medir las contribuciones de los canales.
Nota
El objetivo de PyMC-Marketing es brindar herramientas para aplicaciones reales. Normalmente, necesitamos pensar en la estructura causal, la calibración de pruebas de lift y la optimización avanzada del presupuesto. Este ejemplo debe considerarse como un primer paso hacia un conjunto de herramientas más complejo y completo para impulsar decisiones de marketing por valor de millones de dólares. En nuestra galería de ejemplos encontrarás recursos extensos para ayudarte y guiarte a través del proceso iterativo de modelado MMM.
Truco
Para una versión extendida de este ejemplo, consulta Cuaderno de ejemplo para MMM. Aquí profundizamos en el proceso de generación de datos y en el diagnóstico del modelo. También incluimos la estimación del ROAS y las predicciones fuera de muestra.
Si deseas ver un análisis completo de principio a fin, consulta Caso de Estudio Completo de MMM. Aquí tomamos un conjunto de datos «real» y recorremos todo el proceso de especificación del modelo, ajuste, optimización y planificación de escenarios.
Preparar el cuaderno#
Importemos las librerías necesarias:
import arviz as az
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from pymc_extras.prior import Prior
from pymc_marketing.mmm import GeometricAdstock, LogisticSaturation
from pymc_marketing.mmm.multidimensional import MMM
az.style.use("arviz-darkgrid")
plt.rcParams["figure.figsize"] = [12, 7]
plt.rcParams["figure.dpi"] = 100
%config InlineBackend.figure_format = "retina"
%load_ext autoreload
%autoreload 2
# Set random seed for reproducibility
seed = sum(map(ord, "mmm"))
rng = np.random.default_rng(seed=seed)
Cargar datos#
Usaremos un conjunto de datos sintético que simula datos semanales de ventas junto con el gasto en dos canales de marketing (x1 y x2), además de algunas variables de control para eventos especiales.
# Load the data
url = "https://raw.githubusercontent.com/pymc-labs/pymc-marketing/main/data/mmm_example.csv"
data = pd.read_csv(url, parse_dates=["date_week"])
print(f"Data shape: {data.shape}")
data.head()
| date_week | y | x1 | x2 | event_1 | event_2 | dayofyear | t | |
|---|---|---|---|---|---|---|---|---|
| 0 | 2018-04-02 | 3984.662237 | 0.318580 | 0.0 | 0.0 | 0.0 | 92 | 0 |
| 1 | 2018-04-09 | 3762.871794 | 0.112388 | 0.0 | 0.0 | 0.0 | 99 | 1 |
| 2 | 2018-04-16 | 4466.967388 | 0.292400 | 0.0 | 0.0 | 0.0 | 106 | 2 |
| 3 | 2018-04-23 | 3864.219373 | 0.071399 | 0.0 | 0.0 | 0.0 | 113 | 3 |
| 4 | 2018-04-30 | 4441.625278 | 0.386745 | 0.0 | 0.0 | 0.0 | 120 | 4 |
Visualicemos nuestra variable objetivo (ventas) y el gasto en medios a lo largo del tiempo:
fig, axes = plt.subplots(3, 1, figsize=(12, 9), sharex=True)
# Sales
axes[0].plot(data["date_week"], data["y"], color="black", linewidth=2)
axes[0].set(ylabel="Sales", title="Target Variable: Sales")
# Channel 1
axes[1].plot(data["date_week"], data["x1"], color="C0", linewidth=2)
axes[1].set(ylabel="Spend", title="Channel x1")
# Channel 2
axes[2].plot(data["date_week"], data["x2"], color="C1", linewidth=2)
axes[2].set(xlabel="Date", ylabel="Spend", title="Channel x2");
Ingeniería de características#
Para nuestro modelo MMM, incluiremos:
Tendencia: Una tendencia lineal para capturar el crecimiento a largo plazo
Estacionalidad: Estacionalidad anual (manejada automáticamente por el modelo)
Eventos: Indicadores binarios para eventos especiales
Canales de medios: Nuestros dos canales de publicidad
# Add a simple linear trend feature
data["t"] = range(len(data))
# Split into features (X) and target (y)
X = data.drop("y", axis=1)
y = data["y"]
print(f"Features: {X.columns.tolist()}")
Features: ['date_week', 'x1', 'x2', 'event_1', 'event_2', 'dayofyear', 't']
Especificación del modelo#
Ahora configuraremos nuestro modelo MMM. Los componentes clave son:
Transformación Adstock: Usamos
GeometricAdstockcon un retraso máximo de 8 semanasTransformación de saturación: Usamos
LogisticSaturationpara capturar los rendimientos decrecientesDistribuciones a priori: Podemos personalizar las distribuciones a priori según el conocimiento del dominio
Configuración de distribuciones a priori#
Una característica poderosa del modelado bayesiano es la capacidad de incorporar conocimiento previo. Aquí hay una heurística sencilla para las distribuciones a priori de los canales basada en la participación de gasto:
# Calculate spend share for each channel
total_spend_per_channel = data[["x1", "x2"]].sum(axis=0)
spend_share = total_spend_per_channel / total_spend_per_channel.sum()
print("Spend share per channel:")
print(spend_share)
# Use spend share to inform prior on channel contributions
n_channels = 2
prior_sigma = n_channels * spend_share.to_numpy()
print(f"\nPrior sigma for channels: {prior_sigma}")
Spend share per channel:
x1 0.65632
x2 0.34368
dtype: float64
Prior sigma for channels: [1.31263903 0.68736097]
Ahora definamos la configuración de nuestro modelo:
my_model_config = {
"intercept": Prior("Normal", mu=0.5, sigma=0.2),
"saturation_beta": Prior("HalfNormal", sigma=prior_sigma, dims="channel"),
"gamma_control": Prior("Normal", mu=0, sigma=0.05, dims="control"),
"gamma_fourier": Prior("Laplace", mu=0, b=0.2, dims="fourier_mode"),
"likelihood": Prior("Normal", sigma=Prior("HalfNormal", sigma=6), dims="date"),
}
# Sampler configuration
my_sampler_config = {"progressbar": True}
# Initialize the MMM model
mmm = MMM(
model_config=my_model_config,
sampler_config=my_sampler_config,
date_column="date_week",
adstock=GeometricAdstock(l_max=8),
saturation=LogisticSaturation(),
channel_columns=["x1", "x2"],
control_columns=["event_1", "event_2", "t"],
yearly_seasonality=2,
)
mmm.build_model(X, y)
mmm.add_original_scale_contribution_variable(
[
"y",
"intercept_contribution",
"control_contribution",
"channel_contribution",
"fourier_contribution",
"yearly_seasonality_contribution",
]
)
mmm.table()
Variable Expression Dimensions ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────── channel_scale = Data channel[2] target_scale = Data channel_data = Data date[179] × channel[2] target_data = Data date[179] control_data = Data date[179] × control[3] dayofyear = Data date[179] intercept_contribution ~ Normal(0.5, 0.2) adstock_alpha ~ Beta(1, 3) channel[2] saturation_lam ~ Gamma(3, f()) channel[2] saturation_beta ~ HalfNormal(0, <constant>) channel[2] gamma_control ~ Normal(0, 0.05) control[3] gamma_fourier ~ Laplace(0, 0.2) fourier_mode[4] y_sigma ~ HalfNormal(0, 6) Parameter count = 15 channel_contribution = f(saturation_beta, saturation_lam, date[179] × channel[2] adstock_alpha) total_media_contribution_original_scale f(saturation_beta, saturation_lam, = adstock_alpha) control_contribution = f(gamma_control) date[179] × control[3] fourier_contribution = f(gamma_fourier) date[179] × fourier_mode[4] yearly_seasonality_contribution = f(gamma_fourier) date[179] y_original_scale = f(y) date[179] intercept_contribution_original_scale = f(intercept_contribution) control_contribution_original_scale = f(gamma_control) date[179] × control[3] channel_contribution_original_scale = f(saturation_beta, saturation_lam, date[179] × channel[2] adstock_alpha) fourier_contribution_original_scale = f(gamma_fourier) date[179] × fourier_mode[4] yearly_seasonality_contribution_origina… f(gamma_fourier) date[179] = y ~ Normal(f(intercept_contribution, date[179] gamma_fourier, gamma_control, saturation_beta, saturation_lam, adstock_alpha), y_sigma)
Verificación predictiva a priori#
Truco
La verificación predictiva a priori es una excelente manera de comprobar que nuestras distribuciones a priori son razonables. Por ello, se recomienda encarecidamente realizar esta verificación antes de ajustar el modelo. Si eres nuevo en el modelado bayesiano, consulta nuestro notebook de guía Modelado Predictivo Anterior.
Antes de ajustar, verifiquemos que nuestras distribuciones a priori sean razonables:
# Generate prior predictive samples
mmm.sample_prior_predictive(X, y, samples=1_000, random_seed=rng)
# Plot prior predictive distribution
fig, ax = plt.subplots(figsize=(12, 6))
ax.plot(mmm.prior.date, mmm.y, color="black", label="Observed")
ax.plot(
mmm.prior.date,
mmm.prior.y_original_scale.mean(("chain", "draw")),
color="C0",
label="Mean Prediction",
)
for hdi in (0.94, 0.5):
az.plot_hdi(
mmm.prior.date,
mmm.prior.y_original_scale,
hdi_prob=hdi,
smooth=False,
ax=ax,
color="C0",
fill_kwargs=dict(alpha=0.3 if hdi == 0.94 else 0.5, label=f"{hdi:.0%} HDI"),
)
ax.legend()
ax.set(title="Prior predictive check", xlabel="date", ylabel="y");
En general, la verificación predictiva a priori se ve bien.
Ajuste del modelo#
Ahora ajustemos el modelo a nuestros datos usando muestreo MCMC. Ten en cuenta que podemos usar diferentes muestreadores pasando el argumento nuts_sampler. Por ejemplo, podemos usar los muestreadores numpyro, nutpie o blackjax (consulta Otros muestreadores NUTS para más detalles).
# Fit the model
_ = mmm.fit(
X=X,
y=y,
chains=4,
target_accept=0.85,
random_seed=rng,
)
Diagnóstico del modelo#
Después del ajuste, debemos verificar la calidad del modelo. Comencemos con las divergencias:
# Check for divergences
n_divergences = mmm.idata["sample_stats"]["diverging"].sum().item()
print(f"Number of divergences: {n_divergences}")
if n_divergences == 0:
print("✓ No divergences - sampling was successful!")
else:
print("⚠ Warning: Model had divergences. Consider increasing target_accept.")
Number of divergences: 0
✓ No divergences - sampling was successful!
Resumen de parámetros#
Examinemos los parámetros estimados:
# Plot traces for key parameters
_ = az.plot_trace(
data=mmm.fit_result,
var_names=[
"saturation_beta",
"saturation_lam",
"adstock_alpha",
],
compact=True,
backend_kwargs={"figsize": (10, 6), "layout": "constrained"},
)
plt.gcf().suptitle("Trace Plots", fontsize=16);
Los gráficos de traza adecuados deben mostrar:
Lado izquierdo: Distribuciones suaves con forma de campana
Lado derecho: patrones de «oruga difusa» (buen mezclado) sin tendencias
Verificación predictiva posterior#
¿Qué tan bien se ajusta nuestro modelo a los datos observados?
# Sample from posterior predictive distribution
mmm.sample_posterior_predictive(X, extend_idata=True, combined=True)
# Plot model fit
fig, ax = plt.subplots(figsize=(12, 6))
ax.plot(mmm.prior.date, mmm.y, color="black", label="Observed")
ax.plot(
mmm.prior.date,
mmm.posterior_predictive.y_original_scale.mean(("chain", "draw")),
color="C0",
label="Mean Prediction",
)
for hdi in (0.94, 0.5):
az.plot_hdi(
mmm.prior.date,
mmm.posterior_predictive.y_original_scale,
hdi_prob=hdi,
smooth=False,
ax=ax,
color="C0",
fill_kwargs=dict(alpha=0.3 if hdi == 0.94 else 0.5, label=f"{hdi:.0%} HDI"),
)
ax.legend()
ax.set(title="Prior predictive check", xlabel="date", ylabel="y");
El modelo captura bien los datos observados si los puntos negros (ventas reales) caen dentro de las bandas de incertidumbre sombreadas.
Análisis de contribución#
Ahora viene la parte divertida: ¡entender cuánto contribuye cada componente a las ventas!
Contribuciones de componentes a lo largo del tiempo#
Visualicemos la contribución de cada componente del modelo a lo largo del tiempo:
date = mmm.model.coords["date"]
post = mmm.idata.posterior
fig, ax = plt.subplots()
sns.lineplot(data=data, x="date_week", y="y", color="black", label="Sales", ax=ax)
for i, hdi_prob in enumerate([0.94, 0.5]):
hdi_kwargs = {
"x": date,
"smooth": False,
"hdi_prob": hdi_prob,
"ax": ax,
}
alpha = 0.3 + i * 0.1
label = f"{hdi_prob:.0%} HDI {0}"
az.plot_hdi(
y=post["channel_contribution_original_scale"].sum(dim="channel"),
color="C0",
fill_kwargs={"alpha": alpha, "label": label.format("Channels Contribution")},
**hdi_kwargs,
)
az.plot_hdi(
y=post["control_contribution_original_scale"].sum(dim="control"),
color="C1",
fill_kwargs={"alpha": alpha, "label": label.format("Control")},
**hdi_kwargs,
)
az.plot_hdi(
y=post["yearly_seasonality_contribution_original_scale"],
color="C2",
fill_kwargs={"alpha": alpha, "label": label.format("Fourier")},
**hdi_kwargs,
)
az.plot_hdi(
y=post["intercept_contribution_original_scale"].expand_dims(
{"date": date}, axis=-1
),
color="C3",
fill_kwargs={"alpha": alpha, "label": label.format("Intercept")},
**hdi_kwargs,
)
ax.legend(
loc="upper center",
bbox_to_anchor=(0.5, -0.1),
ncol=3,
)
fig.suptitle(
"Posterior Predictive - Channel Contributions",
fontsize=18,
fontweight="bold",
y=1.03,
);
Observamos que hemos capturado la tendencia lineal, las contribuciones de los eventos y las estacionalidades en los datos. La variación restante se debe a los canales de medios, que es exactamente lo que queremos entender.
Gráfico de cascada: Contribución total por componente#
Un gráfico de cascada muestra la contribución total de cada componente en todo el período de tiempo:
Este gráfico responde a la pregunta: «¿Cuánto contribuyó cada componente a las ventas totales?»
Curvas de contribución directa#
Estas curvas muestran la relación entre gasto y contribución, teniendo en cuenta la saturación:
# Plot direct contribution curves (saturation curves)
fig = mmm.plot.saturation_scatterplot(original_scale=True)
plt.suptitle("Direct Contribution Curves", fontsize=16, y=1.02);
Observa cómo las curvas se aplanan a niveles de gasto más altos: ¡este es el efecto de saturación en acción!
Cuadrícula de contribución por canal#
Una vista complementaria del rendimiento de los medios consiste en evaluar la contribución por canal en diferentes niveles de participación de gasto durante todo el período de entrenamiento. Concretamente, si denotamos por \(\delta\) el nivel porcentual del gasto de entrada del canal, de modo que para \(\delta = 1\) tenemos los datos de gasto de entrada del modelo y para \(\delta = 1.5\) tenemos un aumento del 50% en el gasto, entonces podemos calcular la contribución por canal en una cuadrícula de valores de \(\delta\) y graficar los resultados:
mmm.sensitivity.run_sweep(
sweep_values=np.linspace(0, 1.5, 12),
var_input="channel_data",
var_names="channel_contribution_original_scale",
extend_idata=True,
)
ax = mmm.plot.sensitivity_analysis(
hue_dim="channel",
x_sweep_axis="absolute",
xlabel="input",
ylabel="contribution",
subplot_title_fallback="Channel contribution as a function of cost share",
plot_kwargs={"marker": "o"},
)
ax.axvline(
mmm.X["x1"].sum(),
color="C0",
linestyle="--",
linewidth=2,
label="channel=x1 (realized)",
)
ax.axvline(
mmm.X["x2"].sum(),
color="C1",
linestyle="--",
linewidth=2,
label="channel=x1 (realized)",
);
mmm.X["x1"].sum()
Aquí también podemos ver el efecto de saturación y la contribución relativa de cada canal en función del nivel de participación de gasto en el período de tiempo agregado.
Próximos pasos#
¡Felicidades! Has ajustado con éxito tu primer modelo MMM con PyMC-Marketing. 🎉
Para continuar tu recorrido, revisa:
Una guía completa que incluye generación de datos, cálculo de ROAS y diagnósticos más avanzados.
Asignación de Presupuesto con PyMC-Marketing: Learn how to optimize your marketing budget allocation
Incorporar resultados experimentales para mejorar la precisión del modelo
MMM con parámetros que varían en el tiempo (TVP): Model changing baseline effects over time
MMM con una línea base de medios variable en el tiempo: Model changing media efficiency over time
Características MMM de PyMC-Marketing#
PyMC-Marketing ofrece un conjunto completo de herramientas para el Modelado de Mezcla de Medios:
Característica |
Descripción |
|---|---|
Distribuciones a priori y verosimilitudes personalizadas |
Adapta tu modelo a tus necesidades específicas de negocio incluyendo conocimiento del dominio mediante distribuciones a priori |
Transformación Adstock |
Optimiza los efectos de arrastre en tus canales de marketing |
Efectos de saturación |
Comprende los rendimientos decrecientes en las inversiones en medios |
Personaliza las funciones de adstock y saturación |
Elige entre una variedad de funciones de adstock y saturación, o implementa tus propias funciones personalizadas |
Intercepto variable en el tiempo |
Captura las contribuciones de línea base variables en el tiempo utilizando métodos modernos y eficientes de aproximación con procesos Gaussianos |
Contribución de medios variable en el tiempo |
Captura la eficiencia de los medios variable en el tiempo en tu modelo |
Visualización y diagnóstico del modelo |
Obtén una visión completa del rendimiento y los insights de tu modelo |
Identificación causal |
Introduce un grafo acíclico dirigido basado en objetivos de negocio para identificar variables significativas para conclusiones causales |
Múltiples algoritmos de inferencia |
Elige entre varios muestreadores NUTS (p. ej., BlackJax, NumPyro y Nutpie) |
Compatibilidad con GPU |
Los múltiples backends de PyMC permiten aceleración por GPU |
Predicciones fuera de muestra |
Pronostica el rendimiento futuro de marketing con intervalos creíbles para simulaciones y planificación de escenarios |
Optimización de presupuesto |
Asigna tu gasto de marketing de manera eficiente entre varios canales para maximizar el ROI |
Calibración de experimentos |
Ajusta tu modelo basándote en experimentos empíricos (pruebas de lift) para una visión de marketing más unificada |
Referencias#
Jin, Y., Wang, Y., Sun, Y., Chan, D. y Koehler, J. (2017). Métodos bayesianos para el Modelado de Mezcla de Medios con efectos de arrastre y de forma.
Juan Orduz, Estimación de efectos de medios con PyMC: Adstock, Saturación y Rendimientos decrecientes
%load_ext watermark
%watermark -n -u -v -iv -w -p pymc_marketing
Last updated: Tue, 17 Feb 2026
Python implementation: CPython
Python version : 3.13.11
IPython version : 9.9.0
pymc_marketing: 0.17.1
arviz : 0.23.1
matplotlib : 3.10.8
numpy : 2.3.5
pandas : 2.3.3
pymc_extras : 0.6.1.dev9+g828353dcd
pymc_marketing: 0.17.1
seaborn : 0.13.2
Watermark: 2.6.0