activity in Information Science, particularly if we’re performing an A/B Check to know the results of a given variable over these teams.
The issue is that the world is simply… effectively, actual. I imply, it is vitally stunning to consider a managed setting the place we will isolate only one variable and measure the impact of it. However what occurs more often than not is that life simply runs over every little thing, and the following factor you already know, your boss is asking you to match the impact of the most recent marketing campaign on clients’ bills.
However you by no means ready the information for the experiment. All you could have is the continuing knowledge earlier than and after the marketing campaign.
Enter Propensity Rating Matching
In easy phrases, Propensity Rating Matching (PSM) is a statistical method used to see if a selected motion (a “therapy”) truly prompted a end result.
As a result of we will’t return in time and see what would have occurred if somebody had made a unique selection, we discover a “twin” within the knowledge, somebody who seems nearly precisely like them however didn’t take the therapy motion, and evaluate their outcomes as an alternative. Discovering these “statistical twins” helps us evaluate clients pretty, even once you haven’t run a superbly randomized experiment.
The Drawback With the Averages
Easy averages assume the teams have been an identical to start with. If you evaluate a easy common of a handled group to a management group, you might be measuring all of the pre-existing variations that led folks to decide on that therapy within the first place.
Suppose we wish to take a look at a brand new vitality gel for runners. If we simply evaluate everybody who used the gel to everybody who didn’t, we’re ignoring essential elements like the degrees of expertise and data of the runners. Individuals who purchased the gel is perhaps extra skilled, have higher footwear, and even practice tougher and be supervised by knowledgeable. They have been already “predisposed” to run quicker anyway.
PSM acknowledges the variations and acts like a scout:
- The Scouting Report: For each runner who used the gel, the scout seems at their stats: age, years of expertise, and common coaching miles.
- Discovering the Twin: The scout then seems by means of the group of runners who didn’t use the gel to discover a “twin” with the very same stats.
- The Comparability: Now, you evaluate the end occasions of those “twins.”
Did you discover how now we’re evaluating related teams? Excessive-performers vs. Excessive-performers, Low-Low. In that manner, we will isolate the opposite elements that may trigger the specified impact (confounding) and measure the true impression of the vitality gel.
Nice. Let’s transfer on to discover ways to implement this mannequin.
Step-by-Step of PSM
Now we’ll go over the steps we should take to implement a PSM in our knowledge. That is essential, so we will construct the instinct and be taught logical steps to take when we have to apply this to any dataset.
- Step one is making a easy Logistic Regression Mannequin. This can be a well-known classification mannequin that may attempt to predict what’s the likelihood that the topic might be within the therapy group. In less complicated phrases, what’s the propensity of that particular person to take the motion being studied?
- From the the first step, we’ll add the propensity rating (likelihood) to the dataset.
- Subsequent, we’ll use the Nearest Neighbors algorithm to scan the management group and discover the individual with the closest rating to every handled person.
- As a “high quality filter”, we add a threshold quantity for calibration. If the “closest” match remains to be increased than that threshold, we toss them out. It’s higher to have a smaller, good pattern than a big, biased one.
- We consider the matched pairs utilizing Standardized Imply Distinction (SMD). It’s for checking if two teams are literally comparable.
Let’s code then!
Dataset
For the aim of this train, I’ll generate a dataset of 1000 rows with the next variables:
- Age of the individual
- Previous bills with this firm
- A binary flag indicating the use of a cellular machine
- A binary flag indicating whether or not the individual noticed the promoting
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
Yow will discover the code that generated this dataset within the GitHub repository.
Code Implementation
Subsequent, we’re going to implement the PSM utilizing Python. Let’s begin importing the 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
Now, we will begin by creating the propensity rating.
Step 1: Calculating the Propensity Scores
On this step, we’ll simply run a LogisticRegression mannequin that takes into consideration the age, past_spend, and is_mobile variables and estimates the likelihood that this individual noticed the promoting.
Our concept is to not have a 99% accuracy within the prediction, however to stability the covariates, guaranteeing that the handled and management teams have practically an identical common traits (like age, bills) in order that any distinction within the consequence will be attributed to the therapy relatively than pre-existing variations.
# Step 1: Calculate the Propensity Scores
# Outline covariates and treament
covariates = ['age', 'past_spend', 'is_mobile']
treatment_col = 'saw_ad'
# 1. Estimate Propensity Scores (Chance of therapy)
lr = LogisticRegression()
X = df[covariates]
y = df[treatment_col]
# Match a Logistic Regression
lr.match(X, y)
# Retailer the likelihood of being within the 'Therapy' group
df['pscore'] = lr.predict_proba(X)[:, 1]
So, after we match the mannequin, we sliced the predict_proba() outcomes to return solely the column with the possibilities to be within the therapy group (prediction of saw_ad == 1)

Subsequent, we’ll break up the information into management and take a look at.
- Management: individuals who didn’t see the promoting.
- Therapy: individuals who noticed the promoting.
# 2. Cut up into Therapy and Management
handled = df[df[treatment_col] == 1].copy()
management = df[df[treatment_col] == 0].copy()
It’s time to discover the statistical twins on this knowledge.
Step 2: Discovering the Matching Pairs
On this step, we’ll use NearestNeighbors additionally from Scikit Study to search out the matching pairs for our observations. The concept is easy.
- We have now two teams with their propensity to be a part of the therapy group, contemplating all of the confounding variables.
- So we discover the one commentary from the management dataset that matches probably the most with each from the therapy dataset.
- We use
pscoreandagefor this match. It might be solely the propensity rating, however after wanting on the matched pairs, I noticed that including age would give us a greater match.
# 3. Use Nearest Neighbors to search out matches
# We use a 'caliper', or a threshold to make sure matches aren't too far aside
caliper = 0.05
nn = NearestNeighbors(n_neighbors=1, radius=caliper)
nn.match(management[['pscore', 'age']])
# Discover the matching pairs
distances, indices = nn.kneighbors(handled[['pscore', 'age']])
Now that now we have the pairs, we will calibrate the mannequin to discard these that aren’t too shut to one another.
Step 3: Calibrating the Mannequin
This code snippet filters distances and indices based mostly on the caliper to establish legitimate matches, then extracts the unique Pandas indices for the efficiently matched management and handled observations. Any index over the brink is discarded.
Then we simply concatenate each datasets with the remaining observations that handed the standard management.
# 4. Filter out matches which are exterior our 'caliper' (high quality management)
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]
# Mix the matched pairs into a brand new balanced dataframe
matched_df = pd.concat([df.loc[matched_treated_idx], df.loc[matched_control_idx]])
Okay. We have now a dataset with matched pairs of consumers who noticed the promoting and didn’t see it. And the very best factor is that we are actually capable of evaluate related teams and isolate the impact of the promoting marketing campaign.
print(matched_df.saw_ad.value_counts())
saw_ad
1 532
0 532
Identify: rely, dtype: int64
Let’s see if our mannequin gave good matches.
Step 4: Analysis
To judge a PSM mannequin, the very best metrics are:
- Standardized Imply Distinction (SMD)
- Test the usual deviation of the Propensity Rating.
- Visualize the information overlap
Let’s start by checking the propensity rating statistics.
# Test commonplace deviation (variance across the imply) of the Propensity Rating
matched_df[['pscore']].describe().T

These statistics counsel that our propensity rating matching course of has created a dataset the place the handled and management teams have very related propensity scores. The small commonplace deviation and the concentrated interquartile vary (25%-75%) point out good overlap and stability of propensity scores. This can be a constructive signal that our matching was efficient in bringing the distributions of covariates nearer collectively between the handled and management teams.
Transferring on, To check the technique of different covariates like age and is_mobile after Propensity Rating Matching, we will confer with the Standardized Imply Variations (SMD). A small SMD (usually under 0.1 or 0.05) signifies that the technique of the covariate are well-balanced between the handled and management teams, suggesting profitable matching.
We are going to calculate the SMD metric utilizing a customized perform that takes the imply and commonplace deviation of a given covariate variable and calculates the metric.
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.imply()
mean_control = control_group.imply()
std_treated = treated_group.std()
std_control = control_group.std()
# Pooled commonplace deviation
pooled_std = np.sqrt((std_treated**2 + std_control**2) / 2)
if pooled_std == 0:
return 0 # Keep away from division by zero if there is not any variance
else:
return (mean_treated - mean_control) / pooled_std
# Calculate SMD for every 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 (glorious)" if abs(smd_value) < 0.05 else
"moderately balanced (good)" if abs(smd_value) < 0.1 else
"reasonably balanced" if abs(smd_value) < 0.2 else
"poorly balanced"
print(f"The covariate '{index}' has an SMD of {smd_value:.4f}, indicating it's {interpretation}.")
SMD
age 0.000000
past_spend 0.049338
is_mobile 0.000000
The covariate 'age' has an SMD of 0.0000, indicating it's well-balanced (glorious).
The covariate 'past_spend' has an SMD of -0.0238, indicating it's well-balanced (glorious).
The covariate 'is_mobile' has an SMD of 0.0000, indicating it's well-balanced (glorious).
SMD < 0.05 or 0.1: That is usually thought of well-balanced or glorious stability. Most researchers intention for an SMD lower than 0.1, and ideally lower than 0.05.
We will see that our variables cross this take a look at!
Lastly, let’s examine the distributions overlay between Management and Therapy.
# Management and Therapy Distribution Overlays
plt.determine(figsize=(10, 6))
sns.histplot(knowledge=matched_df, x='past_spend', hue='saw_ad', kde=True, alpha=.4)
plt.title('Distribution of Previous Spend for Handled vs. Management Teams')
plt.xlabel('Previous Spend')
plt.ylabel('Density / Depend')
plt.legend(title='Noticed Advert', labels=['Control (0)', 'Treated (1)'])
plt.present()

It seems good. The distributions are fully overlapping and have a reasonably related form.
This can be a pattern of the matched pairs. Yow will discover the code to construct this on GitHub.

With that stated, I imagine we will conclude that this mannequin is working correctly, and we will transfer on to examine the outcomes.
Outcomes
Okay, since now we have matching teams and distributions, let’s transfer on to the outcomes. We are going to examine the next:
- Distinction of Means between the 2 teams
- T-Check to examine for statistical distinction
- Cohen’s D to calculate the impact measurement.
Listed here are the statistics of the matched dataset.

After Propensity Rating Matching, the estimated causal impact of seeing the advert (saw_ad) on past_spend will be inferred from the distinction in means between the matched handled and management teams.
# Distinction of averages
avg_past_spend_treated = matched_df[matched_df['saw_ad'] == 1]['past_spend'].imply()
avg_past_spend_control = matched_df[matched_df['saw_ad'] == 0]['past_spend'].imply()
past_spend_difference = avg_past_spend_treated - avg_past_spend_control
print(f"Common past_spend (Handled): {avg_past_spend_treated:.2f}")
print(f"Common past_spend (Management): {avg_past_spend_control:.2f}")
print(f"Distinction in common past_spend: {past_spend_difference:.2f}")
- Common
past_spend(Handled Group): 541.97 - Common
past_spend(Management Group): 528.14 - Distinction in Common
past_spend(Handled – Management): 13.82
This means that, on common, customers who noticed the advert (handled) spent roughly 13.82 greater than customers who didn’t see the advert (management), after accounting for the noticed covariates.
Let’s examine if the distinction is statistically important.
# T-Check
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 distinction in past_spend between handled and management teams is statistically important (p < 0.05).")
else:
print("The distinction in past_spend between handled and management teams is NOT statistically important (p >= 0.05).")
T-statistic: 0.805
P-value: 0.421
The distinction in past_spend between handled and management teams
is NOT statistically important (p >= 0.05).
The distinction just isn’t important, on condition that the usual deviation remains to be very excessive (~280) between teams.
Allow us to additionally run a calculation of the impact measurement utilizing Cohen’s D.
# Cohen's D Impact 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.imply(), treated_group.std()
mean2, std2 = control_group.imply(), control_group.std()
n1, n2 = len(treated_group), len(control_group)
# Pooled commonplace deviation
s_pooled = np.sqrt(((n1 - 1) * std1**2 + (n2 - 1) * std2**2) / (n1 + n2 - 2))
if s_pooled == 0:
return 0 # Keep away from 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 impact"
elif abs(d_value) < 0.5:
interpretation = "small impact"
elif abs(d_value) < 0.8:
interpretation = "medium impact"
else:
interpretation = "massive impact"
print(f"This means a {interpretation}.")
Cohen's d for past_spend: 0.049
This means a negligible impact.
The distinction is small, suggesting a negligible common therapy impact on past_spend on this matched pattern.
With that, we conclude this text.
Earlier than You Go
Causal impact is the realm of Information Science that offers us the the reason why one thing occurs, different than simply telling us if that’s possible or to not occur.
Many occasions, you might face this problem of understanding why one thing works (or not) in a enterprise. Corporations love that, much more if it might probably get monetary savings or make gross sales enhance due to that data.
Simply keep in mind the essential steps to create your mannequin.
- Run a Logistic Regression to calculate propensity scores
- Cut up the information into Management and Therapy
- Run Nearest Neighbors to search out the proper match of Management and Therapy teams, so you’ll be able to isolate the true impact.
- Consider your mannequin utilizing SMD
- Calculate your outcomes.
In the event you preferred this content material, discover out extra about me in my web site.
GitHub Repository
https://github.com/gurezende/Propensity-Rating-Matching
References
[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/steady/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)















