Modelo ITS multivariante para mercados saturados#
Este cuaderno se centra en el uso del modelo de Series de Tiempo Interrumpidas Multivariadas (MVITS) para estimar de dónde provienen las ventas de nuevos productos. Si aún no lo ha hecho, le recomendamos que lea nuestra guía de Introducción a la incrementalidad del producto así como la guía de modelo de series de tiempo interrumpidas multivariadas antes de continuar.
Recorreremos una serie de escenarios, desde simples hasta complejos, para mostrar cómo se puede utilizar el modelo MVITS para estimar el impacto de un nuevo producto en los productos existentes.
Configuración del cuaderno#
import arviz as az
import graphviz as gr
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
from pymc_marketing.customer_choice import MVITS, generate_saturated_data, plot_product
az.style.use("arviz-darkgrid")
plt.rcParams["figure.figsize"] = [12, 7]
plt.rcParams["figure.dpi"] = 100
plt.rcParams.update({"figure.constrained_layout.use": True})
seed = sum(map(ord, "Saturated market"))
rng = np.random.default_rng(seed)
%load_ext autoreload
%autoreload 2
%config InlineBackend.figure_format = "retina"
Generación de datos sintéticos#
Generaremos datos sintéticos de acuerdo con el siguiente modelo causal. Esto es intencionadamente simple en esta introducción al modelo MVITS y es probable que no capture las complejidades de los datos del mundo real.
Asumimos que existe una cuota de mercado latente (no observada) que equivale a la popularidad de diferentes productos o marcas.
Asumimos que existe un nivel de demanda que es en gran medida estable, pero es ruidoso. A veces se vende más producto que en otras ocasiones.
Los recuentos de ventas observados empíricamente están influenciados causalmente tanto por la cuota de mercado latente como por la demanda total. Esta relación podría modelarse de diferentes maneras, pero aquí asumimos \(\text{sales counts} \sim \mathrm{Multinomial}(\text{total demand}, \text{latent market share})\).
Participación de mercado empírica
Basado en los recuentos de ventas observados, podríamos calcular participaciones de mercado empíricas. Esto no juega un papel importante en nuestro proceso de generación de datos, pero es importante resaltar la diferencia entre las participaciones de mercado empíricas observadas ruidosas y las participaciones de mercado latentes, que representan alguna forma de popularidad subyacente de productos o marcas.
Si quisiéramos calcular la cuota de mercado empírica, esto sería simplemente un caso de dividir las ventas observadas (para cada producto/marca) por el total de ventas observadas.
Escenario 1 - ventas propias versus ventas de competidores#
El primer escenario que examinaremos es el más simple.
Nuestra empresa ha lanzado un nuevo producto
Tenemos datos de ventas que cubren antes y después del lanzamiento de este producto.
Independientemente de si tenemos datos de ventas a nivel de producto o no, nos enfocamos únicamente en si las ventas del nuevo producto son incrementales (sustrayendo ventas de los competidores) o canibalísticas (sustrayendo ventas de sus productos existentes). Por lo tanto, operamos con datos agregados en «las ventas totales de su empresa» y «las ventas totales de todos los competidores».
Para mantener el ejemplo simple, tenemos cuotas de mercado estables que solo cambian en el momento de introducir el nuevo producto.
Complejidades como la estacionalidad en la demanda total, la fijación de precios de productos u otros atributos se ignoran y no se incluyen en este modelo.
Podemos generar datos sintéticos a partir del modelo causal descrito anteriormente utilizando la función de utilidad generate_saturated_data.
scenario1 = {
"total_sales_mu": 1_000,
"total_sales_sigma": 5,
"treatment_time": 40,
"n_observations": 100,
"market_shares_before": [[0.7, 0.3, 0]],
"market_shares_after": [[0.65, 0.25, 0.1]],
"market_share_labels": ["competitor", "own", "new"],
"random_seed": rng,
}
data = generate_saturated_data(**scenario1)
data.head()
| product | competitor | own | new | pre |
|---|---|---|---|---|
| day | ||||
| 0 | 704 | 304 | 0 | True |
| 1 | 696 | 303 | 0 | True |
| 2 | 698 | 308 | 0 | True |
| 3 | 682 | 317 | 0 | True |
| 4 | 694 | 300 | 0 | True |
Como siempre, ¡debemos visualizar los datos! Podemos observar ventas ruidosas pero en general estables de sus propios productos y de todos los productos de la competencia. Luego, lanza un nuevo producto que se vende bastante bien. Visualmente, tenemos la impresión de que las ventas de sus propios productos y las ventas de la competencia disminuyen, pero ¿podemos cuantificar cuán incrementales o canibalistas es el nuevo producto? Ese será el objetivo de nuestro análisis.
Nota: En este ejemplo asumimos que tenemos un mercado saturado, por lo que las ventas del nuevo producto necesariamente restan ventas a otros productos.
fig, ax = plt.subplots()
plot_product(data, ax=ax)
ax.legend(loc="lower center", bbox_to_anchor=(0.5, -0.2), ncol=4)
ax.set_title("Sales - Scenario 1", fontsize=18, fontweight="bold");
Analicemos la relación entre las ventas del nuevo producto y tanto las ventas propias como las de la competencia. Podemos observar correlaciones negativas, lo cual tiene sentido desde la perspectiva del modelado MVITS. Esto se debe a que modela las ventas de productos existentes como disminuidas por una fracción de las ventas del nuevo producto. Por lo tanto, el modelo asume que (en el período posterior al lanzamiento del producto) a medida que aumentan las ventas del nuevo producto, las ventas propias o las de la competencia disminuirán o permanecerán sin cambios.
Construcción y ajuste de modelos#
Usaremos la clase MVITS para analizar los datos. Proporcionamos los nombres de las columnas correspondientes a las ventas de productos existentes antes de la introducción del nuevo producto. También establecemos la bandera saturated_market en True. Tenga en cuenta que los datos reales se proporcionan al modelo en una etapa posterior.
model1 = MVITS(
existing_sales=["competitor", "own"],
saturated_market=True,
sampler_config={"tune": 1_500, "draws": 2_000},
)
model1.model_config
{'intercept': Prior("Normal", dims="existing_product"),
'likelihood': Prior("TruncatedNormal", lower=0, sigma=Prior("HalfNormal", dims="existing_product"), dims=("time", "existing_product")),
'market_distribution': Prior("Dirichlet", a=[0.5 0.5], dims="existing_product")}
model1.inform_default_prior(
data=data.loc[: scenario1["treatment_time"], ["competitor", "own"]]
)
model1.model_config
{'intercept': Prior("Normal", mu=[697.07317073 298.73170732], sigma=[14.54027208 13.85825456], dims="existing_product"),
'likelihood': Prior("TruncatedNormal", lower=0, sigma=Prior("HalfNormal", sigma=14.199263321722825, dims="existing_product"), dims=("time", "existing_product")),
'market_distribution': Prior("Dirichlet", a=[0.5 0.5], dims="existing_product")}
Ahora proporcionamos los datos al modelo y lo ajustamos.
model1.sample(X=data[["competitor", "own"]], y=data["new"]);
Sampling: [beta, intercept, y, y_sigma]
Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [intercept, beta, y_sigma]
Sampling 4 chains for 1_500 tune and 2_000 draw iterations (6_000 + 8_000 draws total) took 3 seconds.
Sampling: [y]
Sampling: [y]
Diagnósticos del modelo#
Verifiquemos las trazas de los parámetros para ver si son estacionarios y para comprobar si las posteriores están bien mezcladas. Se ve bien.
Y dado que este es nuestro primer encuentro con el modelo MV-ITS, comparemos los anteriores y posteriores de los parámetros del modelo.
az.plot_dist_comparison(model1.idata, var_names="intercept", figsize=(10, 8));
az.plot_dist_comparison(model1.idata, var_names="y_sigma", figsize=(10, 8));
az.plot_dist_comparison(model1.idata, var_names="beta", figsize=(10, 8));
Resultados del modelo#
Evaluemos (visualmente) hasta qué punto el modelo puede explicar los datos de ventas trazando los ajustes predictivos posteriores. El gráfico a continuación se ve bastante bien; podemos observar que las predicciones del modelo (regiones sombreadas) capturan las principales propiedades de los datos de ventas reales, a saber, el cambio abrupto en el momento de la introducción del nuevo producto.
ax = model1.plot_fit()
ax.legend(loc="lower center", bbox_to_anchor=(0.5, -0.2), ncol=3)
Ahora examinemos las ventas predichas por el modelo en el escenario contrafactual donde la empresa no lanzó el nuevo producto. Podemos evaluar esto visualmente para ver si el escenario contrafactual tiene sentido. En este ejemplo ultra simplificado, es fácil evaluar esto como un «sí». El modelo predice lo que intuitivamente esperaríamos, que las ventas propias y las de los competidores habrían continuado como estaban antes del lanzamiento del nuevo producto.
ax = model1.plot_counterfactual()
ax.legend(loc="lower center", bbox_to_anchor=(0.5, -0.2), ncol=3)
Escenario contrafactual de no lanzar el nuevo producto
El escenario contrafactual se evalúa estableciendo las ventas del nuevo producto (utilizando el operador do de PyMC) en cero en los datos de entrada del modelo. Luego, realizamos un muestreo predictivo posterior para observar cómo se ven afectadas las cuentas de ventas esperadas y reales por esta intervención en el gráfico causal.
Al comparar los dos (las predicciones del modelo en los escenarios actual y contrafactual), tenemos nuestros impactos causales estimados sobre cada uno de los productos existentes del lanzamiento del nuevo producto. Aquí podemos ver que tanto las ventas propias como las de los competidores se redujeron debido al lanzamiento de las ventas del nuevo producto. Por lo tanto, el modelo está atribuyendo las ventas del nuevo producto como provenientes tanto de las ventas propias como de las de los competidores en (aproximadamente) cantidades iguales.
Sin embargo, podemos ver que no es exactamente 50/50 en términos de ventas. Para mirar más precisamente, podemos examinar los parámetros \(\beta\).
El coeficiente
beta[competitor]representa la proporción de ventas del nuevo producto que provienen de las ventas del competidor. Por lo tanto, esto representa la incrementalidad en términos de ventas (no de cuota de mercado).El coeficiente
beta[own]representa la proporción de ventas del nuevo producto que provino de sus propias ventas.
Podemos observar que los intervalos de credibilidad del 94% son relativamente ajustados y están aproximadamente centrados en 50/50. Por lo tanto, prácticamente podemos concluir que tenemos aproximadamente un 50% de incrementalidad y un 50% de canibalización.
Ventas frente a la cuota de mercado
Recuerde que los coeficientes \(\beta\) representan la proporción de ventas del nuevo producto que son atribuibles a las ventas de productos de la competencia y de su propio producto.
También podemos representar esto en el “espacio de participación de mercado”. En este ejemplo, con ventas totales estables, el gráfico se asemeja al anterior. Pero esto no tiene que ser así en escenarios más generales, por lo que poder representar el impacto causal tanto en términos de ventas como en términos de participación de mercado es útil.
Escenario 2 - alta variabilidad en las ventas totales#
Este escenario es exactamente el mismo que el anterior, excepto por tener una variabilidad mucho mayor en las cifras de ventas totales. No recorreremos todos los pasos con el mismo nivel de detalle que antes porque el enfoque principal es examinar los efectos de la mayor variabilidad en las ventas totales.
scenario2 = scenario1.copy()
scenario2["total_sales_sigma"] = 80
data = generate_saturated_data(**scenario2)
fig, ax = plt.subplots()
plot_product(data, ax=ax)
ax.legend(loc="lower center", bbox_to_anchor=(0.5, -0.2), ncol=4)
ax.set_title("Sales - Scenario 2", fontsize=18, fontweight="bold");
Echemos un vistazo a las relaciones entre las ventas del nuevo producto y las ventas propias y de los competidores. En comparación con el primer escenario, ahora observamos correlaciones positivas. Esto podría ser una señal de advertencia, ya que va en contra de la suposición del modelo: cuando las nuevas ventas son más altas, las ventas propias y de los competidores son más bajas.
¿Por qué está sucediendo esto? En un momento dado, dado que las ventas propias y las de los competidores se extraen de una distribución multinomial, esperaríamos que los conteos de ventas empíricos reflejen las cuotas de mercado latentes (que no cambian en este ejemplo). Sin embargo, la alta variabilidad en las ventas totales induce una correlación positiva en los conteos de ventas. En los días en que hay más ventas totales, tiende a haber más ventas de nuevos productos y más ventas propias y de competidores.
Esto no significa que el modelo esté muerto en el agua, pero sí resalta una posible desajuste entre el proceso de generación de datos y el modelo MVITS. Pero ajustemos el modelo y veamos si sus estimaciones son razonables.
Como mencionamos, esto es un aviso previo de problemas potenciales. Veamos cómo se desarrolla esto aplicando el modelo MVITS a los datos.
model2 = MVITS(
existing_sales=["competitor", "own"],
saturated_market=True,
sampler_config={"tune": 1_500, "draws": 2_000},
)
model2.inform_default_prior(
data=data.loc[: scenario2["treatment_time"], model2.existing_sales],
)
model2.sample(data.loc[:, model2.existing_sales], data["new"]);
Entonces, ¿cómo se desempeña el modelo? Trazar el impacto causal del nuevo producto sobre las ventas de los productos existentes parece razonable. Esto se debe principalmente a que los cambios en las ventas causados por el nuevo producto se dividen aproximadamente de manera equitativa entre las ventas de la competencia y las propias, y así es como configuramos los datos simulados. Sin embargo, en comparación con el primer ejemplo, podemos observar un nivel de incertidumbre mucho mayor en los impactos causales del nuevo producto.
Esto se puede observar cuantitativamente al examinar los pesos \(\beta\). Los intervalos creíbles del 94% de la contribución relativa de las ventas de nuevos productos a las ventas del competidor y propias son mucho más amplios, aproximadamente a \(\pm 10\%\).
Hasta ahora, hemos visto que para conjuntos de datos sintéticos bastante simples, el modelo MVITS realiza un trabajo razonable al inferir el nivel de incrementalidad y canibalización. Sin embargo, en muchas situaciones, es probable que el modelo no sea lo suficientemente robusto para desempeñarse bien. El escenario 2 ha demostrado esto: por el lado positivo, las estimaciones no parecen estar sesgadas, pero el nivel de incertidumbre en el nivel de canibalización/incrementalidad es bastante alto.
Escenario 3 - modelando productos individualmente#
Ahora ampliaremos más allá de la modelización a un nivel altamente agregado de ventas de «propias» y «competidoras». Ahora modelaremos las ventas de productos individuales.
# Create a list of product names. The last product is the new product
products = [f"product{i + 1}" for i in range(6)]
products.append("new")
scenario3 = {
"total_sales_mu": 5000,
"total_sales_sigma": 20,
"treatment_time": 40,
"n_observations": 100,
"market_shares_before": [[0.1, 0.1, 0.15, 0.2, 0.2, 0.25, 0.0]],
"market_shares_after": [[0.07, 0.08, 0.15, 0.2, 0.18, 0.22, 0.1]],
"market_share_labels": products,
"random_seed": rng,
}
data = generate_saturated_data(**scenario3)
data.head()
| product | product1 | product2 | product3 | product4 | product5 | product6 | new | pre |
|---|---|---|---|---|---|---|---|---|
| day | ||||||||
| 0 | 546 | 475 | 708 | 1028 | 986 | 1240 | 0 | True |
| 1 | 525 | 515 | 768 | 955 | 1025 | 1224 | 0 | True |
| 2 | 526 | 520 | 699 | 1057 | 1027 | 1200 | 0 | True |
| 3 | 533 | 497 | 731 | 1072 | 954 | 1205 | 0 | True |
| 4 | 487 | 534 | 776 | 1028 | 975 | 1187 | 0 | True |
Así que visualicemos los datos de ventas. Podemos ver que las ventas del nuevo producto son bastante altas y que las ventas de algunos de los productos existentes han disminuido.
fig, ax = plt.subplots()
plot_product(data, ax=ax, plot_total_sales=False)
ax.legend(loc="lower center", bbox_to_anchor=(0.5, -0.3), ncol=4)
ax.set_title("Sales - Scenario 3", fontsize=18, fontweight="bold");
Así que procedamos con el análisis. Esta vez proporcionaremos kwargs opcionales para el proceso de muestreo.
fit_kwargs = {"target_accept": 0.95}
model3 = MVITS(
existing_sales=products[:-1],
saturated_market=True,
sampler_config={"tune": 1_500, "draws": 2_000},
)
model3.inform_default_prior(
data=data.loc[: scenario3["treatment_time"], model3.existing_sales],
)
model3.sample(data[model3.existing_sales], data["new"], fit_kwargs=fit_kwargs);
No se han reportado problemas en el proceso de muestreo, y las cadenas parecen estar bien mezcladas.
La representación visual del ajuste del modelo se ve bastante bien.
fig, ax = plt.subplots()
model3.plot_fit(ax=ax, plot_total_sales=False)
ax.legend(loc="lower center", bbox_to_anchor=(0.5, -0.35), ncol=4)
ax.set_title("Sales - Scenario 3", fontsize=18, fontweight="bold");
Así lo hace el gráfico de la estimación del modelo de las ventas contrafactuales.
fig, ax = plt.subplots()
model3.plot_counterfactual(ax=ax, plot_total_sales=False)
ax.legend(loc="lower center", bbox_to_anchor=(0.5, -0.35), ncol=4)
ax.set_title("Sales - Scenario 3", fontsize=18, fontweight="bold");
Y podemos visualizar el impacto causal en términos de ventas primero, y luego en términos de cuota de mercado.
Finalmente, podemos inspeccionar los coeficientes \(\beta\) que nos indican qué proporción de las nuevas ventas proviene de cualquier producto existente.
Así que hemos visto cómo es posible extender el análisis simplificado de «nuestro» frente a «competidor» a un análisis más detallado de productos individuales. Esto es más realista, pero sigue siendo un escenario bastante simple. En el mundo real, probablemente tendríamos que lidiar con datos más complejos, y el modelo MVITS puede que no sea capaz de manejar estas complejidades.
%load_ext watermark
%watermark -n -u -v -iv -w -p pymc_marketing,pytensor
Last updated: Sat Dec 28 2024
Python implementation: CPython
Python version : 3.12.8
IPython version : 8.31.0
pymc_marketing: 0.10.0
pytensor : 2.22.1
seaborn : 0.13.2
numpy : 1.26.4
graphviz : 0.20.3
pymc_marketing: 0.10.0
arviz : 0.20.0
matplotlib : 3.10.0
Watermark: 2.5.0