Quickstart

1:1 nearest-neighbor matching on a propensity score (a logit of the treatment on the covariates, fitted by hdfe-stream’s feglm_stream), with replacement, within the 20 exact strata of r x i, with a caliper of 0.2 standard deviations of the score. Then the balance table and the ATT. The simulated effect is 1 for everyone.

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)

# distance="ps" (the default) fits a logit of d on the formula's covariates
m = match_nn("d ~ x1 + x2 + x3", df, exact=["r", "i"], replace=True, caliper=0.2,
             id="id", verbose=False)
m.summary()

print("\nThe first pairs:")
print(m.matches().head(4).collect())
print("\nThe weights:")
print(m.weights().head(4).collect())

print()
m.balance().summary()

print()
m.effect("y").summary()

Output:

match-stream: nn, 1:1 with replacement, estimand ATT, order largest
  distance: ps (score model: d ~ x1 + x2 + x3, logit)
  exact: r, i (20 strata)
  calipers: ps 0.2 SD
               available     matched   unmatched   discarded
  treated         25,497      25,454          43           0
  control         74,503      18,907      55,596           0

The first pairs:
┌─────────────┬─────────────┬────────────┬────────────┬──────┬──────────┬──────────┐
│ treated_row ┆ control_row ┆ treated_id ┆ control_id ┆ rank ┆ distance ┆ subclass │
│ ---         ┆ ---         ┆ ---        ┆ ---        ┆ ---  ┆ ---      ┆ ---      │
│ i64         ┆ i64         ┆ i64        ┆ i64        ┆ i32  ┆ f64      ┆ i64      │
╞═════════════╪═════════════╪════════════╪════════════╪══════╪══════════╪══════════╡
│ 10          ┆ 30914       ┆ 11         ┆ 30915      ┆ 1    ┆ 0.000008 ┆ 1        │
│ 15          ┆ 49745       ┆ 16         ┆ 49746      ┆ 1    ┆ 0.000055 ┆ 2        │
│ 19          ┆ 87575       ┆ 20         ┆ 87576      ┆ 1    ┆ 0.000025 ┆ 3        │
│ 24          ┆ 55962       ┆ 25         ┆ 55963      ┆ 1    ┆ 0.000135 ┆ 4        │
└─────────────┴─────────────┴────────────┴────────────┴──────┴──────────┴──────────┘

The weights:
┌─────┬─────┬─────────┬──────────┬──────────┐
│ row ┆ id  ┆ treated ┆ weight   ┆ subclass │
│ --- ┆ --- ┆ ---     ┆ ---      ┆ ---      │
│ i64 ┆ i64 ┆ bool    ┆ f64      ┆ i64      │
╞═════╪═════╪═════════╪══════════╪══════════╡
│ 0   ┆ 1   ┆ false   ┆ 1.485582 ┆ 1118     │
│ 3   ┆ 4   ┆ false   ┆ 0.742791 ┆ 23714    │
│ 4   ┆ 5   ┆ false   ┆ 0.742791 ┆ 20793    │
│ 6   ┆ 7   ┆ false   ┆ 0.742791 ┆ 21128    │
└─────┴─────┴─────────┴──────────┴──────────┘

Balance before matching
┌──────────┬───────────────┬───────────────┬─────────────────┬────────────┬───────────┬──────────┐
│ term     ┆ Means Treated ┆ Means Control ┆ Std. Mean Diff. ┆ Var. Ratio ┆ eCDF Mean ┆ eCDF Max │
╞══════════╪═══════════════╪═══════════════╪═════════════════╪════════════╪═══════════╪══════════╡
│ distance ┆ 0.3027        ┆ 0.2386        ┆ 0.5452          ┆ 1.3085     ┆ 0.1612    ┆ 0.2347   │
│ x1       ┆ 0.3396        ┆ -0.1194       ┆ 0.4676          ┆ 1.0123     ┆ 0.1301    ┆ 0.1863   │
│ x2       ┆ -0.2115       ┆ 0.0704        ┆ -0.2853         ┆ 0.9946     ┆ 0.0796    ┆ 0.1160   │
│ x3       ┆ 0.1384        ┆ -0.0417       ┆ 0.1801          ┆ 1.0056     ┆ 0.0507    ┆ 0.0741   │
└──────────┴───────────────┴───────────────┴─────────────────┴────────────┴───────────┴──────────┘

Balance after matching
┌──────────┬─────────┬─────────┬───────────┬────────────┬───────────┬──────────┬───────────────────┐
│ term     ┆ Means   ┆ Means   ┆ Std. Mean ┆ Var. Ratio ┆ eCDF Mean ┆ eCDF Max ┆ Std. Pair Dist.   │
│          ┆ Treated ┆ Control ┆ Diff.     ┆            ┆           ┆          ┆                   │
╞══════════╪═════════╪═════════╪═══════════╪════════════╪═══════════╪══════════╪═══════════════════╡
│ distance ┆ 0.3020  ┆ 0.3020  ┆ 0.0001    ┆ 1.0008     ┆ 0.0001    ┆ 0.0009   ┆ 0.0017            │
│ x1       ┆ 0.3351  ┆ 0.3373  ┆ -0.0022   ┆ 1.0145     ┆ 0.0017    ┆ 0.0042   ┆ 0.6745            │
│ x2       ┆ -0.2092 ┆ -0.2024 ┆ -0.0069   ┆ 1.0063     ┆ 0.0035    ┆ 0.0102   ┆ 0.9852            │
│ x3       ┆ 0.1373  ┆ 0.1420  ┆ -0.0047   ┆ 1.0234     ┆ 0.0027    ┆ 0.0077   ┆ 1.0737            │
└──────────┴─────────┴─────────┴───────────┴────────────┴───────────┴──────────┴───────────────────┘

Sample sizes
┌───────────────┬────────────┬────────────┐
│ sample        ┆ Control    ┆ Treated    │
╞═══════════════╪════════════╪════════════╡
│ All (ESS)     ┆ 74503.0000 ┆ 25497.0000 │
│ All           ┆ 74503.0000 ┆ 25497.0000 │
│ Matched (ESS) ┆ 14660.4995 ┆ 25454.0000 │
│ Matched       ┆ 18907.0000 ┆ 25454.0000 │
│ Unmatched     ┆ 55596.0000 ┆ 43.0000    │
│ Discarded     ┆ 0.0000     ┆ 0.0000     │
└───────────────┴────────────┴────────────┘

### y: nearest-neighbor matching on ps
Estimand: ATT   Matched focal units: 25,454
Standard errors: Abadie-Imbens (homoskedastic)
┌─────────────┬──────────┬────────────┬──────────┬──────────┐
│ Coefficient ┆ Estimate ┆ Std. Error ┆ t value  ┆ Pr(>|t|) │
│ ---         ┆ ---      ┆ ---        ┆ ---      ┆ ---      │
│ str         ┆ f64      ┆ f64        ┆ f64      ┆ f64      │
╞═════════════╪══════════╪════════════╪══════════╪══════════╡
│ ATT:y       ┆ 0.987949 ┆ 0.014293   ┆ 69.12017 ┆ 0.0      │
└─────────────┴──────────┴────────────┴──────────┴──────────┘

Reading the output.

  • The summary counts units by arm. 43 treated units have no control within the caliper in their stratum and are left unmatched.

  • matches() has one row per pair: the rows in the source (from 0), the ids, the rank of the match, the score gap and a subclass per treated unit.

  • weights() gives the matching weights: treated 1, a control the sum of 1/k over the treated units it serves, rescaled to sum to the number of matched controls.

  • The balance table compares the groups before and after matching: means, standardized mean differences, variance ratios and eCDF gaps.

  • The effect comes with Abadie-Imbens (2006) standard errors, the default for matching with replacement.