Looking Inside Mahalanobis Metric Matching | by Bowen Chen, PhD | Towards Data Science

My previous post shows how the propensity score matching can be implemented using 13 lines of R codes. Though those codes, you can see that matching is much about data pre-processing. The idea is to find control units that are comparable to the treated units, so we can attribute the differences in the outcome between treatment groups to the treatment with more confidence. Besides, I want to argue that matching is, in fact, simple, so you should not be terrified by it.
Today, I look into a popular alternative to propensity score matching: Mahalanobis Metric (MM) matching. The two matching methods are different mainly in that MM matching uses the Mahalanobis metric to find matches instead of propensity scores. I will show you step-by-step that how MM matching is done in practice using the simplest R codes. These steps are embedded in programs that does matching, but they are often invisible.
Similar to my previous post, I simulate for a small data set for the data exercise. You can find the R codes for generating the simulated data from here. The simulated data have two continuous covariates (W1, W2), a binary treatment (A), and a continuous outcome (Y). For simplicity, I generate 10 data points only, and 4 show up as treated units.
The codes for generating the data are:
library(Matching)library(dplyr)library(tidyr) # call libraries set.seed(123) # seed for reproducibilitydata <- generateData(10) # generate 10 data points (need to download # the function provided in the main text)
The simulated data values are:
Step 1. Define a function to calculate Mahalanobis distance
The math formula to calculate Mahalanobis Distance is: MD = (X1 - X2)’S(X1 - X2), where X1, X2 are vectors of covariates (W1 and W2 in our case) for a treated and a control unit, respectively. S is inverse of sample covariance of data. Note that we can calculate distance for each pair (treated versus control). We do not calculate distance between treated units, because we are matching between treatment groups, not within the same treatment group.
Let’s define a function that can calculate MD for one pair of observation units. Then we can loop it over all pairs. The function is given below. Each line of code is self-explanatory, so I do not provide any more discussions here.
mahalDist <- function(Tr, Cn, inv.cov, dataset) { covars <- dimnames(inv.cov)[[1]] # get covariate names xtreat <- as.matrix(dataset[Tr, covars]) # get X1 (treated) xcntrl <- as.matrix(dataset[Cn, covars]) # get X2 (control) xdiffs <- as.matrix(xtreat — xcntrl) # calculate differences out <- as.numeric(xdiffs %*% inv.cov %*% t(xdiffs)) # MD names(out) <- paste0(Tr, ‘-’, Cn) # Set a name for the output return(out)}
Step 2. Calculate Mahalanobis distance for each treated/control pair
Now we calculate MD for each treated/control pair using the codes below:
icv <- solve(cov(data[, c(‘W1’, ‘W2’)])) # Inverse of the variancetreat.rows <- which(data$A == 1) # treated rowscntrl.rows <- which(data$A == 0) # control rowsmdist <- mapply(function(x,y) mahalDist(x, y, inv.cov = icv, data = data), rep(treat.rows, each = length(cntrl.rows)),       rep(cntrl.rows, times = length(treat.rows)))
Note that the last mapply function is simply looping over the calculation of MD (defined by mahalDist) over all possible treated/control pairs based on their row numbers. Below reports the output of mdist, which says that, for example, the MD between the treated unit located at row 1 and the control unit located at row 3 is 12.12. It seems that this value is the largest among all MDs associated with the treated unit located at row 1. The smallest MD is 1.67 associated with the control unit located at row 6.
Step 3. Look for the best matches with the lowest distances
Since the sample size is low, we can easily find the best matches (with the lowest MD) for each treated unit. But below are some codes to automatically find the best matches:
mdist_data <-  data.frame(pairs = names(mdist), maha_dist = unlist(mdist), stringsAsFactors = F) %>% # Create a data frame to save pair names #and MD values.  separate(pairs, into = c(‘treat’, ‘cntrl’), sep = ‘-’) %>% # get #separate names for treated and control units. group_by(treat) %>% # Search for the lowest MD by each treated unit filter(maha_dist == min(maha_dist)) # return the lowest MD 
The output of mdist_data is:
You can see that four control units with the lowest MD are matched to four treated units. The last pair (i.e., treated unit at row 7– control unit at row 9) is the closest pair, while the first part (i.e., treat unit at row 1– control unit at row 6) is the most distinct pair, relatively speaking. Indeed, as shown in figure 1, the treated unit at row 1 happens to have the lowest W1 and highest W2. Therefore, it would be hard to find a good match for it (too different to others).
After three steps with a handful of codes, we have completed the data pre-processing based on Mahalanobis metric matching. One can do further analyses with the matched data. I hope that you have enjoyed the reading!