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:

  1. 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.

  2. 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 GeometricAdstock con un retraso máximo de 8 semanas

  • Transformación de saturación: Usamos LogisticSaturation para capturar los rendimientos decrecientes

  • Distribuciones 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:

# Waterfall decomposition
mmm.plot.waterfall_components_decomposition(original_scale=True);

Este gráfico responde a la pregunta: «¿Cuánto contribuyó cada componente a las ventas totales?»

Participación de contribución por canal#

¿Qué porcentaje de ventas impulsadas por medios proviene de cada canal?

# Plot channel contribution share
fig = mmm.plot.channel_contribution_share_hdi(figsize=(8, 3));

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()
../../_images/7ace9e0b30e71b851ec5e551ff53fa22cc301abbca6167fa8980afce75412fc7.png

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:

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#

%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