Modelo ITS multivariado para mercados no saturados#

Este cuaderno muestra cómo utilizar el modelo de Series de Tiempo Interrumpidas Multivariadas (MVITS) para estimar de dónde provienen las ventas de nuevos productos en el caso de un mercado no saturado. Es decir, cuando un nuevo producto puede provocar (algunas) reducciones en las ventas de productos existentes, pero algunas de las ventas del nuevo producto se dan en forma de crecimiento del mercado.

También veremos un escenario como este que el modelo MVITS no puede manejar (al menos actualmente). Es importante ser consciente de las limitaciones del modelo y de si su conjunto de datos se ajusta a las suposiciones del modelo.

Configuración del cuaderno#

import arviz as az
import matplotlib.pyplot as plt
import numpy as np

from pymc_marketing.customer_choice import (
    MVITS,
    generate_unsaturated_data,
    plot_product,
)

rng = np.random.default_rng(123)

az.style.use("arviz-darkgrid")
plt.rcParams["figure.figsize"] = [12, 7]
plt.rcParams["figure.dpi"] = 100
plt.rcParams.update({"figure.constrained_layout.use": True})


%load_ext autoreload
%autoreload 2
%config InlineBackend.figure_format = "retina"

Un ejemplo de mercado no saturado#

Exploremos una situación en la que se lanza un nuevo producto y se vende razonablemente bien. Algunas de esas ventas del nuevo producto provienen de productos existentes, pero algunas provienen de nuevos clientes.

Así que el aumento total de ventas, pero lo importante es que esas ventas adicionales se dirigen al nuevo producto. Este es probablemente un escenario bastante común en el mundo real. Se lanza un nuevo producto y quizás sea mucho mejor que los productos existentes. Así que si se venden 1000 unidades, algunas de esas ventas serán “tomadas” de las ventas de productos existentes, pero el nuevo producto es tan bueno que atrae a nuevos clientes que no habrían comprado ninguno de los productos existentes. Esto podría suceder si el nuevo producto cae por debajo de un punto de precio psicológicamente significativo, o si está asociado con un nuevo marketing, o simplemente tiene una nueva característica que atrae a nuevos clientes.

scenario1 = {
    "total_sales_before": [800],
    "total_sales_after": [950],
    "total_sales_sigma": 10,
    "treatment_time": 40,
    "n_observations": 100,
    "market_shares_before": [[500 / 800, 300 / 800, 0]],
    "market_shares_after": [[400 / 950, 200 / 950, 350 / 950]],
    "market_share_labels": ["competitor", "own", "new"],
    "random_seed": rng,
}

data = generate_unsaturated_data(**scenario1)

data.head()
product competitor own new pre
day
0 497 293 0 True
1 471 325 0 True
2 522 290 0 True
3 497 304 0 True
4 509 300 0 True

Las propiedades clave de este ejemplo son:

  • El aumento total de ventas se produce cuando se lanza el nuevo producto.

  • Algunas de las ventas del nuevo producto provienen de productos existentes. Podemos deducir esto porque a) las ventas de los productos existentes disminuyen, y b) las ventas del nuevo producto son mayores que la disminución en las ventas de los productos existentes.

  • Los números de ventas absolutos de los productos existentes disminuyen. Esta es una de las suposiciones clave del modelo MVITS.

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", fontsize=18, fontweight="bold");
model1 = MVITS(
    existing_sales=["competitor", "own"],
    saturated_market=False,
    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 0.5], dims="all_sources")}
model1.inform_default_prior(
    data=data.loc[: scenario1["treatment_time"], ["competitor", "own"]]
)
model1.model_config
{'intercept': Prior("Normal", mu=[500.75609756 295.95121951], sigma=[19.83655778 19.16631318], dims="existing_product"),
 'likelihood': Prior("TruncatedNormal", lower=0, sigma=Prior("HalfNormal", sigma=19.501435478675347, dims="existing_product"), dims=("time", "existing_product")),
 'market_distribution': Prior("Dirichlet", a=[0.5 0.5 0.5], dims="all_sources")}
model1.sample(data[["competitor", "own"]], data["new"]);
Sampling: [beta_all, intercept, y, y_sigma]
Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [intercept, beta_all, y_sigma]

Sampling 4 chains for 1_500 tune and 2_000 draw iterations (6_000 + 8_000 draws total) took 4 seconds.
Sampling: [y]

Sampling: [y]

Podemos ver que el modelo hace un trabajo razonable al tener en cuenta los datos, ya que captura los niveles de ventas previos a la introducción y la reducción en las ventas en el momento en que se introduce el nuevo producto.

fig, ax = plt.subplots()
model1.plot_fit(ax=ax)
ax.legend(loc="lower center", bbox_to_anchor=(0.5, -0.25), ncol=4);

El gráfico contrafactual también se ve bien. El modelo estima que las ventas contrafactuales de los productos existentes habrían continuado como estaban antes de la introducción del nuevo producto.

fig, ax = plt.subplots()
model1.plot_counterfactual(ax=ax)
ax.legend(loc="lower center", bbox_to_anchor=(0.5, -0.25), ncol=4);

El modelo muestra que la introducción del nuevo producto causó una disminución en las ventas de los productos existentes.

model1.plot_causal_impact_sales();

Y al observar los parámetros \(\beta\), podemos ver:

  • beta_all[competitor] muestra que aproximadamente el 33% de las ventas del nuevo producto son incrementales, en el sentido de que se toman de las ventas de los competidores.

  • beta_all[own] muestra que aproximadamente el 27% de las ventas del nuevo producto son canibalizaciones, en el sentido de que se toman de las ventas de productos existentes de su propia empresa.

  • beta_all[new] muestra que aproximadamente el 40% de las ventas del nuevo producto provienen del crecimiento del mercado.

az.plot_posterior(model1.idata, var_names=["beta_all"], textsize=11, figsize=(12, 3));

Variables beta

Cuando modelamos mercados no saturados utilizando el argumento saturated_market=False en el modelo MVITS, necesitamos usar la variable beta_all, no la variable beta.

Límites del modelo MVITS#

En esta sección exploraremos un escenario que el MVITS es incapaz de modelar, al menos en su forma actual. Es decir, no puede manejar situaciones en las que las ventas de productos existentes aumentan después de la introducción del nuevo producto. Esto se debe a que el modelo MVITS asume que las ventas de los productos existentes son constantes a lo largo del tiempo y que el nuevo producto solo afecta negativamente las ventas de los productos existentes.

Advertencia

Esta sección tiene como objetivo demostrar un escenario de datos que el MVITS no es capaz de modelar bien. Al menos, no en su forma actual.

scenario2 = {
    "total_sales_before": [1000],
    "total_sales_after": [1400],
    "total_sales_sigma": 20,
    "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_unsaturated_data(**scenario2)

En este escenario:

  • La cuota de mercado de los productos de la competencia comienza en 70% y se reduce a 65% cuando se introduce el nuevo producto.

  • La cuota de mercado de nuestros propios productos comienza en un 30% y se reduce al 25% cuando se introduce el nuevo producto.

  • El nuevo producto gana un 10% de cuota de mercado cuando se introduce.

  • Sin embargo, la introducción del nuevo producto también aumenta el tamaño total del mercado de 1000 a 1400 unidades en promedio.

  • Esto significa que, aunque las cuotas de mercado de los productos existentes se reducen, sus ventas en realidad aumentan.

Es este aumento en los números de ventas que el modelo MV-ITS (tal como está implementado actualmente) no puede manejar.

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", fontsize=18, fontweight="bold");
model2 = MVITS(
    existing_sales=["competitor", "own"],
    saturated_market=False,
    sampler_config={"tune": 1_500, "draws": 2_000},
)
model2.inform_default_prior(
    data=data.loc[: scenario2["treatment_time"], ["competitor", "own"]]
)

model2.model_config
{'intercept': Prior("Normal", mu=[700.82926829 300.68292683], sigma=[37.92288388 18.94523558], dims="existing_product"),
 'likelihood': Prior("TruncatedNormal", lower=0, sigma=Prior("HalfNormal", sigma=28.434059731654337, dims="existing_product"), dims=("time", "existing_product")),
 'market_distribution': Prior("Dirichlet", a=[0.5 0.5 0.5], dims="all_sources")}
model2.sample(data[["competitor", "own"]], data["new"]);
Sampling: [beta_all, intercept, y, y_sigma]
Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [intercept, beta_all, y_sigma]

Sampling 4 chains for 1_500 tune and 2_000 draw iterations (6_000 + 8_000 draws total) took 5 seconds.
There were 16 divergences after tuning. Increase `target_accept` or reparameterize.
Sampling: [y]

Sampling: [y]

Exploremos cómo esto falla.

Primero, podemos ver que el modelo no hace un buen trabajo al contabilizar los datos. Las verificaciones predictivas posteriores muestran que el modelo no está capturando con precisión los datos de ventas antes de la introducción del nuevo producto.

fig, ax = plt.subplots()
model2.plot_fit(ax=ax)
ax.legend(loc="lower center", bbox_to_anchor=(0.5, -0.25), ncol=4);

Igualmente, las predicciones contrafactuales son deficientes en el sentido de que no representan lo que esperaríamos ver. Es decir, esperaríamos que los números de ventas relativamente constantes continuaran como estaban después de la introducción del nuevo producto.

fig, ax = plt.subplots()
model2.plot_counterfactual(ax=ax)
ax.legend(loc="lower center", bbox_to_anchor=(0.5, -0.25), ncol=4);

Si examinamos los parámetros (en el caso del mercado no saturado examinamos beta_all, no beta), podemos ver que el modelo atribuye una reducción casi nula en los productos de la empresa competidora existente y de la propia empresa. No hay reducción, por supuesto, los números de ventas en realidad aumentaron a pesar de que la cuota de mercado disminuyó.

az.plot_posterior(model2.idata, var_names=["beta_all"], textsize=11, figsize=(12, 3));
%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

matplotlib    : 3.10.0
numpy         : 1.26.4
pymc_marketing: 0.10.0
arviz         : 0.20.0

Watermark: 2.5.0