match-stream
Matching on data larger than memory. It does nearest-neighbor matching, on a propensity score or on the covariates themselves, with or without replacement, calipers on the score and on covariates, exact strata, and kernel, ridge and coarsened exact matching. It also gives balance diagnostics, and treatment effects with Abadie–Imbens or clustered standard errors.
It reads Parquet in a stream, holds only a few narrow columns while it matches, and fits the propensity score out of core, with absorbed fixed effects if you want them. Its results are tested against R's MatchIt and Matching and Stata's teffects and kmatch (see below).
from match_stream import match_nn
m = match_nn("treat ~ age + tenure + log_wage0", "extract/*.parquet",
exact=["state", "naics2"], replace=True, caliper=0.2, id="person_id")
m.balance().summary()
m.effect("earnings").summary() # the ATT, Abadie-Imbens standard errors
m.matches().sink_parquet("pairs.parquet")
Status: version 0.1.0, the first release. The designs, the inference and the documentation are tested against MatchIt, Matching, Stata's teffects, kmatch and psmatch2, and the benchmarks are in the documentation (see Performance).
What it does
Three functions: match_nn (nearest neighbor, on a score or a multivariate
distance), match_kernel (kernel and ridge) and match_cem (coarsened exact
matching). Each returns the matches and weights; effect() and balance()
are methods of the result.
Designs
- k:1 nearest neighbor on a propensity score, with or without replacement.
Without replacement it is greedy in any of five orders (
largest,smallest,closest,random,data). - Calipers on the score (in standard deviations) and on covariates, and exact strata.
- Mahalanobis, inverse-variance or Euclidean distance on covariates, alone
or within a score caliper (
mahalanobis_vars=). - Kernel and ridge matching on the score: five kernels, a bandwidth rule or cross-validation.
- Coarsened exact matching.
- Common support (
discard=,reestimate_score=), ties, a minimum number of matches per unit, and one-pass k:1 matching without replacement.
The score
- Logit or probit, fitted by hdfe-stream, with absorbed fixed effects
(
score_model="treat ~ x | firm"), or a column you supply.
Effects and inference
- ATT, ATC and ATE.
- Abadie–Imbens (2006) standard errors, with or without heteroskedastic
conditional variances (
n_variance_matches). - The Abadie–Imbens (2011) bias adjustment.
- The Abadie–Imbens (2016) correction for an estimated score, also with absorbed fixed effects.
- Influence-function standard errors with the matching weights fixed, by unit or clustered one or several ways.
- Several outcomes in one pass (
effect(["y1", "y2"])), with their joint covariance matrix, for every kind of standard error.
Diagnostics
- A balance table (means, standardized differences, variance ratios, eCDF gaps) and sample sizes.
Resources
profile=Truereports the memory, disk and candidate pairs a match would need, without matching, and a resource request for a scheduler.n_threads=andmax_memory_gb=respect a job's limits (stream-limits).- Large inputs are matched a batch of exact strata at a time
(
memory="by_stratum"), with the same matches.
Interface
- The matches, weights and subclasses as Polars LazyFrames, ready to write to Parquet.
- Results with hdfe-stream's reporting:
summary(),tidy(),confint(),etable,summary_json().
Why it is fast
- Windows, not pairs. Within exact strata and sorted by the score, each unit's candidates are a window found by binary search. Nothing forms all the pairs.
- Cheap skipping. Matching without replacement skips taken controls with path-compressed pointers.
- Trees for covariates. Mahalanobis matching uses k-d trees, and candidate lists for matching without replacement and for calipers.
- Window sums. Kernel and ridge weights come from running sums, with no pairs formed.
- Narrow columns. Only tens of bytes a unit are held. The score model is fitted out of core, and the numba kernels run strata in parallel.
Validation against other implementations
| Implementation | Compared on | Agreement |
|---|---|---|
| MatchIt 4.8.2 | nearest neighbor (every order, k:1, calipers), Mahalanobis, mahalanobis_vars, common support, CEM, weights, balance |
pairs identical, weights and balance to 1e-8 |
| R Matching 4.10-15 | Abadie–Imbens SEs (Var.calc), the bias adjustment |
estimates to 1e-10, SEs to 1e-8 |
| Stata teffects | nnmatch, psmatch with the AI (2016) correction, ipw | to 1e-8 |
| Stata kmatch 1.1.6 | nearest neighbor, kernel, ridge, bandwidths, influence-function SEs | matches identical, estimates to 1e-12 |
| MatchIt + sandwich, fixest | clustered influence-function SEs | to 1e-13 |
The R and Stata scripts that make these references are in tests/reference/.
docs/implementations.md lists the conventions on which these packages
differ, and which option reproduces each.
Performance
The benchmark suites (benchmarks/) compare match-stream with MatchIt and
Matching (R), pymatchit, fastmatch and Polars' join_asof and group_by,
and with bare k-d tree and faiss searches as a floor. They cover
eight designs and the standard errors, from 10,000 to 100 million units.
On a 16-vCPU, 32 GB cloud machine, at 10 million units:
- match-stream is 6 to 35 times faster than MatchIt where MatchIt finishes, with the same matches (estimates agree to about 1e-14). At 100 million units MatchIt needs 28 to 30 GB where match-stream needs 8 to 21 GB, and in two designs it runs out of memory.
- With no strata, 1:1 Mahalanobis matching takes 6.1 s, as long as a bare SciPy k-d tree search of the same data (6.7 s). MatchIt exceeds 30 minutes.
- pymatchit and R's Matching are slower still, and mostly do not finish at 10 million units or more.
- Polars'
group_by(coarsened exact matching) andjoin_asof(1:1 on a score) are faster, as they do less. - At 100 million units, 1:1 Mahalanobis matching without replacement and
with calipers needs
memory="by_stratum"on this machine (187 s, 9.2 GB).
The results, figures and caveats are in the benchmarks.
Small runs, for correctness only, give these results:
- MatchIt's estimates agree to about 1e-15 on every design.
- At 100,000 units, R Matching's Abadie–Imbens standard error (with
Var.calc = 1) took 307 s; match-stream's, the same to 1e-15, took 0.07 s.
Install
pip install match-stream
Python 3.10 or later. It depends on hdfe-stream (with its formula extra),
stream-limits, Polars, NumPy, numba, SciPy and pyarrow. On Python 3.15,
pyfixest (which hdfe-stream's formula extra brings in) has no wheels yet: it
builds from source with a Rust toolchain and
PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 set; everything else installs from wheels.
Documentation
Build the documentation locally with:
pip install -e ".[docs]"
sphinx-build -W --keep-going -b html docs docs/_build/html
The guide covers:
- how it works;
- the conventions that differ from other matching packages;
- the resources a match needs;
- the API reference.
examples/ has runnable scripts, and benchmarks/ the performance
comparisons (see benchmarks/README.md).
A sibling of hdfe-stream, logit-stream and dpd-stream.
License
MIT.
Metadata
Release files for match-stream 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| match_stream-0.1.0.tar.gz | 138.6 kB | With provenance | Details for match_stream-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| match_stream-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for match_stream-0.1.0-py3-none-any.whl |
Total release size: 248.2 kB
Release files / match_stream-0.1.0.tar.gz
| Download URL | match_stream-0.1.0.tar.gz |
|---|---|
| Size | 138.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4e24bc5d34a9c4846426b22f1ffe3767532a234c7a715ecbc6bd4f4fa0864428
|
|
BLAKE2b-256 checksum How to use checksums |
9fbb6283f3eb7a8075a30e6472b8b020d3812e4527a41a932be557f3d64b0ac6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 6, 2026.
Transparency logRelease files / match_stream-0.1.0-py3-none-any.whl
| Download URL | match_stream-0.1.0-py3-none-any.whl |
|---|---|
| Size | 109.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f452005f4dc33a96c60a6416e0633e97900688b6b8a43a6be231d463348c3a95
|
|
BLAKE2b-256 checksum How to use checksums |
ebfae5345a17eb059f0e29d91089e281af8928fa990a5ce19590c4bda98ff26e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 6, 2026.
Transparency logSecurity for match-stream 0.1.0
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.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for match_stream-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 6, 2026.
Transparency logA 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.