Skip to main content
Loss of provenance

A previous release had provenance metadata, but this release does not. View details.

pyrollmatch

Matching and weighting for staggered treatment adoption studies in Python, built on polars and numpy.

Installation

pip install pyrollmatch

From source:

git clone https://github.com/AlanHuang99/pyrollmatch.git
cd pyrollmatch
pip install -e ".[dev]"

Quick Start

import polars as pl
from pyrollmatch import rollmatch

data = pl.read_parquet("panel_data.parquet")

result = rollmatch(
    data,
    treat="treat",
    tm="time",
    entry="entry_time",
    id="unit_id",
    covariates=["x1", "x2", "x3"],
    ps_caliper=0.2,
    num_matches=1,
)

result.balance         # covariate balance table (SMDs)
result.matched_data    # match pairs: [tm, treat_id, control_id, difference]
result.weights         # unit-level weights: [id, weight]

Methods

Nearest-Neighbor Matching (default)

Greedy nearest-neighbor matching on propensity scores or pairwise covariate distances. Supports PS calipers, per-variable calipers, matching order, and three replacement modes.

# Propensity score matching (logistic regression)
result = rollmatch(data, ..., ps_caliper=0.2, num_matches=1)

# Mahalanobis distance matching
result = rollmatch(data, ..., model_type="mahalanobis")

# Mahalanobis matching with PS caliper (MatchIt mahvars pattern)
result = rollmatch(data, ..., ps_caliper=0.25, mahvars=["x1", "x2"])

Entropy Balancing

Direct covariate balance via convex optimization (Hainmueller 2012). Each entry cohort weights the full control pool independently (stacked design).

result = rollmatch(data, ..., method="ebal", moment=1)

result.weights         # unit weights
result.weighted_data   # per-cohort weights [tm, id, weight]

Custom Methods

User-defined per-period weighting functions:

def my_method(treated_data, control_data, covariates, id, **kwargs):
    # Return pl.DataFrame with columns [id, weight]
    ...

result = rollmatch(data, ..., method=my_method)

Scoring Models

11 model types via the model_type parameter:

Propensity score models

Fit a classifier, match on |score_i - score_j|.

model_type Description
"logistic" Standard logistic regression (default)
"probit" Probit model (inverse normal CDF)
"gbm" Gradient boosting (HistGradientBoostingClassifier)
"rf" Random forest
"lasso" L1-regularized logistic
"ridge" L2-regularized logistic
"elasticnet" L1+L2 regularized logistic

Distance-based models

Pairwise covariate distances. No propensity model fitted.

model_type Description
"mahalanobis" Mahalanobis distance (pooled within-group covariance, MatchIt convention)
"scaled_euclidean" Euclidean on covariates standardized by pooled within-group SD
"robust_mahalanobis" Rank-based Mahalanobis (Rosenbaum 2010). Robust to outliers
"euclidean" Raw Euclidean distance

Matching Parameters

Caliper

# Propensity score caliper: ps_caliper * pooled_SD
result = rollmatch(data, ..., ps_caliper=0.2)

# Per-variable calipers (in SD units by default)
result = rollmatch(data, ..., caliper={"age": 0.5, "income": 0.3})

# Per-variable calipers in raw units
result = rollmatch(data, ..., caliper={"age": 5}, std_caliper=False)

ps_caliper_std controls how the pooled SD is computed: "average" (default), "weighted", or "none" (raw units).

Replacement Modes

Controls whether control units can be reused across matches.

replacement= Within period Across periods Use case
"unrestricted" Reuse freely Reuse freely Maximize match rate
"cross_cohort" No reuse Reuse allowed Default. Balanced within-period
"global_no" No reuse No reuse Strictest. Each control used at most once

Matching Order (m_order)

Controls which treated units are matched first. Matters when replacement is constrained.

m_order= Behavior
"largest" Highest PS first (default for PS models). Hard-to-match units get first pick
"smallest" Lowest PS first
"random" Random order
"data" Original data order (default for distance models)

The mahvars Pattern

Match on Mahalanobis distance of specific covariates while using a propensity score caliper to restrict the pool. Follows the MatchIt mahvars convention.

result = rollmatch(
    data, ...,
    covariates=["x1", "x2", "x3"],   # PS estimated on all covariates
    ps_caliper=0.25,                   # PS caliper for pool restriction
    mahvars=["x1", "x2"],             # Mahalanobis matching on these
)

Balance Diagnostics

Post-Matching Balance

from pyrollmatch import balance_test, equivalence_test

# SMD + t-test + variance ratio + KS test
diag = balance_test(scored_data, result.matched_data,
                    "treat", "unit_id", "time", covariates)

# TOST equivalence test (Hartman & Hidalgo 2018)
equiv = equivalence_test(scored_data, result.matched_data,
                         "treat", "unit_id", "time", covariates)

Per-Period Balance

Pooled SMD can mask within-cohort imbalance. Check each entry cohort:

from pyrollmatch import balance_by_period

agg, detail = balance_by_period(
    scored_data, result.matched_data,
    "treat", "unit_id", "time", covariates,
)
# agg: covariate × {wtd_mean_smd, median_abs_smd, max_abs_smd}
# detail: period × covariate × {n_treated, n_controls, smd}

Weighted Diagnostics (for ebal/custom)

from pyrollmatch import balance_test_weighted, equivalence_test_weighted

diag = balance_test_weighted(reduced_data, result.weights,
                             "treat", "unit_id", covariates)

Data Format

Input: polars.DataFrame in long panel format (one row per unit per time period).

Column Type Description
id int/str Unit identifier
tm int Time period (integer, monotonically increasing)
treat int (0/1) Time-invariant treatment group indicator. 1 = eventually treated, 0 = never treated
entry int Treatment onset period for treated units. For controls: any value > max(tm) or null
covariates float Matching variables
unit_id | time | treat | entry_time | x1   | x2
--------|------|-------|------------|------|-----
1       | 1    | 1     | 5          | 2.3  | 1.1   <- treated, enters period 5
1       | 2    | 1     | 5          | 2.5  | 1.0
...
101     | 1    | 0     | 99         | 1.8  | 0.9   <- control (entry=99 sentinel)
101     | 2    | 0     | 99         | 1.9  | 1.0

Pipeline

For advanced use, each step is independently callable:

from pyrollmatch import reduce_data, score_data, compute_balance

# 1. Reduce: select baseline covariates for each entry cohort
reduced = reduce_data(data, "treat", "time", "entry_time", "unit_id", lookback=1)

# 2. Score: fit model, compute scores/distances
scored = score_data(reduced, ["x1", "x2", "x3"], "treat", model_type="logistic")
scored.data          # DataFrame with "score" column
scored.model         # fitted sklearn classifier

# 3. Match: via rollmatch() or match_all_periods() directly

# 4. Balance: assess covariate balance
balance = compute_balance(scored.data, matches, "treat", "unit_id", "time", covariates)

API Reference

  • REFERENCE.md — parameter tables, return types, migration guide
  • API docs — searchable HTML reference (auto-generated from docstrings)

Reproducibility

rollmatch produces bit-identical matches when called repeatedly with the same input data and the same configuration, on the same machine.

  • All distance-based models (mahalanobis, scaled_euclidean, robust_mahalanobis, euclidean) are deterministic by construction.
  • All propensity score models (logistic, probit, lasso, ridge, elasticnet, gbm, rf) use a fixed internal seed (42), and rf runs with n_jobs=1 to avoid joblib's last-bit non-determinism across thread counts.
  • BLAS thread count (OMP_NUM_THREADS, OPENBLAS_NUM_THREADS, etc.) does not affect matching results at any observable level. Pair IDs and distance values are bit-identical across thread counts on the same machine.

To override the internal seed for sensitivity analyses, pass random_state:

# Bootstrap sensitivity analysis over 100 different RF seeds
sigs = []
for s in range(100):
    r = rollmatch(data, ..., model_type="rf", random_state=s)
    sigs.append(r.balance["matched_smd"].abs().max())

random_state is also threaded through to m_order="random" for reproducible random matching order.

Across different machines, pair IDs are robust but last-bit floating-point drift in BLAS routines (different OpenBLAS/MKL builds, different SIMD instruction widths) can in principle produce different difference values. In practice, this only affects matching decisions when two controls are tied to within ~1e-12 Mahalanobis distance of a treated unit — essentially impossible unless your covariates contain exact duplicates. The regression tests in tests/test_reproducibility.py guard same-machine reproducibility.


Testing

uv run pytest tests/           # 239 tests
uv run pytest tests/ -k stress # stress/scale tests

Tests include synthetic data, the Lalonde dataset, and a staggered panel fixture.


Acknowledgements

Inspired by the rollmatch R package by RTI International (Witman et al. 2018). Distance metrics and matching conventions follow MatchIt (Imai, King, Stuart 2011).

References

  • Witman, A., et al. (2018). "Comparison Group Selection in the Presence of Rolling Entry." Health Services Research, 54(1), 262-270.
  • Hainmueller, J. (2012). "Entropy Balancing for Causal Effects." Political Analysis, 20(1), 25-46.
  • Imai, K., King, G., Stuart, E. (2011). MatchIt: Nonparametric Preprocessing for Parametric Causal Inference. Journal of Statistical Software, 42(8).
  • Rosenbaum, P. (2010). Design of Observational Studies, ch. 8.
  • Hartman, E. & Hidalgo, F. D. (2018). "An Equivalence Approach to Balance and Placebo Tests." American Journal of Political Science, 62(4), 1000-1013.

License

MIT

Metadata

Release files for pyrollmatch 0.1.6

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Loss of provenance

These files do not have provenance metadata, despite files in a prior release including this data.

We recommend further investigation before installing these files. View details.

Source distribution (sdist)

Source distribution for pyrollmatch 0.1.6
File Size Uploaded Provenance Actions
pyrollmatch-0.1.6.tar.gz 3.8 MB Without provenance Details for pyrollmatch-0.1.6.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for pyrollmatch 0.1.6
File Interpreter ABI Platform Actions
pyrollmatch-0.1.6-py3-none-any.whl Python 3 none any Details for pyrollmatch-0.1.6-py3-none-any.whl

Total release size: 3.9 MB

Release files / pyrollmatch-0.1.6.tar.gz

Loss of provenance

This file belongs to a release without provenance metadata, despite a prior release including this data.

We recommend further investigation before installing this file. View details.

Download URL pyrollmatch-0.1.6.tar.gz
Size 3.8 MB
Tags Source
SHA-256 checksum
How to use checksums
bbc0fbe491d05f649a8c573fa7f66f6ca3f667f58b414996867d2e46602442ac
BLAKE2b-256 checksum
How to use checksums
66d0e4b69d60c96e732feff257b3a2ef2b8e6968f815dd9b0b154dbb1ac184cd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Release files / pyrollmatch-0.1.6-py3-none-any.whl

Loss of provenance

This file belongs to a release without provenance metadata, despite a prior release including this data.

We recommend further investigation before installing this file. View details.

Download URL pyrollmatch-0.1.6-py3-none-any.whl
Size 37.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e401a7dece6aae87a1c028ff62f0c564e782c51e89dce73302a2ca5976a89a18
BLAKE2b-256 checksum
How to use checksums
b5334eaeb2f0afb798b3e8fed3653b7857176501ef555b77ed13dc71b584c832
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Security for pyrollmatch 0.1.6

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Loss of provenance

A previous release of these files had provenance metadata, but this release does not.

While this could reflect a routine change by the project maintainers, it may also indicate that the release files have been compromised.

Release comparison

  • Removed 0.0.8 2 files With provenance
  • Added 0.1.6 2 files Without provenance

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page