Skip to content

Releases: jtuenner/pymatchit

Release v0.6.1: Optimal Full Matching and mahvars Caliper Fix

Choose a tag to compare

@jtuenner jtuenner released this 02 Oct 14:33
f52196d

What's New in v0.6.1

This release fixes two matching bugs. Results change for full matching, and for nearest neighbor matching that combines mahvars with a caliper; other methods are unaffected.

Changed results

  • Full matching is now optimal: it finds the subclasses with the smallest total distance between treated and control units, as R's optmatch does. A subclass is one treated unit with one or more controls, or one control with one or more treated units. Before, every unit of the smaller group was first paired with a unit of its own, which forced distant pairs when the groups overlap little: on the Lalonde data the standardized mean difference of black after matching was 1.02 and is now 0.02.
  • min_controls_per_subclass and max_controls_per_subclass are part of the optimization instead of being applied afterwards. Controls that max_controls_per_subclass leaves no room for stay unmatched, as few as possible. If there are too few controls to give every treated unit min_controls_per_subclass of its own, subclasses are merged, now with a warning.
  • Nearest neighbor matching with mahvars and a caliper applies the caliper to the propensity score. It was compared with the Mahalanobis distance instead, which left most units unmatched and allowed pairs that were far apart on the propensity score. Optimal, full and genetic matching were not affected.

Fixed

  • A caliper on the distance measure with distance="mahalanobis" (which estimates no propensity score), and invalid min_controls_per_subclass or max_controls_per_subclass values, raise an error that says what to change.

Release v0.6.0: R MatchIt Parity, Option Warnings, and Balanced CBPS

Choose a tag to compare

@jtuenner jtuenner released this 01 Oct 13:39
26c7da4

What's New in v0.6.0

This release aligns pymatchit with R MatchIt (checked against version 4.8.1) and fixes several cases where options were silently ignored. Results change for some methods; see "Changed results" below before upgrading an existing analysis.

New

  • Genetic matching supports exact, antiexact, mahvars and m_order.
  • Full matching supports antiexact and covariate-specific calipers (caliper={"age": 2}).
  • Cardinality matching supports exact (each stratum is solved separately), ratio and mahvars:
    • ratio=k selects the largest balanced sample with k controls per treated unit. Leaving ratio unset keeps profile matching, where the whole treated group is retained.
    • mahvars pairs the selected units, and matches() returns the pairs.
  • GLM links cloglog and cauchit, plus linear.probit. linear is accepted as an alias for linear.logit.
  • Optimal matching supports antiexact and mahvars.
  • estimand="ATC" is implemented for all methods. For nearest, optimal and genetic matching the control group becomes the focal group, and matches() is keyed by control unit.

Unused options now warn

An option the chosen method cannot use (for example caliper with method="subclass") is ignored with a warning that names the option, as in R. Before, most were ignored silently. This lets you switch method without rewriting the call. To turn these warnings into errors, use warnings.simplefilter("error").

Contradictory requests raise an error: estimand="ATE" with nearest, optimal or genetic matching; an unknown link; m_order="largest" without a propensity score; mahvars with cardinality matching but no ratio.

Changed results

  • link: logit and probit now match on the predicted probability, as in R. Use link="linear.logit" for the previous behaviour (matching on the linear predictor). Calipers are in standard deviations of this measure, so their width changes too.
  • User-supplied distances (distance= an array) are used as-is; they are no longer logit-transformed.
  • Nearest matching with ratio > 1 assigns matches in rounds: every unit gets a first match before any gets a second. Control weights are 1/k per match, where k is the number of matches the treated unit actually received.
  • Calipers with exact= use the full-sample standard deviation, not each stratum's.
  • Matching order: m_order defaults to largest when a propensity score is available (smallest for ATC) and to row order otherwise. mahvars matching is ordered by the propensity score.
  • Genetic matching uses a weighted distance on standardized variables, including the propensity score, and optimizes the matching it actually returns.
  • CBPS is the just-identified estimator, which balances the covariates exactly, and follows estimand.
  • Cardinality matching is solved exactly as a mixed-integer program. std_tols uses the same standardization as summary().
  • Full matching seeds subclasses with an optimal 1:1 assignment and enforces calipers and min/max_controls_per_subclass.
  • Subclassification bins on the focal group's scores (treated for ATT, control for ATC, everyone for ATE).
  • CEM bins numeric covariates with Sturges' rule by default instead of 5 bins.
  • KS statistics for the matched sample use the matching weights.

Fixed

  • Optimal matching crashed when a caliper excluded pairs.
  • Genetic matching with a caliper crashed on non-default indexes and dropped most treated units when matching without replacement.
  • m_order="random" reseeded NumPy's global random state.
  • matches() returned missing values for string-labelled indexes.

Signature

  • ratio defaults to None (1:1 for pair matching; profile matching for cardinality). It was 1.
  • m_order defaults to None. It was "largest".

Development

  • Tests run on Python 3.9–3.14 and on the oldest and newest supported pandas. The code is formatted with black.

Thanks to Nick Eubank (@nickeubank) for the bug review and the pull requests behind most of this release, and to @codeteme for the report on control weights.

Release v0.5.0: Advanced Matching Algorithms, Estimators, and Visual Diagnostics

Choose a tag to compare

@jtuenner jtuenner released this 04 Mar 10:19

What's New in v0.5.0

This release significantly expands pymatchit-causal's capabilities, adding major new matching algorithms, additional machine learning distance estimators, and a completely overhauled visual diagnostic suite.

✨ New Matching Algorithms

  • Full Matching (method='full'): Optimal subclassification that minimizes globally calculated total distance within subclasses.
  • Genetic Matching (method='genetic'): Uses an evolutionary algorithm to find optimal covariate weights that maximize balance prior to matching.
  • Cardinality Matching (method='cardinality'): Finds the largest possible subset of the data where treated and control groups satisfy user-specified balance constraints.

🚀 Expanded Distance Metrics

In addition to GLM and Mahalanobis distance, we now officially support and document:

  • CBPS (Covariate Balancing Propensity Score): Jointly optimizes prediction accuracy and covariate balance.
  • New ML Estimators: Added support for decisiontree, adaboost, and penalized approaches (lasso, ridge, elasticnet).
  • User-Supplied Scores: You can now pass a pre-computed numpy array or pandas Series directly into the distance argument.

📊 Publication-Ready Diagnostics

  • Jitter Plots (m.plot(type='jitter')): Intuitively verify propensity score distributions and overlap across matched vs. unmatched units.
  • QQ Plots (m.plot(type='qq')): Compare quantile distributions of treated and control groups covariate by covariate.
  • Visual Cohesion: Overhauled the plotting engine. Love plots, ECDF plots, Propensity densities, and Jitter plots now share a consistent, modern visual aesthetic right out of the box.

Release v0.2.0: Optimal Matching, Covariate Calipers & Subclass Inference

Choose a tag to compare

@jtuenner jtuenner released this 02 Mar 10:39

Release v0.2.0: Optimal Matching, Covariate Calipers & Subclass Inference

We are thrilled to announce a major update to pymatchit-causal! This release brings significant new matching algorithms, critical tools for downstream statistical inference, massive performance optimizations, and a visual overhaul of our diagnostic plots.

Thank you to the community for the excellent GitHub issues and feedback that drove this release.

🚀 New Features

  • Optimal Matching (method='optimal'): You can now perform globally optimal matching. Unlike greedy nearest-neighbor matching, optimal matching minimizes the total absolute distance across all matched pairs in the dataset, ensuring the best possible overall balance.
  • Covariate-Specific Calipers: The caliper argument now accepts a dictionary, allowing you to set strict, independent thresholds for specific variables alongside the propensity score. Example: caliper={'distance': 0.1, 'age': 2} ensures matches are within 0.1 standard deviations of the propensity score and strictly within 2 years of age.
  • Subclass Identifiers for Downstream Inference: The matched_data dataframe now automatically exports a subclass column. This assigns a unique integer ID to each matched pair or stratum, which is essential for calculating cluster-robust standard errors in your final effect estimation models (mirroring the behavior of the R MatchIt package).
  • Matching Order Control (m_order): Added the ability to dictate the sequence of greedy matching. You can now specify m_order="largest", "smallest", or "random".

⚡ Performance & Visual Upgrades

  • $O(N)$ Nearest Neighbor Matching: Completely refactored the underlying KD-Tree logic for Nearest Neighbor matching without replacement. The algorithm now fits the tree globally once (rather than per-unit), eliminating infinite hangs on large datasets and matching thousands of rows in milliseconds.
  • Publication-Ready "Nature-Style" Love Plots: The love_plot has received a massive visual upgrade. It now features a clean, minimalist aesthetic (no heavy gridlines, optimized data-ink ratio, and improved scatter rendering) specifically designed to look beautiful in high-impact scientific journals.

🐛 Bug Fixes & Stability

  • Categorical/String Variable Diagnostics: Fixed a critical bug where running .summary() or .plot() on formulas containing string variables would cause a TypeError. Categorical variables are now safely routed through Patsy to generate properly evaluated dummy matrices (e.g., race[T.Hispanic]).
  • Fractional Weight Variance: Fixed an issue in diagnostics.py that occasionally produced zero or negative variances when calculating weighted statistics for ratios > 1. It now uses the robust unbiased reliability weights variance formula.
  • Caliper Scaling: Fixed the caliper threshold calculation to properly scale based on the global standard deviation of the distance measure, preventing overly restrictive drops.
  • eCDF Guardrails: Added explicit data-type validation to ecdf_plot. Attempting to plot a categorical variable will now gracefully raise an informative TypeError instead of causing a cascading Seaborn crash.
  • NaN Detection: Added explicit, clean guardrails to catch missing values (NaNs) in covariates before they hit the formula parser, providing users with clear instructions to impute or drop missing data.
  • Pandas Int64 Casting: Fixed an issue where casting index columns to Pandas nullable integers would silently fail when exporting matches in wide format.

*** To upgrade:

pip install --upgrade pymatchit-causal

pymatchit-causal: Propensity Matching for Python based on the R MatchIt package.

Choose a tag to compare

@jtuenner jtuenner released this 06 Dec 19:25

Summary

pymatchit is a Python library designed to preprocess observational data for causal inference by balancing covariates between treated and control groups. It implements Propensity Score Matching (PSM) and other matching techniques to reduce selection bias, mirroring the workflow of the widely standard R package MatchIt [MatchIt:2011]. The library provides a familiar formula-based API (e.g., treat ~ age + educ) while leveraging the speed and ecosystem of Python's scikit-learn and pandas. The package is distributed via PyPI as pymatchit-causal.

Statement of Need

In observational studies, estimating the causal effect of a treatment (e.g., a job training program) on an outcome (e.g., wages) is challenging because treated subjects often differ systematically from control subjects. Propensity Score Matching is a common technique to address this by constructing a "synthetic" control group that statistically resembles the treated group [Rosenbaum:1983].

While R users have long relied on MatchIt for this task, the Python ecosystem has lacked a dedicated, unified library for matching that offers:

  1. R-style Formula Syntax: Allowing users to define causal relationships clearly and concisely.
  2. Diverse Distance Metrics: Beyond simple logistic regression, pymatchit supports modern machine learning estimators (Random Forest, Gradient Boosting, Neural Networks) via scikit-learn for robust propensity score estimation.
  3. Standardized Diagnostics: Generating "Love plots" (standardized mean difference plots) and Empirical Cumulative Distribution Function (ECDF) plots is critical for verifying balance but often requires writing extensive custom boilerplate code in Python.

pymatchit fills this gap, enabling data scientists to perform rigorous causal matching and balance assessment entirely within the Python environment without needing to switch languages.

Functionality

The package is designed around a three-step workflow that separates design (matching) from analysis:

  1. Estimation & Matching: Users initialize a MatchIt object and fit a model using R-style formulas. The library calculates propensity scores and matches units using algorithms like Nearest Neighbor.

    from pymatchit import MatchIt, load_lalonde
    
    # Load the standard Lalonde dataset
    df = load_lalonde()
    
    # Match using Nearest Neighbor with a Caliper
    # We use Logistic Regression (GLM) for the propensity score
    m = MatchIt(df, method='nearest', distance='glm', caliper=0.2)
    m.fit("treat ~ age + educ + race + married + nodegree + re74 + re75")
  2. Diagnostics: Users can immediately visualize covariate balance to ensure the matching procedure was successful.

    # Generate Love Plot to check Standardized Mean Differences (SMD)
    m.plot(type='balance')
    
    # Check distributional balance using ECDF plots
    m.plot(type='ecdf', variable='age')
  3. Analysis: The matched dataset (with weights) can be extracted for downstream analysis.

    # Return the matched dataset with a 'weights' column
    matched_data = m.matched_data

The package currently supports Nearest Neighbor, Exact, and Subclassification matching, utilizing the scikit-learn ecosystem for underlying distance estimation.

Validation against R MatchIt

To ensure reliability, pymatchit was validated against the original R MatchIt package using the standard LaLonde dataset [Lalonde:1986]. We performed a nearest-neighbor match (ratio=1, caliper=0.2) using logistic regression for propensity score estimation in both languages.

As shown in Figure 1, the covariate balance results—measured by Standardized Mean Differences (SMD)—are nearly identical. Both implementations successfully reduced the imbalance (red circles) to below 0.1 standard deviations (blue circles) for most covariates.

Covariate Balance Comparison: Python (pymatchit) vs R (MatchIt). The reduction in Standardized Mean Difference is consistent across both implementations.love_plot

We further analyzed the specific units selected by the greedy matching algorithm (Figure 2). While there is substantial overlap (Green Xs, 89 units), there are minor discrepancies in unit selection (Orange circles vs. Blue crosses).

Unit Selection Comparison. Discrepancies occur due to floating-point precision differences in Propensity Score estimation between Python's statsmodels and R's glm.
unit_comparison

These discrepancies are expected and stem from minor floating-point differences between Python's statsmodels (used by pymatchit) and R's glm engine. In greedy nearest-neighbor matching, even a $10^{-8}$ difference in a propensity score can alter the sort order of units, causing the algorithm to pick a different "nearest" neighbor from a dense control pool. Despite these minor unit-level differences, the aggregate statistical balance achieved by pymatchit remains statistically equivalent to the R reference implementation.

Acknowledgements

This project builds upon the methodological foundations laid by the authors of the R MatchIt package: Daniel Ho, Kosuke Imai, Gary King, and Elizabeth Stuart.

If you use the package please cite:
Jonas Tünnermann
DOI

v0.1.1: pymatchit-causal. Propensity Score Matching (PSM) & Causal Inference in Python

Choose a tag to compare

@jtuenner jtuenner released this 06 Dec 11:16

pymatchit-causal is a robust Python port of the standard R package MatchIt. It brings state-of-the-art preprocessing for parametric causal inference to the Python ecosystem, allowing researchers to balance covariates in observational studies effectively and easily create Propensity Score Matching.

🚀 Key Features

  • Matching Algorithms: Support for Nearest Neighbor (Greedy), Exact Matching, Subclassification, and Coarsened Exact Matching (CEM).
  • Flexible Distance Estimation: Estimate propensity scores using GLM (Logistic Regression), Mahalanobis Distance, or Machine Learning methods (Random Forest, Gradient Boosting, Neural Networks, Lasso/Ridge/ElasticNet) via scikit-learn integration.
  • Scientific Validation: Verified against R's MatchIt package to ensure statistical consistency and correctness.
  • Publication-Ready Diagnostics: Generate Love Plots (Standardized Mean Differences), Propensity Score Density Plots, and ECDF Plots using Seaborn.
  • Robustness: Includes strict input validation, missing data handling warnings, and "Common Support" discarding logic.

📦 Installation

pip install pymatchit-causal

📚 Quick Start

from pymatchit import MatchIt, load_lalonde

# Load standard dataset
df = load_lalonde()

# Perform Nearest Neighbor Matching with Caliper
m = MatchIt(df, method='nearest', distance='glm', caliper=0.2)
m.fit("treat ~ age + educ + race + married + re74 + re75")

# Check Balance
m.plot(type='balance')

# Get Matched Data
matched_data = m.matches()

📄 Citation

If you use this software in your research, please cite:

Tünnermann, J. (2025). pymatchit-causal: Propensity Score Matching and Causal Inference in Python (Version 0.1.0) [Computer software]. https://github.com/jtuenner/pymatchit

@software{pymatchit-causal,
  author = {Tünnermann, Jonas},
  title = {pymatchit: Propensity Score Matching and Causal Inference in Python},
  url = {https://github.com/jtuenner/pymatchit},
  version = {0.1.0},
  year = {2025}
}