k nearest neighbors¶
Nearest-neighbor matching on the propensity score with three matches per
treated unit, with replacement, ties kept, a caliper on the score and one on
x1. Units the calipers leave short of three matches
are kept by default, or dropped with min_matches=3. Without
replacement and k = 2, matching in rounds (the default) against one pass
(match_in_rounds=False).
import polars as pl
from match_stream import match_nn, simulate_matching
pl.Config.set_tbl_hide_dataframe_shape(True)
df = simulate_matching(100_000)
kw = dict(exact=["r", "i"], distance="ps", id="id", verbose=False)
# WITH replacement (replace=True): a control can be the match of many treated units
m = match_nn("d ~ x1 + x2 + x3", df, n_matches=3, replace=True, keep_ties=True,
caliper={"ps": 0.1, "x1": 0.1}, **kw)
per = m.matches().group_by("treated_id").len().collect()["len"]
print("matches per treated unit:", per.value_counts().sort("len").rows())
m3 = match_nn("d ~ x1 + x2 + x3", df, n_matches=3, replace=True, keep_ties=True,
caliper={"ps": 0.1, "x1": 0.1}, min_matches=3, **kw)
print("matched treated, keeping short units:", m.counts()["matched_treated"],
" with min_matches=3:", m3.counts()["matched_treated"])
# WITHOUT replacement (replace=False, the default; written out here), k = 2:
# each control is used once. In rounds (every treated unit takes its first
# match, then its second) or in one pass (pairs taken in order of distance)
for rounds in (True, False):
w = match_nn("d ~ x1 + x2 + x3", df, n_matches=2, replace=False,
match_order="closest", match_in_rounds=rounds, **kw)
d = w.matches().select(pl.col("distance").mean()).collect().item()
two = (w.matches().group_by("treated_id").len().collect()["len"] == 2).sum()
print(f"match_in_rounds={rounds}: {two:,} treated with 2 controls, mean distance {d:.5f}")
Output:
matches per treated unit: [(1, 1009), (2, 946), (3, 22334)]
matched treated, keeping short units: 24289 with min_matches=3: 22334
match_in_rounds=True: 25,414 treated with 2 controls, mean distance 0.02845
match_in_rounds=False: 25,447 treated with 2 controls, mean distance 0.02837
Notes.
Ties.
keep_ties=Truekeeps every control at exactly the k-th distance (otherwise exactly k are kept). Each treated unit’s matches share its weight equally.Rounds or one pass. In rounds, every treated unit takes its first match before any takes its second. In one pass, pairs are taken in order of distance and a unit keeps taking controls until it has k. Here the two differ little; they differ more when treated units compete for few controls (see Conventions).