0ceuo0mxhsjbpwpsq.jpeg

IA causal, que explora la integración del razonamiento causal en el aprendizaje automático

Foto por Irina Inga en desempaquetar

Bienvenido a mi serie sobre IA causal, donde exploraremos la integración del razonamiento causal en modelos de aprendizaje automático. Espere explorar una serie de aplicaciones prácticas en diferentes contextos comerciales.

En el último artículo cubrimos medir la influencia causal intrínseca de sus campañas de marketing. En este artículo pasaremos a Validar el impacto causal de los controles sintéticos..

Si se perdió el último artículo sobre la influencia causal intrínseca, consúltelo aquí:

En este artículo nos centraremos en comprender el método de control sintético y explorar cómo podemos validar el impacto causal estimado.

Se cubrirán los siguientes aspectos:

  • ¿Qué es el método de control sintético?
  • ¿Qué desafío intenta superar?
  • ¿Cómo podemos validar el impacto causal estimado?
  • Un estudio de caso de Python que utiliza datos realistas de tendencias de Google y que demuestra cómo podemos validar el impacto causal estimado de los controles sintéticos.

El cuaderno completo se puede encontrar aquí:

¿Qué es?

El método de control sintético es una técnica causal que se puede utilizar para evaluar el impacto causal de una intervención o tratamiento cuando no fue posible realizar un ensayo de control aleatorio (ECA) o una prueba A/B. Fue propuesto originalmente en 2003 por Abadie y Gardezabal. El siguiente artículo incluye un excelente estudio de caso para ayudarlo a comprender el método propuesto:

https://web.stanford.edu/~jhain/Paper/JASA2010.pdf

Imagen generada por el usuario

Cubramos algunos de los conceptos básicos nosotros mismos… El método de control sintético crea una versión contrafactual de la unidad de tratamiento al crear una combinación ponderada de unidades de control que no recibieron la intervención o el tratamiento.

  • Unidad tratada: La unidad que recibe la intervención.
  • Unidades de control: Un conjunto de unidades similares que no recibieron la intervención.
  • Contrafactual: Creado como una combinación ponderada de las unidades de control. El objetivo es encontrar ponderaciones para cada unidad de control que den como resultado un contrafactual que coincida estrechamente con la unidad tratada en el período previo a la intervención.
  • Impacto causal: La diferencia entre la unidad de tratamiento postintervención y contrafactual.

Si realmente quisiéramos simplificar las cosas, podríamos pensar en ello como una regresión lineal donde cada unidad de control es una característica y la unidad de tratamiento es el objetivo. El período previo a la intervención es nuestro conjunto de trenes y utilizamos el modelo para calificar nuestro período posterior a la intervención. La diferencia entre lo real y lo previsto es el impacto causal.

A continuación se muestran un par de ejemplos para darle vida cuando podríamos considerar usarlo:

  • Cuando ejecutamos una campaña de marketing televisivo, no podemos asignar aleatoriamente la audiencia entre aquellos que pueden y los que no pueden ver la campaña. Sin embargo, podríamos seleccionar cuidadosamente una región para probar la campaña y utilizar las regiones restantes como unidades de control. Una vez que hayamos medido el efecto, la campaña podría extenderse a otras regiones. A esto se le suele denominar prueba de geoelevación.
  • Cambios de política que se introducen en algunas regiones pero no en otras. Por ejemplo, un consejo local puede implementar un cambio de política para reducir el desempleo. Otras regiones donde la política no estaba vigente podrían usarse como unidades de control.

¿Qué desafío intenta superar?

Cuando combinamos alta dimensionalidad (muchas características) con observaciones limitadas, podemos obtener un modelo que se sobreajusta.

Tomemos el ejemplo del geo-lift para ilustrar. Si utilizamos datos semanales del último año como nuestro período previo a la intervención, esto nos da 52 observaciones. Si luego decidimos probar nuestra intervención en países de Europa, ¡eso nos dará una proporción de observación a característica de 1:1!

Anteriormente hablamos sobre cómo se podría implementar el método de control sintético mediante regresión lineal. Sin embargo, la relación entre observación y características significa que es muy probable que la regresión lineal se sobreajuste, lo que dará como resultado una estimación deficiente del impacto causal en el período posterior a la intervención.

En la regresión lineal, los pesos (coeficientes) para cada característica (unidad de control) pueden ser negativos o positivos y pueden sumar un número mayor que 1. Sin embargo, el método de control sintético aprende los pesos mientras aplica las siguientes restricciones:

  • Restringir pesos para sumar 1
  • Restringir los pesos para que sean ≥ 0
Imagen generada por el usuario

Estas restricciones ayudan con la regularización y evitan la extrapolación más allá del rango de los datos observados.

Vale la pena señalar que, en términos de regularización, la regresión de Ridge y Lasso puede lograrlo y, en algunos casos, son alternativas razonables. ¡Pero probaremos esto en el estudio de caso!

¿Cómo podemos validar el impacto causal estimado?

Podría decirse que un desafío mayor es el hecho de que no podemos validar el impacto causal estimado en el período posterior a la intervención.

¿Cuánto debe durar mi periodo de preintervención? ¿Estamos seguros de que no hemos sobreajustado nuestro período previo a la intervención? ¿Cómo podemos saber si nuestro modelo se generaliza bien en el período posterior a la intervención? ¿Qué pasa si quiero probar diferentes implementaciones del método de control sintético?

Podríamos seleccionar al azar algunas observaciones del período previo a la intervención y retenerlas para su validación. ¡Pero ya hemos destacado el desafío que supone tener observaciones limitadas para que podamos empeorar aún más las cosas!

¿Qué pasaría si pudiéramos ejecutar algún tipo de simulación previa a la intervención? ¿Podría eso ayudarnos a responder algunas de las preguntas destacadas anteriormente y ganar confianza en el impacto causal estimado de nuestros modelos? ¡Todo será explicado en el estudio de caso!

Fondo

Después de convencer a Finanzas de que el marketing de marca genera un gran valor, el equipo de marketing se acerca a usted para preguntarle sobre las pruebas de elevación geográfica. Alguien de Facebook les dijo que es el próximo gran avance (aunque fue la misma persona que les dijo que Prophet era un buen modelo de pronóstico) y quieren saber si podrían usarlo para medir su nueva campaña de televisión que está por llegar.

Está un poco preocupado, ya que la última vez que realizó una prueba de elevación geográfica, el equipo de análisis de marketing pensó que era una buena idea jugar con el período previo a la intervención utilizado hasta que tuvieran un gran impacto causal.

Esta vez, usted sugiere que realicen una “simulación previa a la intervención”, después de lo cual propone que se acuerde el período de preintervención antes de que comience la prueba.

¡Así que exploremos cómo es una “simulación previa a la intervención”!

Creando los datos

Para que esto sea lo más realista posible, extraje algunos datos de tendencias de Google para la mayoría de los países de Europa. El término de búsqueda no es relevante, simplemente imagine que son las ventas de su empresa (y que opera en toda Europa).

Sin embargo, si está interesado en saber cómo obtuve los datos de tendencias de Google, consulte mi cuaderno:

A continuación podemos ver el marco de datos. Tenemos ventas durante los últimos 3 años en 50 países europeos. El equipo de marketing planea realizar su campaña televisiva en Gran Bretaña.

Imagen generada por el usuario

Ahora aquí viene la parte inteligente. Simularemos una intervención en las últimas 7 semanas de la serie temporal.

np.random.seed(1234)

# Create intervention flag
mask = (df['date'] >= "2024-04-14") & (df['date'] <= "2024-06-02")
df['intervention'] = mask.astype(int)

row_count = len(df)

# Create intervention uplift
df['uplift_perc'] = np.random.uniform(0.10, 0.20, size=row_count)
df['uplift_abs'] = round(df['uplift_perc'] * df['GB'])
df['y'] = df['GB']
df.loc[df['intervention'] == 1, 'y'] = df['GB'] + df['uplift_abs']

Ahora tracemos las ventas reales y contrafactuales en GB para darle vida a lo que hemos hecho:

def synth_plot(df, counterfactual):

plt.figure(figsize=(14, 8))
sns.set_style("white")

# Create plot
sns.lineplot(data=df, x='date', y='y', label='Actual', color='b', linewidth=2.5)
sns.lineplot(data=df, x='date', y=counterfactual, label='Counterfactual', color='r', linestyle='--', linewidth=2.5)
plt.title('Synthetic Control Method: Actual vs. Counterfactual', fontsize=24)
plt.xlabel('Date', fontsize=20)
plt.ylabel('Metric Value', fontsize=20)
plt.legend(fontsize=16)
plt.gca().xaxis.set_major_formatter(plt.matplotlib.dates.DateFormatter('%Y-%m-%d'))
plt.xticks(rotation=90)
plt.grid(True, linestyle='--', alpha=0.5)

# High the intervention point
intervention_date = '2024-04-07'
plt.axvline(pd.to_datetime(intervention_date), color='k', linestyle='--', linewidth=1)
plt.text(pd.to_datetime(intervention_date), plt.ylim()[1]*0.95, 'Intervention', color='k', fontsize=18, ha='right')

plt.tight_layout()
plt.show()

synth_plot(df, 'GB')
Imagen generada por el usuario

Ahora que hemos simulado una intervención, podemos explorar qué tan bien funcionará el método de control sintético.

Preprocesamiento

Todos los países europeos, excepto GB, están configurados como unidades de control (características). La unidad de tratamiento (objetivo) son las ventas en GB con la intervención aplicada.

# Delete the original target column so we don't use it as a feature by accident
del df['GB']

# set feature & targets
X = df.columns[1:50]
y = 'y'

Regresión

A continuación he configurado una función que podemos reutilizar con diferentes períodos previos a la intervención y diferentes modelos de regresión (por ejemplo, Ridge, Lasso):

def train_reg(df, start_index, reg_class):

df_temp = df.iloc[start_index:].copy().reset_index()

X_pre = df_temp[df_temp['intervention'] == 0][X]
y_pre = df_temp[df_temp['intervention'] == 0][y]

X_train, X_test, y_train, y_test = train_test_split(X_pre, y_pre, test_size=0.10, random_state=42)

model = reg_class
model.fit(X_train, y_train)

yhat_train = model.predict(X_train)
yhat_test = model.predict(X_test)

mse_train = mean_squared_error(y_train, yhat_train)
mse_test = mean_squared_error(y_test, yhat_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_train)
r2_test = r2_score(y_test, yhat_test)
print(f"R2 train: {round(r2_train, 2)}")
print(f"R2 test: {round(r2_test, 2)}")

df_temp['pred'] = model.predict(df_temp.loc[:, X])
df_temp['delta'] = df_temp['y'] - df_temp['pred']

pred_lift = df_temp[df_temp['intervention'] == 1]['delta'].sum()
actual_lift = df_temp[df_temp['intervention'] == 1]['uplift_abs'].sum()
abs_error_perc = abs(pred_lift - actual_lift) / actual_lift
print(f"Predicted lift: {round(pred_lift, 2)}")
print(f"Actual lift: {round(actual_lift, 2)}")
print(f"Absolute error percentage: {round(abs_error_perc, 2)}")

return df_temp, abs_error_perc

Para empezar, mantenemos las cosas simples y utilizamos la regresión lineal para estimar el impacto causal, utilizando un pequeño período previo a la intervención:

df_lin_reg_100, pred_lift_lin_reg_100 = train_reg(df, 100, LinearRegression())
Imagen generada por el usuario

Al observar los resultados, la regresión lineal no funciona muy bien. Pero esto no es sorprendente dada la relación entre observaciones y características.

synth_plot(df_lin_reg_100, 'pred')
Imagen generada por el usuario

Método de control sintético

Saltemos y veamos cómo se compara con el método de control sintético. A continuación configuré una función similar a la anterior, pero aplicando el método de control sintético usando sciPy:

def synthetic_control(weights, control_units, treated_unit):

synthetic = np.dot(control_units.values, weights)

return np.sqrt(np.sum((treated_unit - synthetic)**2))

def train_synth(df, start_index):

df_temp = df.iloc[start_index:].copy().reset_index()

X_pre = df_temp[df_temp['intervention'] == 0][X]
y_pre = df_temp[df_temp['intervention'] == 0][y]

X_train, X_test, y_train, y_test = train_test_split(X_pre, y_pre, test_size=0.10, random_state=42)

initial_weights = np.ones(len(X)) / len(X)

constraints = ({'type': 'eq', 'fun': lambda w: np.sum(w) - 1})

bounds = [(0, 1) for _ in range(len(X))]

result = minimize(synthetic_control,
initial_weights,
args=(X_train, y_train),
method='SLSQP',
bounds=bounds,
constraints=constraints,
options={'disp': False, 'maxiter': 1000, 'ftol': 1e-9},
)

optimal_weights = result.x

yhat_train = np.dot(X_train.values, optimal_weights)
yhat_test = np.dot(X_test.values, optimal_weights)

mse_train = mean_squared_error(y_train, yhat_train)
mse_test = mean_squared_error(y_test, yhat_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_train)
r2_test = r2_score(y_test, yhat_test)
print(f"R2 train: {round(r2_train, 2)}")
print(f"R2 test: {round(r2_test, 2)}")

df_temp['pred'] = np.dot(df_temp.loc[:, X].values, optimal_weights)
df_temp['delta'] = df_temp['y'] - df_temp['pred']

pred_lift = df_temp[df_temp['intervention'] == 1]['delta'].sum()
actual_lift = df_temp[df_temp['intervention'] == 1]['uplift_abs'].sum()
abs_error_perc = abs(pred_lift - actual_lift) / actual_lift
print(f"Predicted lift: {round(pred_lift, 2)}")
print(f"Actual lift: {round(actual_lift, 2)}")
print(f"Absolute error percentage: {round(abs_error_perc, 2)}")

return df_temp, abs_error_perc

Mantengo el mismo período previo a la intervención para crear una comparación justa con la regresión lineal:

df_synth_100, pred_lift_synth_100 = train_synth(df, 100)
Imagen generada por el usuario

¡Guau! ¡Seré el primero en admitir que no esperaba una mejora tan significativa!

synth_plot(df_synth_100, 'pred')
Imagen generada por el usuario

Comparación de resultados

No nos dejemos llevar todavía. A continuación realizamos algunos experimentos más que exploran tipos de modelos y períodos previos a las intervenciones:

# run regression experiments
df_lin_reg_00, pred_lift_lin_reg_00 = train_reg(df, 0, LinearRegression())
df_lin_reg_100, pred_lift_lin_reg_100 = train_reg(df, 100, LinearRegression())
df_ridge_00, pred_lift_ridge_00 = train_reg(df, 0, RidgeCV())
df_ridge_100, pred_lift_ridge_100 = train_reg(df, 100, RidgeCV())
df_lasso_00, pred_lift_lasso_00 = train_reg(df, 0, LassoCV())
df_lasso_100, pred_lift_lasso_100 = train_reg(df, 100, LassoCV())

# run synthetic control experiments
df_synth_00, pred_lift_synth_00 = train_synth(df, 0)
df_synth_100, pred_lift_synth_100 = train_synth(df, 100)

experiment_data = {
"Method": ["Linear", "Linear", "Ridge", "Ridge", "Lasso", "Lasso", "Synthetic Control", "Synthetic Control"],
"Data Size": ["Large", "Small", "Large", "Small", "Large", "Small", "Large", "Small"],
"Value": [pred_lift_lin_reg_00, pred_lift_lin_reg_100, pred_lift_ridge_00, pred_lift_ridge_100,pred_lift_lasso_00, pred_lift_lasso_100, pred_lift_synth_00, pred_lift_synth_100]
}

df_experiments = pd.DataFrame(experiment_data)

Usaremos el siguiente código para visualizar los resultados:

# Set the style
sns.set_style="whitegrid"

# Create the bar plot
plt.figure(figsize=(10, 6))
bar_plot = sns.barplot(x="Method", y="Value", hue="Data Size", data=df_experiments, palette="muted")

# Add labels and title
plt.xlabel("Method")
plt.ylabel("Absolute error percentage")
plt.title("Synthetic Controls - Comparison of Methods Across Different Data Sizes")
plt.legend(title="Data Size")

# Show the plot
plt.show()

Imagen generada por el usuario

¡Los resultados para el pequeño conjunto de datos son realmente interesantes! Como se esperaba, la regularización ayudó a mejorar las estimaciones del impacto causal. ¡El control sintético fue un paso más allá!

Los resultados del gran conjunto de datos sugieren que los períodos previos a la intervención más largos no siempre son mejores.

Sin embargo, lo que quiero que usted recuerde es lo valioso que es realizar una simulación previa a la intervención. ¡Hay tantas vías que podrías explorar con tu propio conjunto de datos!

Hoy exploramos el método de control sintético y cómo se puede validar el impacto causal. Los dejo con algunas reflexiones finales:

  • La simplicidad del método de control sintético lo convierte en una de las técnicas más utilizadas dentro de la caja de herramientas de la IA causal.
  • Desafortunadamente, también es el más utilizado: ejecutemos el paquete R CausalImpact, cambiando el período previo a la intervención hasta que veamos una mejora que nos guste. 😭
  • Aquí es donde recomiendo encarecidamente ejecutar simulaciones previas a la intervención para acordar el diseño de la prueba por adelantado.
  • El método de control sintético es un área muy investigada. Vale la pena consultar las adaptaciones propuestas SC aumentado, SC robusto y SC penalizado.

Alberto Abadie, Alexis Diamond y Jens Hainmueller (2010) Métodos de control sintético para estudios de casos comparativos: estimación del efecto del programa de control del tabaco de California, Revista de la Asociación Estadounidense de Estadística, 105:490, 493–505, DOI: 10.1198/jasa.2009 .ap08746