matching two groups into pairs
Optimal one-to-one matching by linear assignment, solved exactly in C++.
Hand it two groups. couplr returns the pairing that minimizes total covariate distance across the whole sample, solved by linear assignment (Jonker-Volgenant, Hungarian, Auction, cost-scaling) on RcppEigen. The common tool, greedy nearest neighbour, locks in each pair as it goes and gives back whatever the ordering produced. This finds the global minimum, and the same answer every run.
library(couplr)
# match treatment and control on covariates, in a single call
result <- match_couples(treated, control, vars = c("age", "income"), auto_scale = TRUE)
# analysis-ready paired data
join_matched(result, treated, control)Optimal, not greedy
MatchIt, the most used matching package in R, pairs units greedily on an estimated propensity score: it takes them in order, grabs the nearest free control for each, and the result depends on that order. couplr matches directly on the covariates and solves the assignment exactly, so total distance is the global minimum and the pairing is the same every time.
match_couples(treated, control, vars = c("age", "income"), auto_scale = TRUE) # optimal, deterministic
match_couples(treated, control, vars = c("age", "income"), strategy = "pq", method = "greedy") # greedy, for large poolsFor large control pools, match_couples(method = "greedy") trades the exact guarantee for speed, with three strategies (sorted, row_best, pq) and the same preprocessing and constraints.
What’s in the box
-
match_couples(): optimal one-to-one matching with automatic scaling (robust / standardize / range), distance constraints (max_distance,calipers), blocking, andratio/replacematching. -
match_couples(method = "greedy"): fast approximate matching for large datasets, three strategies. -
full_match()/cem_match()/subclass_match()/cardinality_match(): variable-ratio full matching, coarsened exact matching, propensity subclassification, and balance-constrained matching. -
ps_match(): propensity score matching with a logit caliper. -
balance_diagnostics()/sensitivity_analysis(): standardized differences, variance ratios, KS tests, and Rosenbaum bounds for hidden bias. -
lap_solve(): tidy interface to the assignment backend, 20 solvers withmethod = "auto", pluslap_solve_batch()andlap_solve_kbest()(Murty’s algorithm).
The assignment backend
lap_solve() exposes the solver layer directly. It takes a cost matrix, handles rectangular shapes and forbidden edges (NA / Inf), and picks an algorithm from the problem when method = "auto":
cost <- matrix(c(4, 2, 8, 4, 3, 7, 3, 1, 6), nrow = 3, byrow = TRUE)
lap_solve(cost) # auto-selected solver
lap_solve(cost, method = "hungarian")
lap_solve_kbest(cost, k = 3) # the three best assignmentsThe solvers span the classics and the scaling algorithms: Jonker-Volgenant, Hungarian, Kuhn-Munkres, Bertsekas auction (with epsilon-scaling variants), Goldberg-Kennedy cost-scaling, Gabow-Tarjan bit-scaling, push-relabel, network simplex, and Sinkhorn entropy-regularized transport.
match_couples: optimal or greedy?
match_couples() |
match_couples(method = "greedy") |
|
|---|---|---|
| Result | Globally optimal | Approximate |
| Deterministic? | Yes | Yes |
| Cost | O(n^3) |
O(n^2) or better |
| Best for | n < 5000 |
large control pools |
| Constraints, blocking? | Yes | Yes |
Start with match_couples(). Switch to match_couples(method = "greedy") when the optimal solve runs too long.
Fits the matching ecosystem
couplr results convert to matchit-class with as_matchit(), so cobalt balance tables and marginaleffects estimates work without rewiring your analysis. match_data() returns treatment, weights, and subclass columns in one analysis-ready frame, and autoplot() methods cover matching results, balance, and sensitivity.
Installation
install.packages("couplr") # CRAN
install.packages("pak") # development version
pak::pak("gcol33/couplr")Support
“Software is like sex: it’s better when it’s free.” — Linus Torvalds
I’m a PhD student who builds R packages in my free time because I believe good tools should be free and open. I started these projects for my own work and figured others might find them useful too.
If this package saved you some time, buying me a coffee is a nice way to say thanks. It helps with my coffee addiction.