Cómo proteger la previsión de la demanda con gráficos causales | por Ryan O’Sullivan | Junio ​​de 2024

IA causal: exploración de la integración del razonamiento causal en el aprendizaje automático

11 minutos de lectura

hace 13 horas

Foto por Boris Dunand en Dejar de salpicar

Bienvenidos a mi serie sobre IA causal, en la que exploraremos la integración del razonamiento causal en los modelos de aprendizaje automático. Se espera que exploremos una serie de aplicaciones prácticas en diferentes contextos comerciales.

En el último artículo cubrimos Mejorando el modelado de la combinación de marketing con Causal AI. En este artículo pasaremos a salvaguardar la previsión de la demanda con gráficos causales.

Si se perdió el último artículo sobre modelos de marketing mix, consúltelo aquí:

En este artículo profundizaremos en cómo salvaguardar la previsión de la demanda (o cualquier caso de uso de previsión, para ser honesto) con gráficos causales.

Se explorarán las siguientes áreas:

  • Un pronóstico rápido 101.
  • ¿Qué es la previsión de la demanda?
  • Un repaso sobre los gráficos causales.
  • ¿Cómo pueden los gráficos causales salvaguardar la previsión de la demanda?
  • Un estudio de caso de Python que ilustra cómo los gráficos causales pueden proteger sus pronósticos de correlaciones falsas.

El cuaderno completo se puede encontrar aquí:

Pronóstico 101

La previsión de series temporales implica predecir valores futuros basándose en observaciones históricas.

Imagen generada por el usuario

Para empezar, hay una serie de términos con los que vale la pena familiarizarse:

  1. Autocorrelación — La correlación de una serie con sus valores anteriores en diferentes desfases temporales. Ayuda a identificar si hay una tendencia presente.
  2. Estacionario — Esto ocurre cuando las propiedades estadísticas de una serie son constantes a lo largo del tiempo (por ejemplo, media, varianza). Algunos métodos de pronóstico suponen estacionariedad.
  3. Diferenciación — Esto es cuando restamos la observación anterior de la observación actual para transformar una serie no estacionaria en una estacionaria. Un paso importante para los modelos que suponen estacionariedad.
  4. Estacionalidad — Un ciclo regular que se repite y ocurre a intervalos fijos (por ejemplo, diariamente, semanalmente, anualmente).
  5. Tendencia — El movimiento a largo plazo en una serie.
  6. Retraso — El número de pasos de tiempo entre una observación y un valor anterior.
  7. Derechos residuales de autor — La diferencia entre los valores previstos y los reales.
  8. Media móvil — Se utiliza para suavizar las fluctuaciones de corto plazo promediando un número fijo de observaciones pasadas.
  9. Suavizado exponencial —Se aplican ponderaciones a observaciones pasadas, con mayor énfasis en los valores recientes.
  10. Descomposición estacional — Aquí es cuando separamos una serie temporal en componentes estacionales, de tendencia y residuales.
Imagen generada por el usuario

Existen varios métodos diferentes que se pueden utilizar para realizar pronósticos:

  • ETS (Error, Tendencia, Estacional) — Un método de suavizado exponencial que modela componentes de error, tendencia y estacionalidad.
  • Modelos autorregresivos (modelos AR) — Modela el valor actual de la serie como una combinación lineal de sus valores anteriores.
  • Modelos de media móvil (modelos MA) — Modela el valor actual de la serie como una combinación lineal de errores de pronóstico pasados.
  • Modelos ARIMA (promedio móvil integrado autorregresivo) — Combina los modelos AR y MA con la incorporación de diferenciador para hacer la serie estacionaria.
  • Modelos de espacio de estados — Deconstruye la serie temporal en componentes individuales como tendencia y estacionalidad.
  • Modelos jerárquicos — Un método que maneja datos estructurados en una jerarquía, como regiones.
  • Regresión lineal — Utiliza una o más variables independientes (características) para predecir la variable dependiente (objetivo).
  • Aprendizaje automático (ML) — Utiliza algoritmos más flexibles, como el impulso, para capturar relaciones complejas.

Si desea profundizar más en este tema, le recomiendo el siguiente recurso, que es bien conocido como la guía de consulta para realizar pronósticos (la versión a continuación es gratuita 😀):

En términos de aplicar algunos de los modelos de pronóstico usando Python, recomendaría explorar Nixtla, que tiene una extensa lista de modelos implementados y una API fácil de usar:

Previsión de la demanda

Predecir la demanda de su producto es importante.

  • Puede ayudarle a gestionar su inventario, evitando el exceso o la falta de existencias.
  • Puede mantener satisfechos a sus clientes, garantizando que los productos estén disponibles cuando los deseen.
  • Reducir los costos de almacenamiento y minimizar el desperdicio es rentable.
  • Esencial para la planificación estratégica.

Mantener los pronósticos de demanda precisos es esencial. En la siguiente sección, comencemos a pensar en cómo los gráficos causales podrían salvaguardar nuestros pronósticos…

Actualización de gráficos causales

He cubierto los gráficos causales algunas veces en mi serie, pero en caso de que necesites un repaso, consulta mi primer artículo donde lo cubro en detalle:

¿Cómo pueden los gráficos causales salvaguardar la previsión de la demanda?

Tomando el siguiente gráfico como ejemplo, digamos que queremos pronosticar nuestra variable objetivo. Descubrimos que tenemos 3 variables que están correlacionadas con él, por lo que las usamos como características. ¿Por qué sería un problema incluir la correlación espuria? Cuantas más funciones incluyamos, mejor será nuestro pronóstico, ¿verdad?

Imagen generada por el usuario

Bueno en realidad no….

Cuando se trata de pronosticar la demanda, uno de los principales problemas es la deriva de los datos. La deriva de datos en sí misma no es un problema si la relación entre la característica de interés y el objetivo permanece constante. Pero cuando la relación no se mantiene constante, la precisión de nuestros pronósticos se deteriorará.

Pero, ¿cómo nos va a ayudar un gráfico causal? La idea es que es mucho más probable que las correlaciones falsas se desvíen, y es mucho más probable que causen problemas cuando lo hagan.

¿No estás convencido? ¡Bien, es hora de pasar al estudio de caso!

Fondo

Tu amigo ha comprado un camión de helados. Le ha pagado mucho dinero a un consultor para que le construyera un modelo de previsión de la demanda. Funcionó muy bien durante los primeros meses, pero en los últimos meses tu amigo ha tenido escasez de helados. Recuerda que tu puesto de trabajo era “datos o algo así” y te pide consejo.

Creación de los datos del estudio de caso

Permítanme comenzar explicando cómo creé los datos para este estudio de caso. Creé un gráfico causal simple con las siguientes características:

  1. La venta de helados es el nodo objetivo (X0)
  2. Las visitas costeras son causa directa de la venta de helados (X1)
  3. La temperatura es una causa indirecta de las ventas de helados (X2)
  4. Los ataques de tiburones son una correlación espuria (X3)
Imagen generada por el usuario

Luego utilicé el siguiente proceso de generación de datos:

Imagen generada por el usuario

Puede ver que cada nodo está influenciado por valores pasados ​​de sí mismo y un término de ruido, así como por sus padres directos. Para crear los datos utilizo un módulo útil del paquete de Python de análisis causal de series temporales Tigramite:

Tigramite es un gran paquete, pero esta vez no lo cubriré en detalle porque merece su propio artículo. A continuación utilizamos el módulo estructural_causal_process siguiendo el proceso de generación de datos anterior:

seed=42
np.random.seed(seed)

# create node lookup for channels
node_lookup = {0: 'ice cream sales',
1: 'coastal visits',
2: 'temperature',
3: 'shark attacks',
}

# data generating process
def lin_f(x):
return x

links_coeffs = {0: [((0, -1), 0.2, lin_f), ((1, -1), 0.9, lin_f)],
1: [((1, -1), 0.5, lin_f), ((2, -1), 1.2, lin_f)],
2: [((2, -1), 0.7, lin_f)],
3: [((3, -1), 0.2, lin_f), ((2, -1), 1.8, lin_f) ],
}

# time series length
T = 1000

data, _ = toys.structural_causal_process(links_coeffs, T=T, seed=seed)
T, N = data.shape

# create var name lookup
var_names = [node_lookup[i] for i in sorted(node_lookup.keys())]

# initialize dataframe object, specify time axis and variable names
df = pp.DataFrame(data,
datatime = {0:np.arange(len(data))},
var_names=var_names)

Podemos entonces visualizar nuestra serie temporal:

tp.plot_timeseries(df)
plt.show()
Imagen generada por el usuario

Ahora que comprende cómo creé los datos, ¡volvamos al estudio de caso en la siguiente sección!

Comprender el proceso de generación de datos

Empiece por intentar comprender el proceso de generación de datos tomando los datos utilizados en el modelo. Hay 3 características incluidas en el modelo:

  1. Visitas costeras
  2. Temperatura
  3. Ataques de tiburones

Para comprender el gráfico causal, se utiliza PCMCI (que tiene una excelente implementación en Tigramite), un método que es adecuado para el descubrimiento de series temporales causales. No voy a tratar el PCMCI en esta ocasión, ya que necesita su propio artículo dedicado. Sin embargo, si no está familiarizado con el descubrimiento causal en general, utilice mi artículo anterior para obtener una buena introducción:

Imagen generada por el usuario

El resultado del gráfico causal de PCMCI se puede ver arriba. Saltan a la vista las siguientes cosas:

  1. Las visitas costeras son causa directa de la venta de helados
  2. La temperatura es una causa indirecta de la venta de helados
  3. Los ataques de tiburones son una correlación espuria

¡Te preguntas por qué alguien con sentido común incluiría los ataques de tiburones como una característica! Al observar la documentación, parece que el consultor usó ChatGPT para obtener una lista de características a considerar para el modelo y luego usó autoML para entrenar el modelo.

Entonces, si ChatGPT y autoML piensan que los ataques de tiburones deberían estar en el modelo, ¿seguramente no pueden estar causando ningún daño?

Preprocesamiento de los datos del estudio de caso

A continuación, veamos cómo procesé previamente los datos para que sean adecuados para este estudio de caso. Para crear nuestras funciones, necesitamos seleccionar los valores rezagados para cada columna (revise el proceso de generación de datos para comprender por qué las funciones deben ser los valores rezagados):

# create dataframne
df_pd = pd.DataFrame(df.values[0], columns=var_names)

# calcuate lagged values for each column
lag_periods = 1

for col in var_names:
df_pd[f'{col}_lag{lag_periods}'] = df_pd[col].shift(lag_periods)

# remove 1st obervations where we don't have lagged values
df_pd = df_pd.iloc[1:, :]

df_pd

Imagen generada por el usuario

Podríamos utilizar estas características rezagadas para predecir las ventas de helado, pero antes de hacerlo, introduzcamos alguna desviación de datos en la correlación espuria:

# function to introduce feature drift based on indexes
def introduce_feature_drift(df, start_idx, end_idx, drift_amount):
drift_period = (df.index >= start_idx) & (df.index <= end_idx)
df.loc[drift_period, 'shark attacks_lag1'] += np.linspace(0, drift_amount, drift_period.sum())
return df

# introduce feature drift
df_pd = introduce_feature_drift(df_pd, start_idx=500, end_idx=999, drift_amount=50.0)

# visualise drift
plt.figure(figsize=(12, 6))
sns.lineplot(data=df_pd[['shark attacks_lag1']])
plt.title('Feature Drift Over Time')
plt.xlabel('Index')
plt.ylabel('Value')
plt.legend(['shark attacks_lag1'])
plt.show()

Imagen generada por el usuario

Volvamos al estudio de caso y comprendamos lo que estamos viendo. ¿Por qué ha disminuido el número de ataques de tiburones? Investigas un poco y descubres que una de las causas de los ataques de tiburones es la cantidad de personas que practican surf. En los últimos meses se ha producido un enorme aumento en la popularidad del surf, provocando un aumento de los ataques de tiburones. Entonces, ¿cómo afectó esto a la previsión de ventas de helados?

Entrenamiento de modelos

Decide recrear el modelo utilizando las mismas características que el consultor y luego utilizando sólo las causas directas:

# use first 500 observations for training
df_train = df_pd.iloc[0:500, :]

# use last 100 observations for evaluation
df_test = df_pd.iloc[900:, :]

# set feature lists
X_causal_cols = ["ice cream sales_lag1", "coastal visits_lag1"]
X_spurious_cols = ["ice cream sales_lag1", "coastal visits_lag1", "temperature_lag1", "shark attacks_lag1"]

# create target, train and test sets
y_train = df_train['ice cream sales'].copy()
y_test = df_test['ice cream sales'].copy()
X_causal_train = df_train[X_causal_cols].copy()
X_causal_test = df_test[X_causal_cols].copy()
X_spurious_train = df_train[X_spurious_cols].copy()
X_spurious_test = df_test[X_spurious_cols].copy()

El modelo entrenado sólo con las causas directas se ve bien tanto en el conjunto de entrenamiento como en el de prueba.

# train and validate model
model_causal = RidgeCV()
model_causal = model_causal.fit(X_causal_train, y_train)
print(f'Coefficient: {model_causal.coef_}')

yhat_causal_train = model_causal.predict(X_causal_train)
yhat_causal_test = model_causal.predict(X_causal_test)

mse_train = mean_squared_error(y_train, yhat_causal_train)
mse_test = mean_squared_error(y_test, yhat_causal_test)
print(f"Mean Squared Error train: {round(mse_train, 2)}")
print(f"Mean Squared Error test: {round(mse_test, 2)}")

r2_train = r2_score(y_train, yhat_causal_train)
r2_test = r2_score(y_test, yhat_causal_test)
print(f"R2 train: {round(r2_train, 2)}")
print(f"R2 test: {round(r2_test, 2)}")

Imagen generada por el usuario

Sin embargo, cuando entrenas el modelo usando todas las características, ves que el modelo funciona bien en el conjunto de entrenamiento, pero no en el conjunto de prueba. ¡Parece que identificaste el problema!

# train and validate model
model_spurious = RidgeCV()
model_spurious = model_spurious.fit(X_spurious_train, y_train)
print(f'Coefficient: {model_spurious.coef_}')

yhat_spurious_train = model_spurious.predict(X_spurious_train)
yhat_spurious_test = model_spurious.predict(X_spurious_test)

mse_train = mean_squared_error(y_train, yhat_spurious_train)
mse_test = mean_squared_error(y_test, yhat_spurious_test)
print(f"Mean Squared Error train: {round(mse_train, 2)}")
print(f"Mean Squared Error test: {round(mse_test, 2)}")

r2_train = r2_score(y_train, yhat_spurious_train)
r2_test = r2_score(y_test, yhat_spurious_test)
print(f"R2 train: {round(r2_train, 2)}")
print(f"R2 test: {round(r2_test, 2)}")

Imagen generada por el usuario

Cuando comparamos las predicciones de ambos modelos del conjunto de prueba, ¡podemos ver por qué tu amigo ha estado escatimando en helado!

# combine results
df_comp = pd.DataFrame({
'Index': np.arange(99),
'Actual': y_test,
'Causal prediction': yhat_causal_test,
'Spurious prediction': yhat_spurious_test
})

# melt the DataFrame to long format for seaborn
df_melted = df_comp.melt(id_vars=['Index'], value_vars=['Actual', 'Causal prediction', 'Spurious prediction'], var_name='Series', value_name='Value')

# visualise results for test set
plt.figure(figsize=(12, 6))
sns.lineplot(data=df_melted, x='Index', y='Value', hue='Series')
plt.title('Actual vs Predicted')
plt.xlabel('Index')
plt.ylabel('Value')
plt.legend(title='Series')
plt.show()

Imagen generada por el usuario

Hoy exploramos lo dañino que puede ser incluir correlaciones espurias en sus modelos de pronóstico. Terminemos con algunas reflexiones finales:

  • El objetivo de este artículo fue comenzar a pensar en cómo comprender el gráfico causal puede mejorar sus pronósticos.
  • Sé que el ejemplo fue un poco exagerado (¡esperaba que el sentido común hubiera ayudado en este escenario!), pero espero que ilustre el punto.
  • Otro punto interesante a mencionar es que el coeficiente de ataques de tiburones fue negativo. Éste es otro escollo, ya que lógicamente habríamos esperado que esta correlación espuria fuera positiva.
  • La previsión de la demanda a medio y largo plazo es muy difícil: a menudo se necesita un modelo de previsión para cada característica a fin de poder prever múltiples intervalos de tiempo en el futuro. Es interesante que los gráficos causales (en concreto, los modelos causales estructurales) se presten bien a este problema.