---
title: "Matching and Weighting Part 1: matching"
date: "2025-05-29"
---
This chapter follows R's `MatchIt` and `WeightIt` packages (Greifer et al.).
## Assumptions
Matching and weighting require three assumptions for causal identification in observational studies: (1) SUTVA, (2) ignorability (unconfoundedness), and (3) overlap (positivity). Under ignorability, conditioning on $X$ makes treatment assignment $D$ independent of the potential outcomes $Y(0)$ and $Y(1)$. In practice, the distribution of $X$ is rarely balanced across treatment and control groups — matching and weighting restore that balance.
## Distance
Matching finds control units close to treated units in $X$-space. The first question is how to define "close."
### Propensity score
The most common distance measure is the propensity score (PS), which is the probability of being treated given $X$. The benefit is that when there is high dimension of $X$, we can reduce the dimension to a single number, the PS.
There are many ways to estimate the propensity score, such as logit, or more flexible parametric "gam", or other machine learning methods, such as "gbm", "lasso", "rpart", "randomforest", "cbps", etc. Here CBPS has seen more popularity. The propensity scores are estimated using the covariate balancing propensity score (CBPS) algorithm, which is a form of logistic regression where balance constraints are incorporated into a generalized method of moments estimation of the model coefficients.
### Distance from covariates
Or we can compute distance from covariates directly, not using propensity score. The distance can be computed using Mahalanobis distance, or Euclidean distance, or other distance measures.
## matching methods
With a distance, we then decide which method to use. The first few matching methods all need to specify a distance measure. They are matching based on distance. Then there are stratum matching methods, which do not need a distance measure. The last two methods are subsetting methods.
### Nearest neighbor matching
NNM is widely used; it is also known as greedy matching. It matches each treated unit to the closest control unit in terms of distance. The closest control unit is defined as the one with the smallest distance to the treated unit. If there are multiple control units with the same distance and we want a 1:1 match, one is randomly selected.
### Optimal pair matching
NNM is not optimal in the sense that it does not necessarily minimize the total distance between matched pairs. Optimal pair matching is a method that minimizes the total distance (or some other overall criterion) between matched pairs. It is more computationally intensive than NNM, but it can lead to better balance.
### Optimal full matching
OFM assigns each unit (treated or control) to a subclass. For each subclass then to calculate the sum of within-subclass distances, and then to minimize the total distance across all subclasses. It is more computationally intensive than NNM and optimal pair matching. Weights are used based on subclass membership. Because all units are included, it can be used to estimate ATE.
### Generalized Full matching
GFM is a variant of OFM. It uses a faster algorithm for large datasets.
### Genetic matching
Genetic matching (`method = "genetic"`, Diamond and Sekhon 2013) runs nearest
neighbor matching on a *scaled* generalized Mahalanobis distance, where the
scaling weights on the covariates are not fixed in advance but chosen by a
genetic algorithm that searches for the weighting giving the best covariate
balance. That search is the point of the method --- and also why it is far
slower than the alternatives above.
### Exact matching
It's a stratum matching. First subclasses are defined, by combinations of covariate values. Then units are assigned to subclasses. When a subclass has only either control or treated units, it is discarded.
Exact matching is nonparametric, but it works only when covariates are discrete and it happens that there are enough treated and control units in each subclass.
### Coarsened exact matching
CEM is a more popular stratum matching. First bins are created after coarsening the covariates. then use exact matching on the covariate bins. CEM is nonparametric. User needs to decide how many bins to create for each covariate.
### Subclassification
Subclassification is another kind of stratum matching. It uses the propensity score to define bins, usually quantiles of PS in the treated group, or control group or overall, depending on target estimand.
### Cardinality and profile matching
Cardinality matching involves finding the largest sample that satisfies user-supplied balance constraints and constraints on the ratio of matched treated to matched control units. Profile matching involves identifying a target distribution (e.g., the full sample for the ATE or the treated units for the ATT) and finding the largest subset of the treated and control groups that satisfy user-supplied balance constraints with respect to that target.
## Example
Here is an example with Lalonde data, we are interested in the effect of treatment on "re78" (1978 real earnings). There are 614 observations, 185 treated and 429 control, with covariates age, education, race, marital status, degree status, and earnings in 1974 and 1975. The first `matchit` call uses `method = NULL`, which fits the propensity score but performs no matching — a way of getting the pre-match balance table.
```{r}
#| message: false
library("MatchIt")
data("lalonde")
head(lalonde)
# No matching; constructing a pre-match matchit object
m.out0 <- matchit(treat ~ age + educ + race + married +
nodegree + re74 + re75,
data = lalonde,
method = NULL,
distance = "glm")
summary(m.out0)
```
The imbalance is severe. Standardized mean differences are $-0.72$ for 1974
earnings, 1.76 for the black indicator, $-1.88$ for white, $-0.83$ for married
and $-0.31$ for age, and the propensity score itself differs by 1.79 standard
deviations. The treated average \$2,096 in 1974 earnings against \$5,619 for
controls, and 84% are black against 20%. These are not two samples that differ
at the margin.
Or use "cobalt" to see the balance:
```{r}
#| message: false
library("cobalt")
bal.tab(treat ~ age + educ + race + married + nodegree + re74 + re75,
data = lalonde, estimand = "ATT", thresholds = c(m = .05))
```
`bal.tab` says the same thing against a stricter 0.05 threshold: all nine
covariate contrasts fail, with `re74` the worst at $-0.7211$.
First do a PS match. The chunks below run several distance measures in turn —
the logit of a logistic propensity score, a GAM score with smooth terms in age
and education, CBPS targeting the ATC with replacement, Mahalanobis distance on
the raw covariates, and full matching on a probit score. Each prints its own
balance table; what changes across them is the `distance` row and how much of the
covariate imbalance survives.
```{r}
# Matching on logit of a PS estimated with logistic
# regression:
m.out1 <- matchit(treat ~ age + educ + race + married +
nodegree + re74 + re75,
data = lalonde,
distance = "glm",
link = "linear.logit")
summary(m.out1)
plot(m.out1, type = "jitter", interactive = FALSE)
plot(summary(m.out1))
```
We see there are still some imbalance on variables such as "raceblack". We can use more flexible models for the PS.
```{r}
# GAM logistic PS with smoothing splines (s()):
m.out2 <- matchit(treat ~ s(age) + s(educ) +
race + married +
nodegree + re74 + re75,
data = lalonde,
distance = "gam")
summary(m.out2)
plot(m.out2, type = "jitter", interactive = FALSE)
plot(summary(m.out2))
```
Or use CBPS.
```{r}
# CBPS for ATC matching w/replacement, using the just-
# identified version of CBPS (setting method = "exact"):
m.out3 <- matchit(treat ~ age + educ + race + married +
nodegree + re74 + re75,
data = lalonde,
distance = "cbps",
estimand = "ATC",
distance.options = list(method = "exact"),
replace = TRUE)
summary(m.out3)
plot(m.out3, type = "jitter", interactive = FALSE)
plot(summary(m.out3))
```
```{r}
# Mahalanobis distance matching - no PS estimated
m.out4 <- matchit(treat ~ age + educ + race + married +
nodegree + re74 + re75,
data = lalonde,
distance = "mahalanobis")
summary(m.out4)
m.out4$distance #NULL
plot(m.out4, interactive = FALSE)
plot(summary(m.out4))
```
```{r}
# Full matching on a probit PS
m.out5 <- matchit(treat ~ age + educ + race + married +
nodegree + re74 + re75,
data = lalonde,
method = "full",
distance = "glm",
link = "probit")
summary(m.out5)
plot(m.out5, type = "jitter", interactive = FALSE)
plot(summary(m.out5))
```
Note that `m.out4$distance` prints `NULL`: Mahalanobis matching estimates no
propensity score at all, so there is no distance column and no `distance` row in
its balance table. That is the point of the method — it works on the covariate
space directly rather than collapsing it to a scalar.
## Estimation
After we get a matched data that we think is good enough, we can estimate the treatment effect. We use `m.out5`, the full match on a probit score.
```{r}
m.data <- match_data(m.out5)
head(m.data)
```
What "match_data" does is to grab the matched data with a few additional variables such as "weights", "subclass", and "distance". Then we can use it in a simple linear model with weights.
```{r}
library("marginaleffects")
fit <- lm(re78 ~ treat * (age + educ + race + married +
nodegree + re74 + re75),
data = m.data,
weights = weights)
avg_comparisons(fit,
variables = "treat",
vcov = ~subclass,
newdata = subset(m.data, treat == 1))
```
The ATT is \$1,977 with a subclass-clustered standard error of \$704,
$p = 0.005$, and a 95% interval of $[596, 3357]$. Training raised 1978 earnings
for those who took it.
Note we allow interaction of treatment and covariates, but we do not need to demean the covariates here, since we are relying on "marginaleffects" and its "avg_comparisons" function to compute the average treatment effect. The "newdata" argument is used to specify the effect, in this case ATT. Note the weights are generated this way: since it's ATT, all treated units are weighted by 1, and control units are weighted by $p/(1-p)$, where $p$ is proportion of treated units in the subclass. Anyway, for full matching, we get the comparison of potential outcomes for the treated, with weights, and clustered standard errors on subclasses, because that the level the comparison is conducted.
---
<!-- see-also-footer -->
*Systematic treatment: [R](https://xiangao.github.io/causal_econometrics_guide/matching.html) · [Julia](https://xiangao.github.io/causal_econometrics_julia/matching.html).*