Matching and balancing¶
statspai.matching covers classical matching, balancing weights, diagnostics,
and Love plots behind a unified sp.match(...) dispatcher plus standalone
estimator functions for power users.
See also the decision guide: Choosing a matching estimator, and the exhaustive auto-generated listing under Full API reference -> matching.
Choosing an entry point¶
import statspai as sp
# Default nearest-neighbour propensity-score matching.
r = sp.match(
df,
y="earnings",
treat="training",
covariates=["age", "education", "earnings_pre"],
)
# Balancing-weight estimators are available through method=...
r_ebal = sp.match(df, y="earnings", treat="training",
covariates=["age", "education"], method="ebalance")
# ...or as standalone functions with estimator-specific options.
w = sp.overlap_weights(df, treat="training",
covariates=["age", "education", "earnings_pre"])
diag = sp.balance_diagnostics(df, treat="training",
covariates=["age", "education"],
weights=w.weights)
Estimator families¶
| Family | Functions | Typical use |
|---|---|---|
| Classical matching | sp.match(method="nearest" | "psm" | "mahalanobis" | "cem" | "stratify") |
Matched samples or subclassification with transparent design choices. |
| Entropy / CBPS / SBW | sp.ebalance, sp.cbps, sp.sbw |
Direct covariate balance by reweighting. |
| Genetic matching | sp.genmatch |
Automated balance search over covariate weights. |
| Overlap weights | sp.overlap_weights |
ATE-style overlap-population estimands with stable weights. |
| Diagnostics | sp.balance_diagnostics, sp.love_plot |
Standardised mean differences, variance ratios, and Love plots. |
Method-level API¶
sp.match(...)¶
match ¶
Matching estimators for observational causal inference.
Unified interface supporting orthogonal design choices:
- distance: how to measure unit similarity
'propensity'— logit propensity score (Rosenbaum & Rubin 1983)'mahalanobis'— Mahalanobis distance (Rubin 1980)'euclidean'— normalized Euclidean distance-
'exact'— exact covariate values (no approximation) -
method: how to use those distances
'nearest'— k-nearest-neighbor matching'stratify'— subclassification / stratification-
'cem'— coarsened exact matching (Iacus, King & Porro 2012) -
bias_correction: Abadie-Imbens (2011) regression adjustment for matching discrepancies in nearest-neighbor matching.
Backward compatible: method='psm', method='mahalanobis', and
method='cem' still work and map to the new parameter space.
References
Rosenbaum, P.R. and Rubin, D.B. (1983). Biometrika, 70(1), 41-55. Abadie, A. and Imbens, G.W. (2006). Econometrica, 74(1), 235-267. Abadie, A. and Imbens, G.W. (2011). JBES, 29(1), 1-11. Iacus, S.M., King, G., and Porro, G. (2012). Political Analysis, 20(1), 1-24. King, G. and Nielsen, R. (2019). Political Analysis, 27(4), 435-454. Cunningham, S. (2021). Causal Inference: The Mixtape. Yale University Press. Ch. 5: Matching and Subclassification. https://mixtape.scunning.com/ [@rosenbaum1983central]
MatchEstimator ¶
Unified matching estimator supporting multiple distance × method combinations.
match ¶
match(data: DataFrame, y: str, treat: str, covariates: List[str], *, distance: Optional[str] = None, method: str = 'nearest', estimand: str = 'ATT', n_matches: int = 1, caliper: Optional[float] = None, replace: bool = True, bias_correction: bool = False, ps_poly: int = 1, n_strata: int = 5, n_bins: Optional[int] = None, alpha: float = 0.05) -> CausalResult
Estimate treatment effect using matching.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
Input data. |
required |
y
|
str
|
Outcome variable. |
required |
treat
|
str
|
Binary treatment variable (0/1). |
required |
covariates
|
list of str
|
Variables to match on. |
required |
distance
|
str
|
Distance metric: 'propensity', 'mahalanobis', 'euclidean', 'exact'. Default is 'propensity' for method='nearest'/'stratify'. |
None
|
method
|
str
|
Matching algorithm: 'nearest', 'stratify', 'cem'. Legacy values 'psm', 'mahalanobis' also accepted. |
'nearest'
|
estimand
|
str
|
Target estimand: 'ATT' or 'ATE'. |
'ATT'
|
n_matches
|
int
|
Number of nearest-neighbor matches per unit. |
1
|
caliper
|
float
|
Maximum distance for a valid match. |
None
|
replace
|
bool
|
Match with replacement (nearest-neighbor only). |
True
|
bias_correction
|
bool
|
Apply Abadie-Imbens (2011) bias correction via regression adjustment on the matching discrepancy. |
False
|
ps_poly
|
int
|
Polynomial degree for the propensity score logit model.
|
1
|
n_strata
|
int
|
Number of strata for method='stratify'. |
5
|
n_bins
|
int
|
Number of bins per covariate for method='cem'. Default uses Sturges' rule. |
None
|
alpha
|
float
|
Significance level. |
0.05
|
Returns:
| Type | Description |
|---|---|
CausalResult
|
|
Examples:
>>> # Propensity score matching (default)
>>> result = sp.match(df, y='wage', treat='training',
... covariates=['age', 'edu', 'exp'])
>>> # Mahalanobis distance + bias correction
>>> result = sp.match(df, y='wage', treat='training',
... covariates=['age', 'edu', 'exp'],
... distance='mahalanobis', bias_correction=True)
>>> # Exact matching
>>> result = sp.match(df, y='wage', treat='training',
... covariates=['age', 'edu'],
... distance='exact')
>>> # Propensity score stratification (5 strata)
>>> result = sp.match(df, y='wage', treat='training',
... covariates=['age', 'edu', 'exp'],
... method='stratify', n_strata=5)
>>> # CEM
>>> result = sp.match(df, y='wage', treat='training',
... covariates=['age', 'edu'],
... method='cem')
>>> # Quadratic PS model (Cunningham 2021, Ch. 5 style)
>>> result = sp.match(df, y='wage', treat='training',
... covariates=['age', 'edu', 'exp'],
... ps_poly=2)
balanceplot ¶
balanceplot(result: CausalResult, threshold: float = 0.1, ax=None, figsize: tuple = (8, None), title: str = None)
Love plot: covariate balance visualization (SMD dot plot).
Displays standardized mean differences (SMD) for each covariate. The standard threshold for good balance is |SMD| < 0.1.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
result
|
CausalResult
|
Result from |
required |
threshold
|
float
|
SMD threshold lines. |
0.1
|
ax
|
matplotlib Axes
|
|
None
|
figsize
|
tuple
|
Height auto-scales with number of covariates if None. |
(8, None)
|
title
|
str
|
|
None
|
Returns:
| Type | Description |
|---|---|
(fig, ax)
|
|
psplot ¶
psplot(data: DataFrame, treat: str, covariates: List[str], *, n_bins: int = 40, ax=None, figsize: tuple = (8, 5), title: str = None, labels: tuple = ('Control', 'Treated'), colors: tuple = ('#3498DB', '#E74C3C'), trim: Optional[float] = None)
Propensity score distribution plot (common support diagnostic).
Overlays histograms of the estimated propensity score for treated and control groups, so the user can visually assess whether the common support (overlap) assumption holds.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
|
required |
treat
|
str
|
Binary treatment column. |
required |
covariates
|
list of str
|
Covariates used to estimate the propensity score. |
required |
n_bins
|
int
|
Number of histogram bins. |
40
|
ax
|
matplotlib Axes
|
|
None
|
figsize
|
tuple
|
|
(8, 5)
|
title
|
str
|
|
None
|
labels
|
tuple of str
|
Labels for (control, treated). |
('Control', 'Treated')
|
colors
|
tuple of str
|
Colors for (control, treated). |
('#3498DB', '#E74C3C')
|
trim
|
float
|
If set, draw vertical lines at (trim, 1-trim) to show the recommended trimming region. |
None
|
Returns:
| Type | Description |
|---|---|
(fig, ax)
|
|
Examples:
sp.ebalance(...)¶
ebalance ¶
Entropy Balancing (Hainmueller 2012).
Reweights the control group so that weighted covariate moments (mean, variance, skewness) exactly match the treated group, without dropping observations or relying on propensity score models.
More robust than PSM because it directly targets balance rather than modeling the selection process.
References
Hainmueller, J. (2012). "Entropy Balancing for Causal Effects: A Multivariate Reweighting Method to Produce Balanced Samples in Observational Studies." Political Analysis, 20(1), 25-46. [@hainmueller2012entropy]
ebalance ¶
ebalance(data: DataFrame, y: str, treat: str, covariates: List[str], moments: int = 1, alpha: float = 0.05) -> CausalResult
Entropy Balancing treatment effect estimator.
Reweights control units to exactly match treated covariate moments, then estimates ATT via weighted difference in means.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
|
required |
y
|
str
|
Outcome variable. |
required |
treat
|
str
|
Binary treatment indicator (0/1). |
required |
covariates
|
list of str
|
Covariates to balance on. |
required |
moments
|
int
|
Number of moments to balance: - 1: means only - 2: means and variances - 3: means, variances, and skewness |
1
|
alpha
|
float
|
|
0.05
|
Returns:
| Type | Description |
|---|---|
CausalResult
|
ATT estimate with entropy-balanced weights and balance table. |
Examples:
>>> result = sp.ebalance(df, y='outcome', treat='treated',
... covariates=['age', 'income', 'education'])
>>> print(result.summary())
>>> # Check balance improvement
>>> print(result.model_info['balance'])
Notes
Entropy balancing solves:
.. math:: \min_w \sum_i w_i \log(w_i / q_i)
subject to balance constraints (weighted moments match) and normalization (weights sum to 1).
Unlike PSM, this guarantees exact balance on specified moments without iteration or caliper tuning.
See Hainmueller (2012, Political Analysis).
sp.cbps(...)¶
cbps ¶
Covariate-Balancing Propensity Score (Imai & Ratkovic 2014).
CBPS estimates the propensity score by solving a moment condition that jointly enforces:
(a) the logit score equation (standard MLE first-order condition);
(b) exact mean-balance of covariates under the implied IPW weights.
The "just-identified" (exact) variant uses K moment conditions where
K equals the covariate dimension (drops the score equation).
The "over-identified" variant stacks both sets and solves via GMM.
This module implements both.
Mathematically, denote π(X; β) = 1 / (1 + exp(-X'β)). The
over-identified moment vector for ATE is
g_i(β) = [ (T_i - π_i) * X_i , (MLE)
(T_i - π_i) / (π_i (1 - π_i)) X_i ] (Balance)
CBPS minimises ḡ' W ḡ with W = identity for the exact case
(K equations, K unknowns → method of moments) or with the efficient GMM
weighting matrix for the over-identified case.
Treatment-effect point estimate uses the resulting weights in the standard (normalised Hajek) IPW formula; SEs come from a paired bootstrap re-estimation by default.
References
Imai, K., Ratkovic, M. (2014). "Covariate Balancing Propensity Score." JRSS-B, 76(1), 243-263. [@imai2014covariate]
Fong, C., Ratkovic, M., Imai, K. (2022). CBPS R package documentation.
cbps ¶
cbps(data: DataFrame, y: str, treat: str, covariates: List[str], estimand: Literal['ATE', 'ATT'] = 'ATE', variant: Literal['exact', 'over'] = 'over', n_bootstrap: int = 500, alpha: float = 0.05, seed: Optional[int] = None, add_intercept: bool = True, trim: float = 0.0) -> CausalResult
Covariate-Balancing Propensity Score estimator (Imai-Ratkovic 2014).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
|
required |
y
|
str
|
Outcome column. |
required |
treat
|
str
|
Binary 0/1 treatment column. |
required |
covariates
|
list of str
|
Covariates entering the logit score. |
required |
estimand
|
('ATE', 'ATT')
|
|
'ATE'
|
variant
|
('exact', 'over')
|
'exact': just-identified CBPS (only balance moments). 'over': over-identified CBPS (MLE + balance, solved via two-step GMM). |
'exact'
|
n_bootstrap
|
int
|
|
500
|
alpha
|
float
|
|
0.05
|
seed
|
int
|
|
None
|
add_intercept
|
bool
|
Prepend a constant to the covariate matrix. |
True
|
trim
|
float
|
Optional pscore clip for stability. |
0.0
|
Returns:
| Type | Description |
|---|---|
CausalResult
|
|
sp.genmatch(...)¶
genmatch ¶
Genetic Matching (Diamond & Sekhon 2013).
The user-supplied generalised Mahalanobis distance is
.. math::
d_W(x_i, x_j) = (x_i - x_j)^\top S^{-1/2}\, W\, S^{-1/2} (x_i - x_j),
where :math:S is the sample covariance of covariates and :math:W
is a diagonal weight matrix found by a genetic (evolutionary) search
that maximises the minimum across-covariate balance p-value
(Kolmogorov-Smirnov + t-tests, following the Matching R package).
Outputs
- the optimal weight vector,
- matched treated-control pair indices,
- a
balancetable of standardised mean differences pre/post match, - the ATT estimate + bootstrap SE.
References
Diamond, A. & Sekhon, J. S. (2013). "Genetic matching for estimating causal effects." Review of Economics and Statistics, 95(3), 932-945. [@diamond2013genetic]
genmatch ¶
genmatch(data: DataFrame, y: str, treat: str, covariates: Sequence[str], k: int = 1, population_size: int = 40, generations: int = 20, mutation_rate: float = 0.2, alpha: float = 0.05, random_state: int = 42) -> GenMatchResult
Genetic Matching for ATT estimation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
|
required |
y
|
str
|
|
required |
treat
|
str
|
Binary treatment indicator. |
required |
covariates
|
sequence of str
|
|
required |
k
|
int
|
Number of matches per treated unit. |
1
|
population_size
|
int
|
|
40
|
generations
|
int
|
|
20
|
mutation_rate
|
float
|
|
0.2
|
alpha
|
float
|
|
0.05
|
random_state
|
int
|
|
42
|
Returns:
| Type | Description |
|---|---|
GenMatchResult
|
|
sp.sbw(...)¶
sbw ¶
Stable Balancing Weights (Zubizarreta 2015, JASA).
Finds weights that minimise dispersion (e.g. variance, or KL divergence
from the uniform distribution) while imposing user-specified covariate
balance tolerances. Unlike entropy balancing, SBW allows approximate
balance via per-covariate tolerance δ_j, which is essential when
exact balance is infeasible or would blow up variance.
Formulation
For ATT estimation with treated group :math:\mathcal{T} and control
group :math:\mathcal{C}, solve
.. math::
\min_{w} \; \frac{1}{|\mathcal{C}|}\sum_{i \in \mathcal{C}} w_i^2
\text{s.t.} \quad
\left| \frac{1}{|\mathcal{T}|}\sum_{i \in \mathcal{T}} X_{ij}
- \sum_{i \in \mathcal{C}} w_i X_{ij} \right|
\;\le\; \delta_j \sigma_j \; \forall j,
\sum_{i \in \mathcal{C}} w_i = 1, \; w_i \ge 0.
The variance-minimising objective is equivalent to maximising effective
sample size :math:\mathrm{ESS}(w) = (\sum w_i)^2 / \sum w_i^2.
This complements :func:ebalance (exact balance, KL objective) and
:func:cbps (covariate-balancing propensity score) — together forming
the 2026 triumvirate of modern weighting estimators.
References
Zubizarreta, J.R. (2015). "Stable Weights that Balance Covariates for Estimation with Incomplete Outcome Data." Journal of the American Statistical Association, 110(511), 910-922. [@zubizarreta2015stable]
Wang, Y. and Zubizarreta, J.R. (2020). "Minimal dispersion approximately balancing weights: asymptotic properties and practical considerations." Biometrika, 107(1), 93-105. [@wang2019minimal]
SBWResult ¶
Bases: CausalResult
Stable balancing weights with a diagnostic panel.
Thin subclass of :class:CausalResult that attaches the weight
vector, effective sample size, and covariate balance table.
sbw ¶
sbw(data: DataFrame, treat: str, covariates: List[str], y: Optional[str] = None, *, estimand: str = 'att', delta: Union[float, Sequence[float]] = 0.02, objective: str = 'variance', tolerance_scale: str = 'sd', include_squares: bool = False, alpha: float = 0.05, solver_options: Optional[dict] = None) -> SBWResult
Stable Balancing Weights (Zubizarreta 2015) with optional ATT/ATE treatment-effect estimation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
|
required |
treat
|
str
|
Binary 0/1 treatment indicator column. |
required |
covariates
|
list of str
|
Columns whose means must be balanced. |
required |
y
|
str
|
Outcome column. If provided, a weighted ATT/ATE estimate with
HC-robust SE is attached to the returned :class: |
None
|
estimand
|
('att', 'ate', 'atc')
|
|
'att'
|
delta
|
float or sequence
|
Balance tolerance. With |
0.02
|
objective
|
('variance', 'entropy')
|
Dispersion objective. |
'variance'
|
tolerance_scale
|
('sd', 'raw')
|
Whether |
'sd'
|
include_squares
|
bool
|
Also balance second-moments (w_j² columns). |
False
|
alpha
|
float
|
Significance level for inference on the outcome. |
0.05
|
solver_options
|
dict
|
Passed to |
None
|
Returns:
| Type | Description |
|---|---|
SBWResult
|
|
Examples:
sp.overlap_weights(...)¶
overlap_weights ¶
Overlap weights (Li, Morgan, Zaslavsky 2018).
Overlap weights target the "average treatment effect among the overlap population" (ATO) — those observations with genuine equipoise. The weight function is
w_i = 1 - e(X_i) for treated i (T_i=1)
w_i = e(X_i) for control i (T_i=0)
which is proportional to the "tilting" that minimises the variance of
the resulting weighted treatment-effect estimator subject to exact
covariate balance on the moments used to fit e(·) (Li et al. 2018,
Theorem 1 & 3). Overlap weights:
- are bounded in [0, 1] — no extreme weights from small propensity scores, so results are stable even with poor overlap;
- exactly balance the log-odds covariates when
e(X)is a logit fit; - target ATO, not ATE, and should be interpreted accordingly.
References
Li, F., Morgan, K.L., Zaslavsky, A.M. (2018). "Balancing Covariates via Propensity Score Weighting." JASA, 113(521), 390-400.
Li, F., Thomas, L.E., Li, F. (2019). "Addressing Extreme Propensity Scores via the Overlap Weights." American Journal of Epidemiology, 188(1), 250-257.
overlap_weights ¶
overlap_weights(data: DataFrame, y: str, treat: str, covariates: List[str], estimand: str = 'ATO', n_bootstrap: int = 500, alpha: float = 0.05, seed: Optional[int] = None, trim: float = 0.0) -> CausalResult
Overlap-weight (ATO) treatment effect estimator.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
|
required |
y
|
str
|
Outcome column. |
required |
treat
|
str
|
Binary 0/1 treatment column. |
required |
covariates
|
list of str
|
Covariates for the logistic propensity-score model. |
required |
estimand
|
('ATO', 'ATE', 'ATT', 'ATC', 'matching', 'entropy')
|
Which generalized-weight scheme to use. All follow Li-Li-Li
(2019) Table 1; 'ATO' uses the overlap weights; 'matching'
uses the |
'ATO'
|
n_bootstrap
|
int
|
Paired-sample bootstrap replications for SE. |
500
|
alpha
|
float
|
|
0.05
|
seed
|
int
|
|
None
|
trim
|
float
|
Optional clip of pscore to |
0.0
|
Returns:
| Type | Description |
|---|---|
CausalResult
|
|
sp.balance_diagnostics(...)¶
balance_diagnostics ¶
balance_diagnostics(data: DataFrame, treatment: str, covariates: List[str], weights: Optional[Union[ndarray, Series, str]] = None, ps: Optional[Union[ndarray, Series, str]] = None, method: str = 'logit', threshold: float = 0.1) -> BalanceDiagnosticsResult
Unified balance diagnostics for matching and weighting estimators.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
Analysis frame. |
required |
treatment
|
str
|
Binary treatment indicator. |
required |
covariates
|
list of str
|
Covariates to audit. |
required |
weights
|
array - like or str
|
Observation weights after matching/weighting. If omitted, ATE
inverse-propensity weights are computed from |
None
|
ps
|
array - like or str
|
Propensity scores. If omitted, estimated with |
None
|
method
|
(logit, probit, gbm)
|
Propensity-score model when |
'logit'
|
threshold
|
float
|
Balance threshold for absolute standardized mean differences. |
0.1
|
Returns:
| Type | Description |
|---|---|
BalanceDiagnosticsResult
|
|
sp.love_plot(...)¶
love_plot ¶
love_plot(data: DataFrame, treatment: str, covariates: List[str], weights: Optional[Union[ndarray, Series]] = None, threshold: float = 0.1, ps_method: str = 'logit', ax=None, figsize: Tuple[float, float] = (7, None), title: str = 'Covariate Balance (Love Plot)') -> Tuple
Love plot: dot plot of standardized mean differences before/after.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
Input data. |
required |
treatment
|
str
|
Binary treatment column. |
required |
covariates
|
list of str
|
Covariate columns. |
required |
weights
|
array - like
|
IPW or matching weights. If None, inverse-PS weights are computed. |
None
|
threshold
|
float
|
SMD threshold for the vertical dashed line (default 0.1). |
0.1
|
ps_method
|
str
|
PS estimation method for balance computation. |
'logit'
|
ax
|
matplotlib Axes
|
|
None
|
figsize
|
tuple
|
(width, height). Height defaults to 0.4 * n_covariates + 1. |
(7, None)
|
title
|
str
|
Plot title. |
'Covariate Balance (Love Plot)'
|
Returns:
| Type | Description |
|---|---|
(fig, ax) : tuple
|
|