
Corrélation vs causalité : mesurer le véritable impact grâce à la correspondance des scores de propension
tâche en science des données, surtout si nous effectuons un test A/B pour comprendre les effets d’une variable donnée sur ces groupes.
Le problème est que le monde est tout simplement… eh bien, réel. Je veux dire, c’est très beau de penser à un environnement contrôlé où nous pouvons isoler une seule variable et en mesurer l’effet. Mais ce qui arrive la plupart du temps, c’est que la vie écrase tout, et l’instant d’après, votre patron vous demande de comparer l’effet de la dernière campagne sur les dépenses des clients.
Mais vous n’avez jamais préparé les données pour l’expérience. Tout ce dont vous disposez, ce sont les données continues avant et après la campagne.
Entrez la correspondance du score de propension
En termes simples, Correspondance du score de propension (PSM) est une technique statistique utilisée pour voir si une action spécifique (un « traitement ») a réellement provoqué un résultat.
Parce que nous ne pouvons pas remonter le temps et voir ce qui se serait passé si quelqu’un avait fait un choix différent, nous trouvons un « jumeau » dans les données, quelqu’un qui lui ressemble presque exactement mais qui n’a pas pris le parti. traitement action, et comparez plutôt leurs résultats. La découverte de ces « jumeaux statistiques » nous aide à comparer les clients de manière équitable, même si vous n’avez pas mené d’expérience parfaitement aléatoire.
Le problème avec les moyennes
Les moyennes simples supposent que les groupes étaient identiques au départ. Lorsque vous comparez une simple moyenne d’un groupe traité à un groupe témoin, vous mesurez toutes les différences préexistantes qui ont conduit les gens à choisir ce traitement en premier lieu.
Supposons que nous souhaitions tester un nouveau gel énergétique pour les coureurs. Si nous comparons simplement tous ceux qui ont utilisé le gel à tous ceux qui ne l’ont pas fait, nous ignorons des facteurs importants tels que le niveau d’expérience et de connaissances des coureurs. Les personnes qui ont acheté le gel peuvent être plus expérimentées, avoir de meilleures chaussures ou même s’entraîner plus dur et être encadrées par un professionnel. De toute façon, ils étaient déjà « prédisposés » à courir plus vite.
Les MSP reconnaissent les différences et agit comme un éclaireur :
- Le rapport de scoutisme : Pour chaque coureur ayant utilisé le gel, l’éclaireur examine ses statistiques : âge, années d’expérience et kilomètres d’entraînement moyens.
- Trouver le jumeau : Le recruteur examine ensuite le groupe de coureurs qui n’a pas utilisez le gel pour trouver un « jumeau » avec exactement les mêmes statistiques.
- La comparaison : Maintenant, comparez les temps d’arrivée de ces « jumeaux ».
Avez-vous remarqué que nous comparons désormais des groupes similaires ? Très performants vs très performants, faible-faible. De cette façon, nous pouvons isoler les autres facteurs pouvant provoquer l’effet recherché (confondant) et mesurer l’impact réel du gel énergétique.
Super. Passons maintenant à apprendre comment mettre en œuvre ce modèle.
Étape par étape du PSM
Nous allons maintenant passer en revue les étapes à suivre pour mettre en œuvre un PSM dans nos données. Ceci est important, afin que nous puissions développer notre intuition et apprendre les étapes logiques à suivre lorsque nous devons l’appliquer à n’importe quel ensemble de données.
- La première étape consiste à créer un modèle de régression logistique simple. Il s’agit d’un modèle de classification bien connu qui tentera de prédire quelle est la probabilité que le sujet puisse faire partie du groupe de traitement. En termes plus simples, quelle est la propension de cet individu à entreprendre l’action étudiée ?
- Dès la première étape, nous ajouterons le score de propension (probabilité) à l’ensemble de données.
- Ensuite, nous utiliserons l’algorithme des voisins les plus proches pour analyser le groupe témoin et trouver la personne avec le score le plus proche de chaque utilisateur traité.
- En tant que « filtre de qualité », nous ajoutons un nombre de seuil pour l’étalonnage. Si la correspondance « la plus proche » est toujours supérieure à ce seuil, nous les excluons. Il est préférable d’avoir un échantillon plus petit et parfait qu’un échantillon important et biaisé.
- Nous évaluons les paires appariées en utilisant Différence moyenne standardisée (DMS). Il s’agit de vérifier si deux groupes sont réellement comparables.
Codons alors !
Ensemble de données
Pour les besoins de cet exercice, je vais générer un ensemble de données de 1 000 lignes avec les variables suivantes :
- Âge de la personne
- Dépenses passées avec cette entreprise
- Un drapeau binaire indiquant le utilisation d’un appareil mobile
- Un drapeau binaire indiquant si la personne a vu la publicité
age past_spend is_mobile saw_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
Vous pouvez trouver le code qui a généré cet ensemble de données dans le Dépôt GitHub.
Implémentation du code
Ensuite, nous allons implémenter le PSM en utilisant Python. Commençons par importer les modules.
import pandas as pd
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import NearestNeighbors
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
Maintenant, nous pouvons commencer par créer le score de propension.
Étape 1 : Calculer les scores de propension
Dans cette étape, nous allons simplement exécuter un LogisticRegression modèle qui prend en compte les age, past_spendet is_mobile variables et estime la probabilité que cette personne ait vu la publicité.
Notre idée n’est pas d’avoir une précision de 99 % dans la prédiction, mais de équilibrer les covariablesen veillant à ce que les groupes traités et témoins aient des caractéristiques moyennes presque identiques (comme l’âge, les dépenses), de sorte que toute différence dans les résultats peut être attribuée au traitement plutôt qu’à des différences préexistantes.
# Step 1: Calculate the Propensity Scores
# Define covariates and treament
covariates = ['age', 'past_spend', 'is_mobile']
treatment_col = 'saw_ad'
# 1. Estimate Propensity Scores (Probability of treatment)
lr = LogisticRegression()
X = df[covariates]
y = df[treatment_col]
# Fit a Logistic Regression
lr.fit(X, y)
# Store the probability of being in the 'Treatment' group
df['pscore'] = lr.predict_proba(X)[:, 1]
Ainsi, après avoir ajusté le modèle, nous avons découpé le predict_proba() résultats pour renvoyer uniquement la colonne avec les probabilités d’être dans le groupe de traitement (prédiction de saw_ad == 1)

Ensuite, nous diviserons les données en contrôle et test.
- Contrôle: les gens qui n’ont pas vu la publicité.
- Traitement: les gens qui ont vu la publicité.
# 2. Split into Treatment and Control
treated = df[df[treatment_col] == 1].copy()
control = df[df[treatment_col] == 0].copy()
Il est temps de trouver les jumeaux statistiques dans ces données.
Étape 2 : Trouver les paires correspondantes
Dans cette étape, nous utiliserons NearestNeighbors également à partir de Scikit Learn pour trouver les paires correspondantes pour nos observations. L’idée est simple.
- Nous avons deux groupes avec leur propension à faire partie du groupe de traitement, compte tenu de toutes les variables confondantes.
- On retrouve donc le un observation de l’ensemble de données de contrôle qui correspond le plus à chacune des observations de l’ensemble de données de traitement.
- Nous utilisons
pscoreetagepour ce match. Il pourrait s’agir uniquement du score de propension, mais après avoir examiné les paires appariées, j’ai vu qu’en ajoutant l’âge, nous obtiendrions une meilleure correspondance.
# 3. Use Nearest Neighbors to find matches
# We use a 'caliper', or a threshold to ensure matches aren't too far apart
caliper = 0.05
nn = NearestNeighbors(n_neighbors=1, radius=caliper)
nn.fit(control[['pscore', 'age']])
# Find the matching pairs
distances, indices = nn.kneighbors(treated[['pscore', 'age']])
Maintenant que nous avons les paires, nous pouvons calibrer le modèle pour éliminer celles qui ne sont pas trop proches les unes des autres.
Étape 3 : Calibrer le modèle
Cet extrait de code filtre distances et indices basé sur le caliper pour identifier les correspondances valides, puis extrait les indices Pandas d’origine pour les observations de contrôle et traitées avec succès. Tout index dépassant le seuil est ignoré.
Ensuite, nous concaténons simplement les deux ensembles de données avec les observations restantes qui ont passé le contrôle qualité.
# 4. Filter out matches that are outside our 'caliper' (quality control)
matched_control_idx = [control.index[i[0]] for d, i in zip(distances, indices) if d[0] <= caliper]
matched_treated_idx = [treated.index[i] for i, d in enumerate(distances) if d[0] <= caliper]
# Combine the matched pairs into a new balanced dataframe
matched_df = pd.concat([df.loc[matched_treated_idx], df.loc[matched_control_idx]])
D’accord. Nous disposons d’un ensemble de données avec des paires de clients appariés qui ont vu la publicité et ne l’ont pas vue. Et le meilleur, c’est que nous sommes désormais en mesure de comparer des groupes similaires et d’isoler l’effet de la campagne publicitaire.
print(matched_df.saw_ad.value_counts())
saw_ad
1 532
0 532
Name: count, dtype: int64
Voyons si notre modèle a donné de bonnes correspondances.
Étape 4 : Évaluation
Pour évaluer un modèle PSM, les meilleures métriques sont :
- Différence moyenne standardisée (DMS)
- Vérifiez l’écart type du score de propension.
- Visualisez le chevauchement des données
Commençons par vérifier les statistiques du score de propension.
# Check standard deviation (variance around the mean) of the Propensity Score
matched_df[['pscore']].describe().T

Ces statistiques suggèrent que notre processus d’appariement des scores de propension a créé un ensemble de données dans lequel les groupes traités et témoins ont des scores de propension très similaires. Le petit écart type et le intervalle interquartile concentré (25 % à 75 %) indiquent un bon chevauchement et un bon équilibre des scores de propension. Il s’agit d’un signe positif indiquant que notre appariement a réussi à rapprocher les distributions de covariables entre les groupes traités et témoins.
Passons à autre chose, pour comparer les moyennes d’autres covariables comme age et is_mobile après Propensity Score Matching, on peut se référer aux différences moyennes standardisées (DMS). Un petit SMD (généralement inférieur à 0,1 ou 0,05) indique que les moyennes de la covariable sont bien équilibrées entre les groupes traités et témoins, ce qui suggère une correspondance réussie.
Nous calculerons la métrique SMD à l’aide d’une fonction personnalisée qui prend la moyenne et l’écart type d’une variable covariable donnée et calculera la métrique.
def calculate_smd(df, covariate, treatment_col):
treated_group = df[df[treatment_col] == 1][covariate]
control_group = df[df[treatment_col] == 0][covariate]
mean_treated = treated_group.mean()
mean_control = control_group.mean()
std_treated = treated_group.std()
std_control = control_group.std()
# Pooled standard deviation
pooled_std = np.sqrt((std_treated**2 + std_control**2) / 2)
if pooled_std == 0:
return 0 # Avoid division by zero if there's no variance
else:
return (mean_treated - mean_control) / pooled_std
# Calculate SMD for each covariate
smd_results = {}
for cov in covariates:
smd_results[cov] = calculate_smd(matched_df, cov, treatment_col)
smd_df = pd.DataFrame.from_dict(smd_results, orient='index', columns=['SMD'])
# Interpretation of SMD values
for index, row in smd_df.iterrows():
smd_value = row['SMD']
interpretation = "well-balanced (excellent)" if abs(smd_value) < 0.05 else \
"reasonably balanced (good)" if abs(smd_value) < 0.1 else \
"moderately balanced" if abs(smd_value) < 0.2 else \
"poorly balanced"
print(f"The covariate '{index}' has an SMD of {smd_value:.4f}, indicating it is {interpretation}.")
SMD
age 0.000000
past_spend 0.049338
is_mobile 0.000000
The covariate 'age' has an SMD of 0.0000, indicating it is well-balanced (excellent).
The covariate 'past_spend' has an SMD of -0.0238, indicating it is well-balanced (excellent).
The covariate 'is_mobile' has an SMD of 0.0000, indicating it is well-balanced (excellent).
CMS < 0,05 ou 0,1: Ceci est souvent considéré bien équilibré ou excellent équilibre. La plupart des chercheurs visent un DMS inférieur à 0,1, et idéalement inférieur à 0,05.
Nous pouvons voir que nos variables réussissent ce test !
Enfin, vérifions la superposition des distributions entre Control et Treatment.
# Control and Treatment Distribution Overlays
plt.figure(figsize=(10, 6))
sns.histplot(data=matched_df, x='past_spend', hue='saw_ad', kde=True, alpha=.4)
plt.title('Distribution of Past Spend for Treated vs. Control Groups')
plt.xlabel('Past Spend')
plt.ylabel('Density / Count')
plt.legend(title='Saw Ad', labels=['Control (0)', 'Treated (1)'])
plt.show()

Ça a l’air sympa. Les distributions se chevauchent entièrement et ont une forme assez similaire.
Ceci est un échantillon des paires appariées. Vous pouvez trouver le code pour construire ceci sur GitHub.

Cela dit, je pense que nous pouvons conclure que ce modèle fonctionne correctement et que nous pouvons passer à la vérification des résultats.
Résultats
Ok, puisque nous avons des groupes et des distributions correspondants, passons aux résultats. Nous vérifierons les éléments suivants :
- Différence de moyennes entre les deux groupes
- Test T pour vérifier la différence statistique
- Cohen’s D pour calculer la taille de l’effet.
Voici les statistiques de l’ensemble de données correspondant.

Après correspondance du score de propension, l’effet causal estimé de la visualisation de l’annonce (saw_ad) sur past_spend peut être déduit de la différence de moyenne entre les groupes traités appariés et les groupes témoins.
# Difference of averages
avg_past_spend_treated = 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_treated - avg_past_spend_control
print(f"Average past_spend (Treated): {avg_past_spend_treated:.2f}")
print(f"Average past_spend (Control): {avg_past_spend_control:.2f}")
print(f"Difference in average past_spend: {past_spend_difference:.2f}")
- Moyenne
past_spend(Groupe traité): 541,97 - Moyenne
past_spend(Groupe de contrôle): 528.14 - Différence de moyenne
past_spend(Traité – Contrôle): 13.82
Cela indique qu’en moyenne, les utilisateurs qui ont vu l’annonce (treated) ont dépensé environ 13,82 de plus que les utilisateurs qui n’ont pas vu l’annonce (control), après avoir pris en compte les covariables observées.
Vérifions si la différence est statistiquement significative.
# T-Test
treated_spend = 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(treated_spend, control_spend, equal_var=False)
print(f"T-statistic: {t_stat:.3f}")
print(f"P-value: {p_value:.3f}")
if p_value < 0.05:
print("The difference in past_spend between treated and control groups is statistically significant (p < 0.05).")
else:
print("The difference in past_spend between treated and control groups is NOT statistically significant (p >= 0.05).")
T-statistic: 0.805
P-value: 0.421
The difference in past_spend between treated and control groups
is NOT statistically significant (p >= 0.05).
La différence n’est pas significative étant donné que l’écart type reste très élevé (~280) entre les groupes.
Exécutons également un calcul de la taille de l’effet à l’aide du D de Cohen.
# Cohen's D Effect measurement
def cohens_d(df, outcome_col, treatment_col):
treated_group = df[df[treatment_col] == 1][outcome_col]
control_group = df[df[treatment_col] == 0][outcome_col]
mean1, std1 = treated_group.mean(), treated_group.std()
mean2, std2 = control_group.mean(), control_group.std()
n1, n2 = len(treated_group), len(control_group)
# Pooled standard deviation
s_pooled = np.sqrt(((n1 - 1) * std1**2 + (n2 - 1) * std2**2) / (n1 + n2 - 2))
if s_pooled == 0:
return 0 # Avoid division by zero
else:
return (mean1 - mean2) / s_pooled
# Calculate Cohen's d for 'past_spend'
d_value = cohens_d(matched_df, 'past_spend', 'saw_ad')
print(f"Cohen's d for past_spend: {d_value:.3f}")
# Interpret Cohen's d
if abs(d_value) < 0.2:
interpretation = "negligible effect"
elif abs(d_value) < 0.5:
interpretation = "small effect"
elif abs(d_value) < 0.8:
interpretation = "medium effect"
else:
interpretation = "large effect"
print(f"This indicates a {interpretation}.")
Cohen's d for past_spend: 0.049
This indicates a negligible effect.
La différence est faible, ce qui suggère un effet moyen négligeable du traitement sur past_spend dans cet échantillon apparié.
Sur ce, nous concluons cet article.
Avant de partir
L’effet causal est le domaine de la science des données qui nous donne la raisons pour lesquelles quelque chose se produit, autre que simplement nous dire si cela est probable ou non.
Il arrive souvent que vous soyez confronté au défi de comprendre pourquoi quelque chose fonctionne (ou non) dans une entreprise. Les entreprises aiment cela, encore plus si elles peuvent économiser de l’argent ou augmenter leurs ventes grâce à ces informations.
N’oubliez pas les étapes de base pour créer votre modèle.
- Exécutez une régression logistique pour calculer les scores de propension
- Divisez les données en contrôle et traitement
- Exécutez les voisins les plus proches pour trouver la correspondance parfaite entre les groupes de contrôle et de traitement, afin de pouvoir isoler l’effet réel.
- Évaluez votre modèle à l’aide de SMD
- Calculez vos résultats.
Si vous avez aimé ce contenu, découvrez-en plus sur moi sur mon site Web.
Dépôt GitHub
https://github.com/gurezende/Propensity-Score-Matching
Références
[1. Propensity Score Matching] (https://en.wikipedia.org/wiki/Propensity_score_matching)
[2. A Detailed Introduction to Causal Inference] (https://medium.com/data-science-collective/a-detailed-introduction-to-causal-inference-b72a70e86a87?sk=16545d9faa55f83c83f2d3792d0d135d)
[3. Logistic Regression Scikit-Learn Documentation] (https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html)
[4. Nearest Neighbors Scikit-Learn Documentation] (https://scikit-learn.org/0.15/modules/generated/sklearn.neighbors.NearestNeighbors.html)
[5. Complete Guide on PSM from DataCamp] (https://www.datacamp.com/tutorial/propensity-score)
https://sites.google.com/site/econometricsacademy/econometrics-models/propensity-score-matching



