Introducción al Modelo de Difusión de Bass#
¿Qué es el Modelo de Bass?#
El modelo de difusión de Bass, desarrollado por Frank Bass en 1969, es un modelo matemático que describe cómo los nuevos productos son adoptados en una población a lo largo del tiempo. Se utiliza ampliamente en marketing para prever las ventas de nuevos productos, especialmente cuando los datos históricos son limitados o inexistentes.
El modelo captura todo el ciclo de vida de la adopción del producto, desde la introducción hasta la saturación, convirtiéndolo en una herramienta poderosa para la planificación de productos y el desarrollo de estrategias de marketing.
La motivación detrás del modelo Bass#
Antes del modelo de Bass, las empresas luchaban por predecir los patrones de adopción de nuevos productos. Los métodos de pronóstico tradicionales a menudo fallaban porque no podían tener en cuenta las dinámicas sociales que impulsan la adopción de productos. Frank Bass reconoció que la adopción de productos sigue un patrón distinto:
Crecimiento inicial lento: Cuando un producto se lanza por primera vez, la adopción comienza lentamente.
Aceleración rápida: A medida que más personas adoptan, el boca a boca se difunde y la adopción se acelera.
Saturación eventual: Eventualmente, el mercado se satura y la adopción se ralentiza.
El modelo de Bass proporciona un marco matemático para capturar estos patrones, lo que permite a las empresas tomar decisiones más informadas sobre la planificación de la producción, la gestión de inventarios y la asignación de recursos de marketing.
Formulación Matemática#
El modelo de Bass se basa en una ecuación diferencial que describe la tasa de adopción:
Dónde:
\(F(t)\) es la fracción de base instalada (proporción acumulativa de adoptantes)
\(f(t)\) es la tasa de cambio de la fracción de la base instalada (\(f(t) = F'(t)\))
\(p\) es el coeficiente de innovación o influencia externa
\(q\) es el coeficiente de imitación o influencia interna
La solución a esta ecuación proporciona la curva de adopción:
La tasa de adopción en el tiempo \(t\) se da por:
Alternativamente, esto se puede escribir como:
Componentes Clave de la Implementación del Modelo Bass#
La implementación del modelo Bass en PyMC-Marketing consiste en varios componentes clave:
Adoptantes - El número de nuevas adopciones en el momento \(t\):
Innovadores - Adopciones impulsadas por influencia externa (publicidad, etc.):
Imitadores - Adopciones impulsadas por influencia interna (boca a boca):
Hora de Adopción Máxima - Cuando la tasa de adopción alcanza su máximo:
El número total de adoptantes a lo largo del tiempo es la suma de innovadores e imitadores, lo que equivale a \(\text{adopters}(t)\). Todos estos componentes están implementados directamente en el modelo de PyMC, lo que nos permite analizar cada aspecto del proceso de difusión por separado.
Comprendiendo la Relación Entre los Componentes#
Una idea clave del modelo de Bass es cómo descompone la adopción en dos fuentes:
En cada punto temporal:
Innovadores (\(m \cdot p \cdot (1 - F(t))\)) representa nuevas adopciones provenientes de personas que están influenciadas por factores externos como la publicidad.
Imitadores (\(m \cdot q \cdot F(t) \cdot (1 - F(t))\)) representa las nuevas adopciones que provienen de personas que están influenciadas por los adoptantes anteriores.
A medida que avanza el tiempo:
Inicialmente, los innovadores dominan el proceso de adopción cuando pocas personas han adoptado (\(F(t)\) es pequeño)
Más tarde, los imitadores se convierten en la fuente principal de nuevas adopciones a medida que crece el efecto de boca a boca.
Eventualmente, ambos disminuyen a medida que el mercado se acerca a la saturación (\(F(t)\) se aproxima a 1)
La adopción acumulativa en cualquier momento es:
Esto significa que a medida que \(t \to \infty\), la adopción acumulativa se aproxima al potencial total del mercado \(m\):
Por lo tanto, el modelo de Bass proporciona un análisis completo del mercado:
En cada momento, los nuevos adoptantes son innovadores o imitadores.
A lo largo de todo el ciclo de vida del producto, todos los posibles adoptantes (m) eventualmente adoptan el producto.
El modelo rastrea tanto la tasa de adopción (nuevos adoptantes por período de tiempo) como la adopción acumulativa (total de adoptantes hasta la fecha).
Esta estructura permite a los comercializadores comprender no solo cuántas personas adoptarán a lo largo del tiempo, sino también las fuerzas impulsoras detrás de la adopción en diferentes etapas del ciclo de vida del producto.
Comprendiendo los Parámetros Clave#
El modelo tiene tres parámetros principales:
Potencial de mercado (m): Número total de adoptantes eventuales (el tamaño final del mercado)
Coeficiente de innovación (p): Mide la influencia externa como la publicidad y los medios - típicamente \(0.01-0.03\)
Coeficiente de imitación (q): Mide la influencia interna como el boca a boca - típicamente \(0.3-0.5\)
Interpretación de Parámetros#
Un valor p más alto indica una influencia externa más fuerte (publicidad, marketing)
Un valor q más alto indica una influencia interna más fuerte (boca a boca, interacciones sociales)
La relación q/p indica la fuerza relativa de las influencias internas frente a las externas.
El pico de adopción ocurre en el momento {like_this}.
Innovadores vs. Imitadores#
El modelo de Bass distingue entre dos tipos de adoptantes:
Innovadores: Personas que adoptan independientemente de las decisiones de otros, influenciadas principalmente por los medios de comunicación masivos y las comunicaciones externas.
Representado matemáticamente como: \(\text{innovators}(t) = m \cdot p \cdot (1 - F(p, q, t))\)
Imitadores: Personas que adoptan debido a la influencia social y el boca a boca de adoptantes anteriores.
Representado matemáticamente como: \(\text{imitators}(t) = m \cdot q \cdot F(p, q, t) \cdot (1 - F(p, q, t))\)
Aplicaciones del mundo real#
El modelo Bass se ha aplicado con éxito para predecir la adopción de diversos productos y tecnologías:
Bienes de consumo duraderos: televisores, refrigeradores, lavadoras
Productos tecnológicos: Smartphones, computadoras, software
Productos farmacéuticos: Nuevos medicamentos y tratamientos
Productos de entretenimiento: Películas, juegos, servicios de streaming
Servicios y suscripciones: Servicios bancarios, planes de suscripción
Valor Empresarial: Por qué el Modelo Bass es Importante para Ejecutivos y Marketers#
Desde una perspectiva empresarial, el modelo de difusión de Bass proporciona ventajas competitivas sustanciales y beneficios de ROI:
1. Resource Optimization and Cash Flow Management#
Planificación de la Producción: Evite la costosa sobreproducción o falta de stock al prever con precisión las curvas de demanda.
Asignación del Presupuesto de Marketing: Optimizar el gasto a lo largo del ciclo de vida del producto, invirtiendo más durante los puntos de inflexión clave.
Eficiencia de la Cadena de Suministro: Coordinar con proveedores y distribuidores en función de las tasas de adopción previstas.
Optimización del Flujo de Efectivo: Predecir mejor los flujos de ingresos, mejorando la planificación financiera y las relaciones con los inversores.
2. Strategic Decision Making#
Tiempo de Lanzamiento: Determine el momento óptimo para ingresar a un mercado basado en patrones de difusión.
Estrategia de Precios: Implementar estrategias de precios dinámicos alineadas con la curva de adopción
Análisis Competitivo: Compare los parámetros de adopción de su producto con los de los competidores para identificar fortalezas y debilidades.
Gestión del Portafolio de Productos: Tome decisiones informadas sobre cuándo retirar productos antiguos e introducir nuevos.
3. Risk Mitigation#
Planificación de Escenarios: Pruebe diferentes supuestos de mercado y factores externos a través de variaciones de parámetros
Sistema de Alerta Temprana: Identificar desviaciones de las curvas de adopción esperadas de manera temprana, lo que permite una intervención más rápida.
Justificación de la Inversión: Proporcione pronósticos basados en datos para justificar las inversiones en I+D y marketing ante los interesados.
4. Performance Measurement#
Efectividad del Marketing: Mida el impacto de las campañas de marketing en el coeficiente de innovación (p)
Fuerza del Boca a Boca: Cuantifique el poder de la influencia social de su marca a través del coeficiente de imitación (q)
Potencial Total del Mercado: Valide o ajuste sus estimaciones del mercado total direccionable (m)
En el entorno empresarial actual impulsado por datos, las empresas que utilizan eficazmente modelos como la difusión de Bass obtienen una ventaja competitiva significativa a través de pronósticos más precisos, una mejor asignación de recursos y un momento estratégico en el mercado.
Extensiones Bayesianas#
En este cuaderno, mostramos cómo generar datos simulados a partir del modelo de Bass y ajustar un modelo bayesiano a estos. La formulación bayesiana ofrece varias ventajas:
Cuantificación de la incertidumbre a través de distribuciones a priori sobre parámetros
Modelado jerárquico para múltiples productos o mercados
Incorporación de conocimiento experto a través de priors informativos
Distribuciones de probabilidad completas para pronósticos de adopción futuros
Lo que haremos en este cuaderno#
En este cuaderno, vamos a:
Configurar parámetros para una simulación del modelo Bass
Generar datos de adopción simulados para múltiples productos
Ajuste el modelo Bass a nuestros datos simulados utilizando PyMC.
Visualizar las curvas de adopción
Preparar el cuaderno#
from typing import Any
import arviz as az
import matplotlib.pyplot as plt
import numpy as np
import numpy.typing as npt
import pandas as pd
import pymc as pm
import xarray as xr
from pymc_extras.prior import Prior, Scaled
from pymc_marketing.bass.model import create_bass_model
from pymc_marketing.plot import plot_curve
az.style.use("arviz-darkgrid")
plt.rcParams["figure.figsize"] = [12, 7]
plt.rcParams["figure.dpi"] = 100
%load_ext autoreload
%autoreload 2
%config InlineBackend.figure_format = "retina"
seed: int = sum(map(ord, "bass"))
rng: np.random.Generator = np.random.default_rng(seed=seed)
Configuración de los parámetros de simulación#
Primero, configuraremos los parámetros para nuestra simulación. Esto incluye:
El período de tiempo para nuestra simulación (en semanas)
El número de productos a simular
Fechas de inicio para el período de simulación
def setup_simulation_parameters(
n_weeks: int = 52,
n_products: int = 9,
start_date: str = "2023-01-01",
cutoff_start_date: str = "2023-12-01",
) -> tuple[
npt.NDArray[np.int_],
pd.DatetimeIndex,
pd.DatetimeIndex,
list[str],
pd.Series,
dict[str, Any],
]:
"""Set up initial parameters for the Bass diffusion model simulation.
Parameters
----------
n_weeks : int
Number of weeks to simulate
n_products : int
Number of products to include in the simulation
start_date : str
Starting date for the simulation period
cutoff_start_date : str
Latest possible start date for products
Returns
-------
T : numpy.ndarray
Time array (weeks)
possible_dates : pandas.DatetimeIndex
All dates in the simulation period
possible_start_dates : pandas.DatetimeIndex
Possible start dates for products
products : list
List of product names
product_start : pandas.Series
Start date for each product
coords : dict
Coordinates for PyMC model
"""
# Set a seed for reproducibility
seed = sum(map(ord, "bass"))
rng = np.random.default_rng(seed)
# Create time array and date range
T = np.arange(n_weeks)
possible_dates = pd.date_range(start_date, freq="W-MON", periods=n_weeks)
cutoff_start_date = pd.to_datetime(cutoff_start_date)
cutoff_start_date = cutoff_start_date + pd.DateOffset(weeks=1)
possible_start_dates = possible_dates[possible_dates < cutoff_start_date]
# Generate product names and random start dates
products = [f"P{i}" for i in range(n_products)]
product_start = pd.Series(
rng.choice(possible_start_dates, size=len(products)),
index=pd.Index(products, name="product"),
)
coords = {"T": T, "product": products}
return T, possible_dates, possible_start_dates, products, product_start, coords
Creando Distribuciones Previas#
Para nuestro modelo de Bass bayesiano, necesitamos especificar distribuciones a priori para los parámetros clave:
m (potencial de mercado): Cuántas unidades se pueden vender potencialmente en total
p (coeficiente de innovación): Tasa de adopción de influencias externas
q (coeficiente de imitación): Tasa de adopción a partir de influencias internas/sociales
probabilidad: La distribución de probabilidad que modela los datos de adopción observados
Para el potencial de mercado m, utilizamos un truco de escalado para especificar un prior sin escala y luego añadimos un factor global:
def create_bass_priors(factor: float) -> dict[str, Prior | Scaled]:
"""Define prior distributions for the Bass model parameters.
Returns
-------
dict
Dictionary of prior distributions for m, p, q, and likelihood
Notes
-----
- m: Market potential (scaled Gamma distribution)
- p: Innovation coefficient (Beta distribution)
- q: Imitation coefficient (Beta distribution)
- likelihood: Observation model (Negative Binomial)
"""
return {
# We use a scaled Gamma distribution for the market potential.
"m": Scaled(Prior("Gamma", mu=1, sigma=0.1, dims="product"), factor=factor),
"p": Prior("Beta", mu=0.03, dims="product").constrain(lower=0.01, upper=0.03),
"q": Prior("Beta", dims="product").constrain(lower=0.3, upper=0.5),
"likelihood": Prior("NegativeBinomial", n=1.5, dims="product"),
}
Generemos y visualicemos los priors.
FACTOR = 50_000
priors = create_bass_priors(factor=FACTOR)
/Users/will/mamba/envs/pymc-marketing-dev/lib/python3.10/site-packages/preliz/distributions/beta.py:127: RuntimeWarning: invalid value encountered in scalar divide
mu = alpha / alpha_plus_beta
/Users/will/mamba/envs/pymc-marketing-dev/lib/python3.10/site-packages/preliz/distributions/beta.py:128: RuntimeWarning: invalid value encountered in scalar divide
sigma = (alpha * beta) ** 0.5 / alpha_plus_beta / (alpha_plus_beta + 1) ** 0.5
The requested mass is 0.95, but the computed one is 0.528
fig, ax = plt.subplots(nrows=2, ncols=1, figsize=(15, 12))
priors["p"].preliz.plot_pdf(ax=ax[0])
ax[0].set(title="Innovation Coefficient (p)")
priors["q"].preliz.plot_pdf(ax=ax[1])
ax[1].set(title="Imitation Coefficient (q)")
fig.suptitle(
"Prior Distributions for Bass Model Parameters",
fontsize=18,
fontweight="bold",
y=0.95,
);
Observe que hemos elegido los priors dentro de los rangos habituales de los estudios empíricos:
Coeficiente de innovación (p): Mide la influencia externa como la publicidad y los medios - típicamente \(0.01-0.03\)
Coeficiente de imitación (q): Mide la influencia interna como el boca a boca - típicamente \(0.3-0.5\)
Generar Datos Sintéticos#
Con el modelo generativo Bass, podemos generar un conjunto de datos sintético muestreando del previo y eligiendo una muestra particular para usar como datos observados. Para este propósito, definimos dos funciones auxiliares.
def sample_prior_bass_data(model: pm.Model) -> xr.DataArray:
"""Generate a sample from the prior predictive distribution of the Bass model.
Parameters
----------
model : pymc.Model
The PyMC model to sample from
Returns
-------
xarray.DataArray
Simulated adoption data
"""
with model:
idata = pm.sample_prior_predictive(random_seed=rng)
return idata["prior"]["y"].sel(chain=0, draw=0)
def transform_to_actual_dates(bass_data, product_start, possible_dates) -> pd.DataFrame:
"""Transform simulation data from time index to calendar dates.
Parameters
----------
bass_data : xarray.DataArray
Simulated bass model data
product_start : pandas.Series
Start date for each product
possible_dates : pandas.DatetimeIndex
All dates in the simulation period
Returns
-------
pandas.DataFrame
Adoption data with actual calendar dates
"""
bass_data = bass_data.to_dataset()
bass_data["product_start"] = product_start.to_xarray()
df_bass_data = (
bass_data.to_dataframe().drop(columns=["chain", "draw"]).reset_index()
)
df_bass_data["actual_date"] = df_bass_data["product_start"] + pd.to_timedelta(
7 * df_bass_data["T"], unit="days"
)
return (
df_bass_data.set_index(["actual_date", "product"])
.y.unstack(fill_value=0)
.reindex(possible_dates, fill_value=0)
)
Ahora podemos generar los datos observados:
# Setup simulation parameters
T, possible_dates, _, products, product_start, coords = setup_simulation_parameters()
# Create and configure the Bass model
generative_model = create_bass_model(t=T, coords=coords, observed=None, priors=priors)
# Sample and select one "observed" dataset.
bass_data = sample_prior_bass_data(generative_model)
actual_data = transform_to_actual_dates(bass_data, product_start, possible_dates)
Sampling: [m_unscaled, p, q, y]
El marco de datos actual_data tiene el formato típico de un conjunto de datos real.
actual_data
| product | P0 | P1 | P2 | P3 | P4 | P5 | P6 | P7 | P8 |
|---|---|---|---|---|---|---|---|---|---|
| 2023-01-02 | 0 | 0 | 451 | 0 | 0 | 0 | 0 | 0 | 0 |
| 2023-01-09 | 0 | 0 | 56 | 0 | 0 | 0 | 0 | 0 | 0 |
| 2023-01-16 | 0 | 0 | 1948 | 0 | 0 | 0 | 0 | 0 | 0 |
| 2023-01-23 | 0 | 0 | 3742 | 0 | 0 | 0 | 0 | 0 | 0 |
| 2023-01-30 | 0 | 239 | 156 | 0 | 0 | 137 | 0 | 0 | 0 |
| 2023-02-06 | 0 | 4199 | 4732 | 0 | 0 | 2208 | 0 | 0 | 0 |
| 2023-02-13 | 0 | 1646 | 6477 | 0 | 0 | 1671 | 0 | 0 | 0 |
| 2023-02-20 | 0 | 1810 | 2824 | 0 | 0 | 780 | 0 | 0 | 0 |
| 2023-02-27 | 0 | 17004 | 2583 | 1485 | 0 | 6002 | 0 | 0 | 0 |
| 2023-03-06 | 0 | 4956 | 2508 | 2679 | 0 | 8211 | 0 | 0 | 0 |
| 2023-03-13 | 1277 | 7956 | 3255 | 2340 | 0 | 2404 | 0 | 0 | 0 |
| 2023-03-20 | 1403 | 4926 | 1030 | 3838 | 0 | 3646 | 0 | 0 | 0 |
| 2023-03-27 | 1818 | 1091 | 3145 | 1443 | 0 | 13557 | 0 | 0 | 0 |
| 2023-04-03 | 2865 | 4696 | 613 | 85 | 0 | 2857 | 0 | 0 | 0 |
| 2023-04-10 | 1410 | 8236 | 717 | 2858 | 0 | 3112 | 0 | 0 | 0 |
| 2023-04-17 | 1472 | 499 | 77 | 3519 | 0 | 3920 | 0 | 0 | 0 |
| 2023-04-24 | 11868 | 1783 | 340 | 2431 | 0 | 1661 | 0 | 0 | 0 |
| 2023-05-01 | 4669 | 102 | 314 | 8487 | 0 | 2908 | 0 | 0 | 0 |
| 2023-05-08 | 3423 | 61 | 210 | 1479 | 0 | 2729 | 0 | 0 | 0 |
| 2023-05-15 | 3625 | 1226 | 11 | 1544 | 0 | 2125 | 0 | 0 | 0 |
| 2023-05-22 | 1591 | 256 | 110 | 391 | 0 | 1164 | 0 | 0 | 0 |
| 2023-05-29 | 3351 | 149 | 78 | 2397 | 0 | 849 | 0 | 0 | 0 |
| 2023-06-05 | 1246 | 205 | 9 | 205 | 0 | 1060 | 0 | 0 | 0 |
| 2023-06-12 | 1805 | 106 | 22 | 187 | 0 | 229 | 0 | 0 | 0 |
| 2023-06-19 | 1353 | 75 | 15 | 131 | 0 | 38 | 0 | 0 | 0 |
| 2023-06-26 | 457 | 48 | 5 | 116 | 0 | 88 | 0 | 0 | 0 |
| 2023-07-03 | 170 | 46 | 8 | 108 | 0 | 79 | 0 | 0 | 0 |
| 2023-07-10 | 44 | 67 | 6 | 57 | 0 | 16 | 0 | 0 | 0 |
| 2023-07-17 | 271 | 8 | 1 | 37 | 0 | 14 | 0 | 0 | 0 |
| 2023-07-24 | 42 | 0 | 1 | 4 | 0 | 19 | 0 | 0 | 2578 |
| 2023-07-31 | 32 | 6 | 1 | 9 | 0 | 0 | 0 | 0 | 1418 |
| 2023-08-07 | 93 | 5 | 0 | 3 | 0 | 10 | 0 | 0 | 1005 |
| 2023-08-14 | 83 | 1 | 1 | 0 | 0 | 4 | 0 | 0 | 2560 |
| 2023-08-21 | 47 | 0 | 0 | 3 | 0 | 1 | 0 | 0 | 3666 |
| 2023-08-28 | 8 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 13816 |
| 2023-09-04 | 4 | 0 | 0 | 0 | 0 | 0 | 0 | 2496 | 7358 |
| 2023-09-11 | 4 | 0 | 0 | 0 | 0 | 0 | 556 | 1266 | 6785 |
| 2023-09-18 | 2 | 0 | 0 | 0 | 0 | 3 | 2151 | 3637 | 2370 |
| 2023-09-25 | 2 | 0 | 0 | 0 | 0 | 0 | 4980 | 1431 | 1351 |
| 2023-10-02 | 0 | 1 | 0 | 1 | 0 | 0 | 2124 | 507 | 2038 |
| 2023-10-09 | 0 | 0 | 0 | 0 | 0 | 0 | 14239 | 1632 | 3441 |
| 2023-10-16 | 0 | 0 | 0 | 0 | 0 | 0 | 25224 | 4681 | 562 |
| 2023-10-23 | 0 | 0 | 0 | 0 | 0 | 0 | 17233 | 2095 | 3801 |
| 2023-10-30 | 0 | 0 | 0 | 0 | 1042 | 0 | 4728 | 1408 | 446 |
| 2023-11-06 | 0 | 0 | 0 | 0 | 1776 | 0 | 1890 | 1890 | 487 |
| 2023-11-13 | 0 | 0 | 0 | 0 | 1697 | 0 | 1058 | 3665 | 673 |
| 2023-11-20 | 0 | 0 | 0 | 0 | 6648 | 0 | 943 | 391 | 169 |
| 2023-11-27 | 0 | 0 | 0 | 0 | 4746 | 0 | 605 | 2720 | 374 |
| 2023-12-04 | 0 | 0 | 0 | 0 | 1843 | 0 | 1554 | 1382 | 98 |
| 2023-12-11 | 0 | 0 | 0 | 0 | 3598 | 0 | 2304 | 147 | 151 |
| 2023-12-18 | 0 | 0 | 0 | 0 | 6576 | 0 | 124 | 2582 | 42 |
| 2023-12-25 | 0 | 0 | 0 | 0 | 3610 | 0 | 215 | 110 | 160 |
Por otro lado, los bass_data tienen los mismos datos que los arreglos indexados por tiempo (relativo) y producto.
Visualicemos ambos.
fig, ax = plt.subplots(
nrows=2, ncols=1, figsize=(15, 12), sharex=False, sharey=True, layout="constrained"
)
# Plot raw simulated data (by time step)
bass_data.to_series().unstack().plot(ax=ax[0])
ax[0].legend(
title="Product", title_fontsize=14, loc="center left", bbox_to_anchor=(1, 0.5)
)
ax[0].set(
title="Simulated Weekly Adoption by Product (Time Steps)",
xlabel="Time Step (Weeks)",
ylabel="Number of Adoptions",
)
# Plot data with actual calendar dates
actual_data.plot(ax=ax[1])
ax[1].legend(
title="Product", title_fontsize=14, loc="center left", bbox_to_anchor=(1, 0.5)
)
ax[1].set(
title="Simulated Weekly Adoption by Product (Calendar Dates)",
xlabel="Date",
ylabel="Number of Adoptions",
)
fig.suptitle(
"Bass Diffusion Model - Simulated Product Adoption", fontsize=18, fontweight="bold"
);
Ajustar el Modelo#
Ahora estamos listos para ajustar el modelo y generar las distribuciones predictivas posteriores.
# We condition the model on observed data.
with pm.observe(generative_model, {"y": bass_data.values}) as model:
idata = pm.sample(
tune=1_500,
draws=2_000,
chains=4,
nuts_sampler="nutpie",
compile_kwargs={"mode": "NUMBA"},
random_seed=rng,
)
idata.extend(
pm.sample_posterior_predictive(
idata, model=model, extend_inferencedata=True, random_seed=rng
)
)
Sampling ... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 0:00:00 / 0:00:01
No tenemos ninguna divergencia. Veamos el resumen de los parámetros.
az.summary(data=idata, var_names=["p", "q", "m"])
| mean | sd | hdi_3% | hdi_97% | mcse_mean | mcse_sd | ess_bulk | ess_tail | r_hat | |
|---|---|---|---|---|---|---|---|---|---|
| p[P0] | 0.026 | 0.005 | 0.018 | 0.036 | 0.000 | 0.000 | 10750.0 | 5899.0 | 1.0 |
| p[P1] | 0.030 | 0.005 | 0.020 | 0.039 | 0.000 | 0.000 | 11579.0 | 6089.0 | 1.0 |
| p[P2] | 0.027 | 0.005 | 0.017 | 0.036 | 0.000 | 0.000 | 11292.0 | 5970.0 | 1.0 |
| p[P3] | 0.030 | 0.005 | 0.020 | 0.040 | 0.000 | 0.000 | 12126.0 | 6250.0 | 1.0 |
| p[P4] | 0.026 | 0.005 | 0.018 | 0.035 | 0.000 | 0.000 | 12027.0 | 6287.0 | 1.0 |
| p[P5] | 0.021 | 0.004 | 0.013 | 0.028 | 0.000 | 0.000 | 10939.0 | 5817.0 | 1.0 |
| p[P6] | 0.032 | 0.005 | 0.022 | 0.042 | 0.000 | 0.000 | 11071.0 | 5834.0 | 1.0 |
| p[P7] | 0.030 | 0.005 | 0.020 | 0.039 | 0.000 | 0.000 | 12427.0 | 5130.0 | 1.0 |
| p[P8] | 0.030 | 0.005 | 0.021 | 0.040 | 0.000 | 0.000 | 11252.0 | 6353.0 | 1.0 |
| q[P0] | 0.402 | 0.019 | 0.368 | 0.439 | 0.000 | 0.000 | 10124.0 | 5923.0 | 1.0 |
| q[P1] | 0.394 | 0.019 | 0.360 | 0.429 | 0.000 | 0.000 | 10431.0 | 6172.0 | 1.0 |
| q[P2] | 0.397 | 0.019 | 0.363 | 0.434 | 0.000 | 0.000 | 11818.0 | 6179.0 | 1.0 |
| q[P3] | 0.450 | 0.022 | 0.413 | 0.493 | 0.000 | 0.000 | 12285.0 | 6374.0 | 1.0 |
| q[P4] | 0.354 | 0.017 | 0.324 | 0.386 | 0.000 | 0.000 | 11480.0 | 6068.0 | 1.0 |
| q[P5] | 0.385 | 0.018 | 0.350 | 0.418 | 0.000 | 0.000 | 9830.0 | 5566.0 | 1.0 |
| q[P6] | 0.430 | 0.020 | 0.393 | 0.468 | 0.000 | 0.000 | 12521.0 | 6256.0 | 1.0 |
| q[P7] | 0.344 | 0.017 | 0.311 | 0.375 | 0.000 | 0.000 | 12231.0 | 5460.0 | 1.0 |
| q[P8] | 0.339 | 0.017 | 0.307 | 0.369 | 0.000 | 0.000 | 10948.0 | 6146.0 | 1.0 |
| m[P0] | 49568.996 | 4491.414 | 40760.951 | 57490.821 | 41.520 | 55.743 | 11720.0 | 5933.0 | 1.0 |
| m[P1] | 51816.841 | 4487.349 | 43766.599 | 60653.332 | 41.988 | 55.334 | 11242.0 | 5793.0 | 1.0 |
| m[P2] | 47118.829 | 4436.392 | 39310.947 | 55843.304 | 41.551 | 54.987 | 11186.0 | 5118.0 | 1.0 |
| m[P3] | 48429.471 | 4518.438 | 40231.737 | 57157.439 | 41.948 | 54.340 | 11472.0 | 5854.0 | 1.0 |
| m[P4] | 51994.167 | 4501.491 | 43959.011 | 60680.809 | 43.404 | 54.416 | 10713.0 | 5973.0 | 1.0 |
| m[P5] | 53276.493 | 4568.591 | 45196.257 | 62426.608 | 46.587 | 61.073 | 9433.0 | 5492.0 | 1.0 |
| m[P6] | 53087.905 | 4612.168 | 44525.164 | 61948.611 | 45.914 | 58.424 | 10125.0 | 5873.0 | 1.0 |
| m[P7] | 47684.913 | 4315.345 | 39879.468 | 55899.426 | 40.015 | 50.408 | 11727.0 | 6210.0 | 1.0 |
| m[P8] | 51232.596 | 4464.032 | 42849.916 | 59268.568 | 43.671 | 57.224 | 10498.0 | 5213.0 | 1.0 |
_ = az.plot_trace(
data=idata,
var_names=["p", "q", "m"],
compact=True,
backend_kwargs={"figsize": (12, 7), "layout": "constrained"},
)
plt.gcf().suptitle("Model Trace", fontsize=16);
En general, los diagnósticos y el seguimiento se ven bien.
A continuación, examinamos las distribuciones posteriores de los parámetros.
ax, *_ = az.plot_forest(idata["posterior"]["p"], combined=True)
ax.axvline(x=priors["p"].parameters["mu"], color="gray", linestyle="--")
ax.get_figure().suptitle("Innovation Coefficient (p)", fontsize=18, fontweight="bold")
ax, *_ = az.plot_forest(idata["posterior"]["q"], combined=True)
ax.axvline(x=priors["q"].preliz.mean(), color="gray", linestyle="--")
ax.get_figure().suptitle("Imitation Coefficient (q)", fontsize=18, fontweight="bold")
Observamos cierta heterogeneidad en los parámetros, pero en general están centrados en los valores verdaderos (del modelo generativo).
Examinando las Predicciones Posteriores para Productos Específicos#
Veamos las distribuciones predictivas posteriores para ver qué tan bien nuestro modelo captura los datos simulados.
fig, axes = plt.subplots(
nrows=3, ncols=3, figsize=(15, 12), sharex=True, sharey=True, layout="constrained"
)
idata["posterior_predictive"]["y"].pipe(plot_curve, {"T"}, axes=axes)
for i, ax in enumerate(axes.flatten()):
ax.plot(T, bass_data[:, i], color="black")
fig.suptitle("Posterior Predictive vs Observed Data", fontsize=18, fontweight="bold");
fig, axes = plt.subplots(
nrows=3, ncols=3, figsize=(15, 12), sharex=True, sharey=True, layout="constrained"
)
idata["posterior_predictive"]["y"].cumsum(dim="T").pipe(plot_curve, {"T"}, axes=axes)
for i, ax in enumerate(axes.flatten()):
ax.plot(T, bass_data[:, i].cumsum(), color="black")
fig.suptitle(
"Cumulative Posterior Predictive vs Cumulative Observed Data",
fontsize=18,
fontweight="bold",
);
observed_cumulative = bass_data.cumsum(dim="T").isel(T=-1).to_series()
ref_val = {
"m": [
{"product": name, "ref_val": value}
for name, value in observed_cumulative.items()
]
}
az.plot_posterior(
idata.posterior,
var_names=["m"],
backend_kwargs=dict(sharex=True, layout="constrained", figsize=(15, 12)),
ref_val=ref_val,
)
max_T = bass_data.coords["T"].max().item()
fig = plt.gcf()
fig.suptitle(
f"Estimated Market Cap (m) vs Observed Cumulative at T={max_T}",
fontsize=18,
fontweight="bold",
);
En general, el modelo hace un buen trabajo al capturar los datos.
A continuación, examinamos a los adoptantes, que representan el valor esperado de la probabilidad.
fig, axes = plt.subplots(
nrows=3, ncols=3, figsize=(15, 12), sharex=True, sharey=True, layout="constrained"
)
idata["posterior"]["adopters"].pipe(plot_curve, {"T"}, axes=axes)
for i, ax in enumerate(axes.flatten()):
ax.plot(T, bass_data[:, i], color="black")
fig.suptitle("Adopters vs Observed Data", fontsize=18, fontweight="bold");
Esto muestra que el ajuste es, de hecho, bastante razonable.
También podemos evaluar la calidad del modelo analizando los datos acumulativos:
Nota
Recuerde que los adoptantes son la media de la distribución, por lo que vemos algunas curvas acumulativas por encima y algunas por debajo.
Mire el idata["posterior_predictive"]["y"] para los datos observados.
fig, axes = plt.subplots(
nrows=3, ncols=3, figsize=(15, 12), sharex=True, sharey=True, layout="constrained"
)
idata["posterior"]["adopters"].cumsum(dim="T").pipe(plot_curve, {"T"}, axes=axes)
for i, ax in enumerate(axes.flatten()):
ax.plot(T, bass_data[:, i].cumsum(), color="black")
fig.suptitle("Adopters Cumulative vs Observed Data", fontsize=18, fontweight="bold");
Podemos mejorar esta vista al analizar los componentes del modelo: innovadores e imitadores (en naranja y verde, respectivamente).
fig, axes = plt.subplots(
nrows=3, ncols=3, figsize=(15, 12), sharex=True, sharey=True, layout="constrained"
)
idata["posterior"]["adopters"].cumsum(dim="T").pipe(
plot_curve, {"T"}, colors=3 * 3 * ["C0"], axes=axes
)
idata["posterior"]["innovators"].pipe(
plot_curve, {"T"}, colors=3 * 3 * ["C1"], axes=axes
)
idata["posterior"]["imitators"].pipe(
plot_curve, {"T"}, colors=3 * 3 * ["C2"], axes=axes
)
for i, ax in enumerate(axes.flatten()):
ax.plot(T, bass_data[:, i].cumsum(), color="black")
fig.suptitle("Innovators vs Imitators", fontsize=18, fontweight="bold");
Finalmente, podemos inspeccionar el pico de la curva de adopción.
ax, *_ = az.plot_forest(idata["posterior"]["peak"], combined=True)
ax.get_figure().suptitle("Peak", fontsize=18, fontweight="bold");
Esto se ajusta bastante bien a los datos observados. Veamos, por ejemplo, el producto P4.
fig, ax = plt.subplots()
product_id = 4
bass_data[:, product_id].plot(ax=ax, color="black")
idata["posterior"]["adopters"].sel(product=f"P{product_id}").pipe(
plot_curve, {"T"}, axes=ax
)
peak_hdi = az.hdi(idata["posterior"]["peak"].sel(product=f"P{product_id}"))["peak"]
ax.axvspan(
peak_hdi.sel(hdi="lower").item(),
peak_hdi.sel(hdi="higher").item(),
color="C1",
alpha=0.4,
)
ax.set_title(f"Peak Product {products[product_id]}", fontsize=18, fontweight="bold");
%load_ext watermark
%watermark -n -u -v -iv -w -p nutpie,pymc_marketing,pytensor
Last updated: Wed May 21 2025
Python implementation: CPython
Python version : 3.10.16
IPython version : 8.34.0
nutpie : 0.14.3
pymc_marketing: 0.13.1
pytensor : 2.30.3
pymc_marketing: 0.13.1
xarray : 2025.3.1
arviz : 0.21.0
numpy : 1.26.4
pandas : 2.2.3
matplotlib : 3.10.1
pymc : 5.22.0
Watermark: 2.5.0