Propensity Score Matching (PSM), Coarsened Exact Matching (CEM), and Causal Inference in Python. A port of R's MatchIt.
Project description
pymatchit-causal: Propensity Score Matching in Python
Scalable Causal Inference, Propensity Score Matching (PSM), and Coarsened Exact Matching (CEM).
pymatchit-causal is a Python port of the standard R package MatchIt. It allows data scientists to preprocess data for causal inference by balancing covariates between treated and control groups using state-of-the-art matching methods.
Why use pymatchit?
If you are looking for Propensity Score Matching in Python, this library provides a robust, "R-style" workflow including:
- Propensity Score Estimation: Logistic Regression (GLM), Random Forest, GBM, Neural Networks.
- Matching Algorithms: Nearest Neighbor (Greedy), Exact, Subclassification, and Coarsened Exact Matching (CEM).
- Diagnostics: Publication-ready Love Plots (Covariate Balance), Propensity Density Plots, and ECDF plots.
Features
- Matching Methods: Nearest Neighbor, Exact, Coarsened Exact Matching (CEM), Subclassification.
- Distance Metrics: Logistic Regression (GLM), Mahalanobis, Random Forest, GBM, Neural Networks, etc.
- Diagnostics: Love Plots, ECDF Plots, Propensity Score Density Plots, and Summary Tables (SMD, Variance Ratios).
- Parity: Designed to mirror the R
MatchItAPI (matchit(formula, data, method=...)).
Table of Contents
Installation
pip install pymatchit-causal
Dependencies: numpy, pandas, scipy, statsmodels, matplotlib, scikit-learn, seaborn, patsy.
Example Workflow (Excel Data)
This example demonstrates a full analysis pipeline: loading data from Excel, configuring matching, assessing balance, and extracting the matched dataset.
Scenario: You have an Excel file healthcare_data.xlsx with a binary treatment variable took_drug, an outcome recovery_time, and confounders like age, severity, and income.
1. Load Data
import pandas as pd
from pymatchit import MatchIt
# Load your dataset
df = pd.read_excel("healthcare_data.xlsx")
# Preview data
# Columns: [patient_id, took_drug, recovery_time, age, severity, income, gender]
print(df.head())
2. Initialize and Match
We will use Nearest Neighbor matching using a Random Forest to estimate the propensity score, applying a caliper to ensure good matches.
# Initialize the matching model
m = MatchIt(
data=df,
method='nearest', # 1:1 Nearest Neighbor matching
distance='randomforest', # Use Random Forest for Propensity Scores
distance_options={'n_estimators': 500}, # Pass kwargs to sklearn
caliper=0.2, # Drop matches > 0.2 std devs apart
replace=False, # Match without replacement
random_state=42 # Reproducibility
)
# Fit the model using an R-style formula
# Format: treatment_variable ~ covariate1 + covariate2 + ...
m.fit("took_drug ~ age + severity + income + gender")
3. Assess Balance (Diagnostics)
Before analyzing the outcome, verify that the treatment and control groups are balanced.
# 1. Statistical Summary
# Check Standardized Mean Differences (SMD) and Variance Ratios
summary = m.summary()
# 2. Visual Inspection: Love Plot
# Displays covariate balance before (red) and after (blue) matching.
# Ideally, all blue dots should be close to 0.
m.plot(type='balance', threshold=0.1)
# 3. Visual Inspection: Propensity Overlap
# Check if treated and control groups share common support
m.plot(type='propensity')
# 4. Visual Inspection: ECDF Plot
# Check distributional balance for continuous variables (e.g., age)
m.plot(type='ecdf', variable='age')
4. Extract Matched Data
If balance is satisfactory, extract the data for analysis.
# Get the final dataset containing only matched units (with weights)
matched_df = m.matches(format='long')
# Merge back with original data to get outcomes if needed,
# or use m.matched_data which retains the original columns + weights.
final_analysis_set = m.matched_data
print(final_analysis_set.head())
# Now you can perform a weighted regression or T-test on 'recovery_time'
API Reference
The MatchIt Class
class MatchIt(
data: pd.DataFrame,
method: str = "nearest",
distance: str = "glm",
link: str = "logit",
replace: bool = False,
caliper: float = None,
ratio: int = 1,
estimand: str = "ATT",
exact: Union[str, List[str]] = None,
subclass: int = 6,
discard: str = "none",
cutpoints: Dict = None,
distance_options: Dict = None,
random_state: int = None
)
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
data |
pd.DataFrame |
Required | The input dataset containing treatment, outcome, and covariates. |
method |
str |
"nearest" |
The matching algorithm to use. • nearest: Nearest Neighbor (Greedy) matching. • exact: Exact matching on all covariates. • subclass: Subclassification (Stratification). • cem: Coarsened Exact Matching. |
distance |
str |
"glm" |
The method used to estimate propensity scores or distance. • glm: Logistic Regression (standard PSM). • mahalanobis: Mahalanobis distance (no PS estimation). • ML Methods: randomforest, gbm, neuralnet, decisiontree, adaboost, lasso, ridge, elasticnet. |
link |
str |
"logit" |
The link function for the distance measure. • logit: Log-odds (linear logit). Recommended for PSM. • linear.logit: Same as logit. • probit: Probit regression (only for GLM). • linear: Raw probabilities (0-1). |
replace |
bool |
False |
Whether to match with replacement. If True, control units can be matched to multiple treated units. |
caliper |
float |
None |
The maximum allowed distance between matches, expressed in standard deviations of the distance measure. Matches exceeding this are dropped. |
ratio |
int |
1 |
The number of control units to match to each treated unit (e.g., 2 for 1:2 matching). |
estimand |
str |
"ATT" |
The target causal estimand. • ATT: Average Treatment Effect on the Treated. • ATE: Average Treatment Effect (entire population). |
exact |
list |
None |
A list of column names (e.g. ['gender']) to enforce exact matching on. Matching will occur within strata defined by these variables. |
subclass |
int |
6 |
Number of subclasses to create when using method='subclass'. |
discard |
str |
"none" |
Logic to discard units outside common support. • none: Keep all units. • treated: Drop treated units outside control range. • control: Drop control units outside treated range. • both: Drop units from both groups outside the intersection. |
cutpoints |
dict |
None |
For method='cem'. Defines cutpoints for continuous variables. Example: {'age': 4, 'income': [0, 20k, 50k, 100k]}. |
distance_options |
dict |
None |
Keyword arguments passed directly to the underlying scikit-learn estimator (e.g., {'n_estimators': 100} for Random Forest). |
Class Methods
fit(formula: str)
Executes the matching process.
formula: A string in the formattreatment ~ cov1 + cov2 + .... The variable on the left is treated as the binary treatment indicator. Variables on the right are the covariates used for matching.
summary(print_output: bool = True)
Calculates balance statistics (Means, Standardized Mean Difference, Variance Ratios) for both matched and unmatched samples.
- Returns: A dictionary containing
unmatchedstats dataframe,matchedstats dataframe, andsample_sizes.
plot(type: str, ...)
Visualizes the matching results.
type='balance': Draws a "Love Plot" of Standardized Mean Differences.- Optional:
threshold(vertical line, e.g., 0.1),var_names(dict to rename vars for display),colors.
- Optional:
type='propensity': Plots Kernel Density Estimates (KDE) of propensity scores for treated vs. control, before and after matching.type='ecdf': Plots Empirical Cumulative Distribution Functions for a specific continuous variable.- Required:
variable(name of the column to plot).
- Required:
matches(format: str = 'long')
Retrieves the map of matched units.
format='long': Returns a DataFrame withtreated_indexandcontrol_index(one row per match).format='wide': Returns a DataFrame where each row is a treated unit and columnscontrol_1,control_2... contain indices of matched controls.
Matching Methods Details
-
Nearest Neighbor (
method='nearest'):- Greedy matching. For each treated unit, selects the closest control unit based on the distance measure.
- Supports
caliperto prune bad matches. - Supports
exactargument for stratified matching (e.g., match nearest neighbor, but only within the same 'Gender' group).
-
Exact Matching (
method='exact'):- Matches units that have identical values for all covariates in the formula.
- Often results in many discarded units if covariates are continuous.
-
Subclassification (
method='subclass'):- Divides the sample into subclasses (bins) based on propensity score quantiles.
- Weights are calculated to balance the subclasses. Robust for estimating ATE.
-
Coarsened Exact Matching (
method='cem'):- Coarsens continuous variables into bins (defined by
cutpoints) and matches exactly on these coarsened bins. - Very fast and reduces model dependence.
- Coarsens continuous variables into bins (defined by
Distance Measures
The distance parameter controls how similarity is calculated.
glm: Default. Fits a Logistic Regression (or Probit iflink='probit') to estimate propensity scores.mahalanobis: Calculates the Mahalanobis distance between units based on covariates. Note: Does not produce a propensity score property, so propensity plots will not be available unless a separate score is estimated.- Machine Learning Estimators:
randomforest: UsesRandomForestClassifier.gbm: UsesGradientBoostingClassifier.decisiontree: UsesDecisionTreeClassifier.neuralnet: UsesMLPClassifier.lasso/ridge/elasticnet: UsesLogisticRegressionwith penalties.adaboost: UsesAdaBoostClassifier.
Citation
If you use pymatchit-causal in your research, please cite it:
Tünnermann, J. (2026). pymatchit: Propensity Score Matching and Causal Inference in Python (Version 0.2.0). Zenodo. https://doi.org/10.5281/zenodo.17839522
BibTeX:
@software{pymatchit_causal,
author = {Jonas Tünnermann},
title = {pymatchit: Propensity Score Matching and Causal Inference in Python},
year = 2026,
publisher = {Zenodo},
version = {0.2.0},
doi = {10.5281/zenodo.17839522},
url = {[https://doi.org/10.5281/zenodo.17839522](https://doi.org/10.5281/zenodo.17839522)}
}
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pymatchit_causal-0.2.0.tar.gz.
File metadata
- Download URL: pymatchit_causal-0.2.0.tar.gz
- Upload date:
- Size: 38.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
55b1de12b7ee18186bf4967343edaec88c3c438594214012fa6f193f44b8b59c
|
|
| MD5 |
d9737a2bc51276106b754e30a456b00f
|
|
| BLAKE2b-256 |
ee01fd83ce9900034b090f963d2c3c9cca387a2053d2e1c46220b2320a5751b4
|
File details
Details for the file pymatchit_causal-0.2.0-py3-none-any.whl.
File metadata
- Download URL: pymatchit_causal-0.2.0-py3-none-any.whl
- Upload date:
- Size: 32.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
11a55f2f7c8598ce9658f53690ab103612e3546f9ea70367ea5c7a9053d184d2
|
|
| MD5 |
40ae382dcb5125cf4286c4d2e0e0ae92
|
|
| BLAKE2b-256 |
6c7f19151553eae8f09401761bfd72905f5a8be058ab718b54010b3c1b900127
|