Correlación versus causalidad: medir el impacto real con el emparejamiento de puntajes de propensión

tarea en Ciencia de Datos, especialmente si estamos realizando un Test A/B para entender los efectos de una determinada variable sobre esos grupos.

El problema es que el mundo es simplemente… bueno, real. Quiero decir, es muy bonito pensar en un entorno controlado donde podamos aislar sólo una variable y medir su efecto. Pero lo que sucede la mayor parte del tiempo es que la vida simplemente lo atropella todo, y lo siguiente que sabes es que tu jefe te pide que compares el efecto de la última campaña en los gastos de los clientes.

Pero nunca preparaste los datos para el experimento. Todo lo que tienes son los datos actuales antes y después de la campaña.

Ingrese la coincidencia de puntaje de propensión

En términos simples, el Propensity Score Matching (PSM) es una técnica estadística que se utiliza para ver si una acción específica (un “tratamiento”) realmente causó un resultado.

Debido a que no podemos retroceder en el tiempo y ver qué habría sucedido si alguien hubiera tomado una decisión diferente, encontramos un "gemelo" en los datos, alguien que se parece casi exactamente a ellos pero que no tomó la acción de tratamiento y, en cambio, comparamos sus resultados. Encontrar estos "gemelos estadísticos" nos ayuda a comparar a los clientes de manera justa, incluso cuando no se ha realizado un experimento perfectamente aleatorio.

El problema de los promedios

Los promedios simples suponen que los grupos eran idénticos desde el principio. Cuando se compara un promedio simple de un grupo tratado con un grupo de control, se están midiendo todas las diferencias preexistentes que llevaron a las personas a elegir ese tratamiento en primer lugar.

Supongamos que queremos probar un nuevo gel energético para corredores. Si simplemente comparamos a todos los que usaron el gel con todos los que no, estamos ignorando factores importantes como los niveles de experiencia y conocimiento de los corredores. Las personas que compraron el gel podrían tener más experiencia, tener mejores zapatos o incluso entrenar más duro y estar supervisados ​​por un profesional. De todos modos, ya estaban "predispuestos" a correr más rápido.

PSM reconoce las diferencias y actúa como un explorador:

El informe de exploración: para cada corredor que usó el gel, el explorador analiza sus estadísticas: edad, años de experiencia y millas de entrenamiento promedio. Encontrar al gemelo: el explorador luego busca entre el grupo de corredores que no usaron el gel para encontrar un "gemelo" con exactamente las mismas estadísticas. La comparación: ahora compara los tiempos de finalización de estos "gemelos".

¿Te diste cuenta de que ahora estamos comparando grupos similares? Empresas de alto rendimiento frente a empresas de alto rendimiento, bajo-bajo. De esa manera, podemos aislar los otros factores que pueden causar el efecto deseado (confusión) y medir el impacto real del gel energético.

Excelente. Pasemos a aprender cómo implementar este modelo.

Paso a paso del PSM

Ahora repasaremos los pasos que debemos seguir para implementar un PSM en nuestros datos. Esto es importante, para que podamos desarrollar la intuición y aprender los pasos lógicos a seguir cuando necesitemos aplicar esto a cualquier conjunto de datos.

El primer paso es crear un modelo de regresión logística simple. Se trata de un conocido modelo de clasificación que intentará predecir cuál es la probabilidad de que el sujeto pueda estar en el grupo de tratamiento. En palabras más simples, ¿cuál es la propensión de ese individuo a realizar la acción que se estudia? Desde el paso uno, agregaremos el puntaje de propensión (probabilidad) al conjunto de datos. A continuación, utilizaremos el algoritmo de Vecinos más cercanos para escanear el grupo de control y encontrar a la persona con la puntuación más cercana a cada usuario tratado. Como "filtro de calidad", agregamos un número de umbral para la calibración. Si la coincidencia "más cercana" aún es superior a ese umbral, la descartamos. Es mejor tener una muestra perfecta y más pequeña que una grande y sesgada. Evaluamos los pares emparejados utilizando la diferencia de medias estandarizada (SMD). Sirve para comprobar si dos grupos son realmente comparables.

¡Codifiquemos entonces!

Conjunto de datos

Para este ejercicio, generaré un conjunto de datos de 1000 filas con las siguientes variables:

Edad de la persona Gastos pasados con esta empresa Un indicador binario que indica el uso de un dispositivo móvil Un indicador binario que indica si la persona vio la publicidad edad past_spend is_mobilesaw_ad 0 29 557.288206 1 1 1 45 246.829612 0 1 2 24 679.609451 0 0 3 67 1039.030017 1 1 4 20 323.241117 0 1

Puede encontrar el código que generó este conjunto de datos en el repositorio de GitHub.

Implementación de código

A continuación, implementaremos PSM usando Python. Comencemos a importar los módulos.

importar pandas como pd importar numpy como np desde sklearn.linear_model importar LogisticRegression desde sklearn.neighbors importar NearestNeighbors importar matplotlib.pyplot como plt importar seaborn como sns desde scipy importar estadísticas

Ahora podemos comenzar creando la puntuación de propensión.

Paso 1: Calcular las puntuaciones de propensión

En este paso, simplemente ejecutaremos un modelo de Regresión Logística que toma en consideración las variables edad, gasto_pasado e is_mobile y estima la probabilidad de que esta persona haya visto la publicidad.

Nuestra idea no es tener una precisión del 99% en la predicción, sino equilibrar las covariables, asegurando que los grupos tratados y de control tengan características promedio casi idénticas (como edad, gastos) de modo que cualquier diferencia en el resultado pueda atribuirse al tratamiento en lugar de diferencias preexistentes.

# Paso 1: Calcular las puntuaciones de propensión # Definir covariables y covariables de tratamiento = ['age', 'past_spend', 'is_mobile']treatment_col = 'saw_ad' # 1. Estimar puntuaciones de propensión (probabilidad de tratamiento) lr = LogisticRegression() X = df[covariables] y = df[treatment_col] # Ajustar una regresión logística lr.fit(X, y) # Almacene la probabilidad de estar en el grupo 'Tratamiento' df['pscore'] = lr.predict_proba(X)[:, 1]

Entonces, después de ajustar el modelo, dividimos los resultados de predict_proba() para devolver solo la columna con las probabilidades de estar en el grupo de tratamiento (predicción de saw_ad == 1)

Puntuación de propensión agregada al conjunto de datos. Imagen del autor.

A continuación, dividiremos los datos en control y prueba.

Control: personas que no vieron la publicidad. Tratamiento: personas que vieron la publicidad. # 2. Dividir en Tratamiento y Control tratado = df[df[tratamiento_col] == 1].copiar() control = df[df[tratamiento_col] == 0].copiar()

Es hora de encontrar los gemelos estadísticos en estos datos.

Paso 2: encontrar los pares coincidentes

En este paso, usaremos NearestNeighbors también de Scikit Learn para encontrar los pares coincidentes para nuestras observaciones. La idea es sencilla.

Tenemos dos grupos con su propensión a formar parte del grupo de tratamiento, considerando todas las variables de confusión. Entonces encontramos la observación del conjunto de datos de control que más coincide con cada una del conjunto de datos de tratamiento. Usamos pscore y edad para este partido. Podría ser solo el puntaje de propensión, pero después de observar los pares coincidentes, vi que agregar la edad nos daría una mejor coincidencia. # 3. Utilice los vecinos más cercanos para encontrar coincidencias # Usamos un 'caliper' o un umbral para garantizar que las coincidencias no estén demasiado separadas caliper = 0.05 nn = NearestNeighbors(n_neighbors=1, radio=caliper) nn.fit(control[['pscore', 'age']]) # Encuentre los pares coincidentes distancias, índices = nn.kneighbors(tratado[['pscore', 'edad']])

Ahora que tenemos los pares, podemos calibrar el modelo para descartar aquellos que no estén demasiado cerca entre sí.

Paso 3: Calibrar el modelo

Este fragmento de código filtra distancias e índices según el calibrador para identificar coincidencias válidas y luego extrae los índices Pandas originales para el control coincidente exitoso y las observaciones tratadas. Cualquier índice que supere el umbral se descarta.

Luego simplemente concatenamos ambos conjuntos de datos con las observaciones restantes que pasaron el control de calidad.

# 4. Filtrar coincidencias que están fuera de nuestro 'caliper' (control de calidad) matched_control_idx = [control.index[i[0]] para d, i en zip(distancias, índices) si d[0]<= calibre] matched_treatment_idx = [treatment.index[i] para i, d en enumerar (distancias) si d[0]<= caliper] # Combina los pares coincidentes en un nuevo marco de datos equilibrado matched_df = pd.concat([df.loc[matched_treatment_idx], df.loc[matched_control_idx]])

De acuerdo. Tenemos un conjunto de datos con pares coincidentes de clientes que vieron la publicidad y no la vieron. Y lo mejor es que ahora podemos comparar grupos similares y aislar el efecto de la campaña publicitaria.

print(matched_df.saw_ad.value_counts()) saw_ad 1 532 0 532 Nombre: recuento, tipo d: int64

Veamos si nuestro modelo dio buenas coincidencias.

Paso 4: Evaluación

Para evaluar un modelo PSM, las mejores métricas son:

Diferencia de medias estandarizada (SMD) Verifique la desviación estándar del puntaje de propensión. Visualice la superposición de datos

Comencemos comprobando las estadísticas del puntaje de propensión.

# Verifique la desviación estándar (varianza alrededor de la media) del puntaje de propensión matched_df[['pscore']].describe().T

Estadísticas de puntuación de propensión. Imagen del autor.

Estas estadísticas sugieren que nuestro proceso de emparejamiento de puntajes de propensión ha creado un conjunto de datos donde los grupos tratados y de control tienen puntajes de propensión muy similares. La pequeña desviación estándar y el rango intercuartílico concentrado (25%-75%) indican una buena superposición y equilibrio de las puntuaciones de propensión. Esta es una señal positiva de que nuestro emparejamiento fue eficaz para acercar las distribuciones de covariables entre los grupos tratados y de control.

Continuando, para comparar las medias de otras covariables como la edad y is_mobile después del emparejamiento por puntuación de propensión, podemos consultar las diferencias de medias estandarizadas (SMD). Una DME pequeña (normalmente inferior a 0,1 o 0,05) indica que las medias de la covariable están bien equilibradas entre los grupos tratados y de control, lo que sugiere un emparejamiento exitoso.

Calcularemos la métrica SMD utilizando una función personalizada que toma la media y la desviación estándar de una variable covariable determinada y calcula la métrica.

def calcular_smd(df, covariable, tratamiento_col): grupo_tratado = df[df[col_tratamiento] == 1][covariable] grupo_control = df[df[col_tratamiento] == 0][covariable] media_tratado = grupo_tratado.media() control_media = grupo_control.media() std_tratado = grupo_tratado.std() std_control = control_group.std() # Desviación estándar agrupada pooled_std = np.sqrt((std_treatment**2 + std_control**2) / 2) if pooled_std == 0: return 0 # Evite la división por cero si no hay varianza else: return (mean_treatment – mean_control) / pooled_std # Calcular SMD para cada covariable smd_results = {} para cov en covariables: smd_results[cov] = calcular_smd(matched_df, cov, tratamiento_col) smd_df = pd.DataFrame.from_dict(smd_results, orient='index', columns=['SMD']) # Interpretación de los valores SMD para índice, fila en smd_df.iterrows(): smd_value = fila['SMD'] interpretación = "bien equilibrado (excelente)" if abs(smd_value) < 0.05 else "razonablemente balanceado (bueno)" if abs(smd_value) < 0.1 else "moderadamente balanceado" if abs(smd_value) < 0.2 else "mal balanceado" print(f"La covariable '{index}' tiene un SMD de {smd_value:.4f}, lo que indica que es {interpretación}.") Edad del SMD 0,000000 past_spend 0,049338 is_mobile 0,000000 La covariable 'edad' tiene un SMD de 0,0000, lo que indica que está bien equilibrada (excelente). La covariable 'past_spend' tiene una SMD de -0,0238, lo que indica que está bien equilibrada (excelente). La covariable 'is_mobile' tiene una SMD de 0,0000, lo que indica que está bien equilibrada (excelente).

SMD <0,05 o 0,1: esto a menudo se considera bien equilibrado o excelente. La mayoría de los investigadores apuntan a una SMD inferior a 0,1 e idealmente inferior a 0,05.

¡Podemos ver que nuestras variables pasan esta prueba!

Finalmente, verifiquemos la superposición de distribuciones entre Control y Tratamiento.

# Superposiciones de distribución de control y tratamiento plt.figure(figsize=(10, 6)) sns.histplot(data=matched_df, x='past_spend', hue='saw_ad', kde=True, alpha=.4) plt.title('Distribución del gasto pasado para los grupos tratados frente a los de control') plt.xlabel('Gasto pasado') plt.ylabel('Densidad/Recuento') plt.legend(title='Vi anuncio', etiquetas=['Control (0)', 'Tratado (1)']) plt.show()

Superposición de distribuciones: Deben estar una sobre otra y tener una forma similar. Imagen del autor.

Se ve bien. Las distribuciones se superponen completamente y tienen una forma bastante similar.

Esta es una muestra de las parejas emparejadas. Puede encontrar el código para crear esto en GitHub.

Muestra del conjunto de datos de pares coincidentes. Imagen del autor.

Dicho esto, creo que podemos concluir que este modelo está funcionando correctamente y podemos pasar a comprobar los resultados.

Resultados

Bien, ya que tenemos grupos y distribuciones coincidentes, pasemos a los resultados. Comprobaremos lo siguiente:

Diferencia de medias entre los dos grupos Prueba T para comprobar la diferencia estadística D de Cohen para calcular el tamaño del efecto.

Aquí están las estadísticas del conjunto de datos coincidente.

Estadísticas sobre el conjunto de datos final. Imagen del autor.

Después de la coincidencia del puntaje de propensión, el efecto causal estimado de ver el anuncio (saw_ad) en el gasto_pasado se puede inferir de la diferencia de medias entre los grupos tratados y de control emparejados.

# Diferencia de promedios avg_past_spend_treatment = matched_df[matched_df['saw_ad'] == 1]['past_spend'].mean() avg_past_spend_control = matched_df[matched_df['saw_ad'] == 0]['past_spend'].mean() past_spend_difference = avg_past_spend_treatment – avg_past_spend_control print(f"Gasto_pasado promedio (Tratado): {avg_gasto_past_tratado:.2f}") print(f"Gasto_pasado promedio (Control): {avg_gasto_pasado_control:.2f}") print(f"Diferencia en gasto_pasado promedio: {past_spend_difference:.2f}") Promedio del gasto_pasado (Grupo tratado): 541,97 Promedio del gasto_pasado (Grupo de control): 528,14 Diferencia en el gasto_pasado promedio (Tratado – Control): 13,82

Esto indica que, en promedio, los usuarios que vieron el anuncio (tratados) gastaron aproximadamente 13,82 más que los usuarios que no vieron el anuncio (control), después de tener en cuenta las covariables observadas.

Comprobemos si la diferencia es estadísticamente significativa.

# Prueba T tratado_gasto = matched_df[matched_df['saw_ad'] == 1]['past_spend'] control_spend = matched_df[matched_df['saw_ad'] == 0]['past_spend'] t_stat, p_value = stats.ttest_ind(treatment_spend, control_spend, equal_var=False) print(f"Estadística-T: {t_stat:.3f}") print(f"Valor-P: {valor_p:.3f}") if valor_p < 0.05: print("La diferencia en el gasto_pasado entre los grupos tratados y de control es estadísticamente significativa (p < 0.05).") else: print("La diferencia en el gasto_pasado entre los grupos tratado y de control NO es estadísticamente significativa (p >= 0.05).") Estadística T: 0,805 Valor p: 0,421 La diferencia en el gasto_pasado entre los grupos tratados y de control NO es estadísticamente significativa (p >= 0,05).

La diferencia no es significativa, dado que la desviación estándar sigue siendo muy alta (~280) entre los grupos.

Realicemos también un cálculo del tamaño del efecto utilizando la D de Cohen.

# Medición del efecto D de Cohen def cohens_d(df, resultado_col, tratamiento_col): grupo_tratado = df[df[col_tratamiento] == 1][col_resultado] grupo_control = df[df[col_tratamiento] == 0][col_resultado] media1, std1 = grupo_tratado.media(), grupo_tratado.std() media2, std2 = grupo_control.media(), control_group.std() n1, n2 = len(treatment_group), len(control_group) # Desviación estándar agrupada s_pooled = np.sqrt(((n1 – 1) * std1**2 + (n2 – 1) * std2**2) / (n1 + n2 – 2)) if s_pooled == 0: return 0 # Evite la división por cero else: return (mean1 – mean2) / s_pooled # Calcular la d de Cohen para 'gasto_pasado' d_value = cohens_d(matched_df, 'past_spend', 'saw_ad') print(f"D de Cohen para el gasto_pasado: {d_value:.3f}") # Interpretar la d de Cohen si abs(d_value) < 0.2: interpretación = "efecto insignificante" elif abs(d_value) < 0,5: interpretación = "efecto pequeño" elif abs(d_value) < 0,8: interpretación = "efecto medio" else: interpretación = "efecto grande" print(f"Esto indica una {interpretación}.") D de Cohen para gasto_pasado: 0,049 Esto indica un efecto insignificante.

La diferencia es pequeña, lo que sugiere un efecto promedio insignificante del tratamiento sobre el gasto_pasado en esta muestra emparejada.

Con esto concluimos este artículo.

Antes de ir

El efecto causal es el área de la ciencia de datos que nos da las razones por las que sucede algo, además de simplemente decirnos si es probable que suceda o no.

Muchas veces, puedes enfrentarte al desafío de entender por qué algo funciona (o no) en un negocio. A las empresas les encanta eso, más aún si pueden ahorrar dinero o aumentar las ventas gracias a esa información.

Solo recuerda los pasos básicos para crear tu modelo.

Ejecute una regresión logística para calcular puntuaciones de propensión. Divida los datos en Control y Tratamiento. Ejecute Vecinos más cercanos para encontrar la combinación perfecta de los grupos de Control y Tratamiento, de modo que pueda aislar el efecto real. Evalúe su modelo usando SMD Calcule sus resultados.

Si te ha gustado este contenido, descubre más sobre mí en mi sitio web.

https://gustavorsantos.me

Repositorio GitHub

https://github.com/gurezende/Propensity-Score-Matching

Referencias

[1. Coincidencia de puntuación de propensión] (https://en.wikipedia.org/wiki/Propensity_score_matching)

[2. Una introducción detallada a la inferencia causal] (https://medium.com/data-science-collective/a-detailed-introduction-to-causal-inference-b72a70e86a87?sk=16545d9faa55f83c83f2d3792d0d135d)

[3. Documentación de Scikit-Learn de regresión logística] (https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html)

[4. Documentación de Scikit-Learn de vecinos más cercanos] (https://scikit-learn.org/0.15/modules/generated/sklearn.neighbors.NearestNeighbors.html)

[5. Guía completa sobre PSM de DataCamp] (https://www.datacamp.com/tutorial/propensity-score)

https://sites.google.com/site/econometricsacademy/econometrics-models/propensity-score-matching