What are robust standard errors? How do we calculate them? Why use them? Why not use them all the time if they’re so robust? Those are the kinds of questions this post intends to address.
To begin, let’s start with the relatively easy part: getting robust standard errors for basic linear models in Stata and R. In Stata, simply appending vce(robust)
to the end of regression syntax returns robust standard errors. “vce” is short for “variance-covariance matrix of the estimators”. “robust” indicates which type of variance-covariance matrix to calculate. Here’s a quick example using the auto data set that comes with Stata 16:
. sysuse auto (1978 Automobile Data) . regress mpg turn trunk, vce(robust) Linear regression Number of obs = 74 F(2, 71) = 68.76 Prob > F = 0.0000 R-squared = 0.5521 Root MSE = 3.926 ------------------------------------------------------------------------------ | Robust mpg | Coef. Std. Err. t P>|t| [95% Conf. Interval] -------------+---------------------------------------------------------------- turn | -.7610113 .1447766 -5.26 0.000 -1.049688 -.472335 trunk | -.3161825 .1229278 -2.57 0.012 -.5612936 -.0710714 _cons | 55.82001 4.866346 11.47 0.000 46.11679 65.52323
Notice the third column indicates “Robust” Standard Errors.
To replicate the result in R takes a bit more work. First we load the haven
package to use the read_dta
function that allows us to import Stata data sets. Then we load two more packages: lmtest
and sandwich
. The lmtest
package provides the coeftest
function that allows us to re-calculate a coefficient table using a different variance-covariance matrix. The sandwich
package provides the vcovHC
function that allows us to calculate robust standard errors. The type
argument allows us to specify what kind of robust standard errors to calculate. “HC1” is one of several types available in the sandwich
package and happens to be the default type in Stata 16. (We talk more about the different types and why it’s called the “sandwich” package below.)
library(haven) library(lmtest) library(sandwich) auto <- read_dta('http://www.stata-press.com/data/r16/auto.dta') m <- lm(mpg ~ turn + trunk, data = auto) coeftest(m, vcov. = vcovHC(m, type = 'HC1')) ## ## t test of coefficients: ## ## Estimate Std. Error t value Pr(>|t|) ## (Intercept) 55.82001 4.86635 11.4706 < 2.2e-16 *** ## turn -0.76101 0.14478 -5.2565 1.477e-06 *** ## trunk -0.31618 0.12293 -2.5721 0.0122 * ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
If you look carefully you’ll notice the standard errors in the R output match those in the Stata output.
If we want 95% confidence intervals like those produced in Stata, we need to use the coefci
function:
coefci(m, vcov. = vcovHC(m, type = 'HC1')) ## 2.5 % 97.5 % ## (Intercept) 46.1167933 65.52322937 ## turn -1.0496876 -0.47233499 ## trunk -0.5612936 -0.07107136
While not really the point of this post, we should note the results say that larger turn circles and bigger trunks are associated with lower gas mileage.
Now that we know the basics of getting robust standard errors out of Stata and R, let’s talk a little about why they’re robust by exploring how they’re calculated.
The usual method for estimating coefficient standard errors of a linear model can be expressed with this somewhat intimidating formula:
\[\text{Var}(\hat{\beta}) = (X^TX)^{-1} X^T\Omega X (X^TX)^{-1}\] where \(X\) is the model matrix (ie, the matrix of the predictor values) and \(\Omega = \sigma^2 I_n\), which is shorthand for a matrix with nothing but \(\sigma^2\) on the diagonal and 0’s everywhere else. Two main things to notice about this equation:
- The \(X^T \Omega X\) in the middle
- The \((X^TX)^{-1}\) on each side
Some statisticians and econometricians refer to this formula as a “sandwich” because it’s like an equation sandwich: we have “meat” in the middle, \(X^T \Omega X\), and “bread” on the outside, \((X^TX)^{-1}\). Calculating robust standard errors means substituting a new kind of “meat”.
Before we do that, let’s use this formula by hand to see how it works when we calculate the usual standard errors. This will give us some insight to the meat of the sandwich. To make this easier to demonstrate, we’ll use a small toy data set.
set.seed(1) x <- c(1:4, 7) y <- c(5 + rnorm(4,sd = 1.2), 35) plot(x, y)
Notice the way we generated y
. It is simply the number 5 with some random noise from a N(0,1.2) distribution plus the number 35. There is no relationship between x
and y
. However, when we regress y
on x
using lm
we get a slope coefficient of about 5.2 that appears to be “significant”.
m <- lm(y ~ x) summary(m) ## ## Call: ## lm(formula = y ~ x) ## ## Residuals: ## 1 2 3 4 5 ## 5.743 1.478 -4.984 -7.304 5.067 ## ## Coefficients: ## Estimate Std. Error t value Pr(>|t|) ## (Intercept) -6.733 5.877 -1.146 0.3350 ## x 5.238 1.479 3.543 0.0383 * ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 ## ## Residual standard error: 6.808 on 3 degrees of freedom ## Multiple R-squared: 0.8071, Adjusted R-squared: 0.7428 ## F-statistic: 12.55 on 1 and 3 DF, p-value: 0.03829
Clearly the 5th data point is highly influential and driving the “statistical significance”, which might lead us to think we have specified a “correct” model. Of course we know that we specified a “wrong” model because we generated the data. y
does not have a relationship with x
! It would be nice if we could guard against this sort of thing from happening: specifying a wrong model but getting a statistically significant result. One way we could do that is modifying how the coefficient standard errors are calculated.
Let’s see how they were calculated in this case using the formula we specified above. Below s2
is \(\sigma^2\), diag(5)
is \(I_n\), and X
is the model matrix. Notice we can use the base R function model.matrix
to get the model matrix from a fitted model. We save the formula result into vce
, which is the variance-covariance matrix. Finally we take square root of the diagonal elements to get the standard errors output in the model summary.
s2 <- sigma(m)^2 X <- model.matrix(m) vce <- solve(t(X) %*% X) %*% (t(X) %*% (s2*diag(5)) %*% X) %*% solve(t(X) %*% X) sqrt(diag(vce)) ## (Intercept) x ## 5.877150 1.478557
Now let’s take a closer look at the “meat” in this sandwich formula:
s2*diag(5) ## [,1] [,2] [,3] [,4] [,5] ## [1,] 46.346 0.000 0.000 0.000 0.000 ## [2,] 0.000 46.346 0.000 0.000 0.000 ## [3,] 0.000 0.000 46.346 0.000 0.000 ## [4,] 0.000 0.000 0.000 46.346 0.000 ## [5,] 0.000 0.000 0.000 0.000 46.346
That is a matrix of constant variance. This is one of the assumptions of classic linear modeling: the errors (or residuals) are drawn from a single Normal distribution with mean 0 and a fixed variance. The s2
object above is the estimated variance of that Normal distribution. But what if we modified this matrix so that the variance was different for some observations? For example, it might make sense to assume the error of the 5th data point was drawn from a Normal distribution with a larger variance. This would result in a larger standard error for the slope coefficient, indicating greater uncertainty in our coefficient estimate.
This is the idea of “robust” standard errors: modifying the “meat” in the sandwich formula to allow for things like non-constant variance (and/or autocorrelation, a phenomenon we don’t address in this post).
So how do we automatically determine non-constant variance estimates? It might not surprise you there are several ways. The sandwich
package provides seven different types at the time of this writing (version 2.5-1). The default version in Stata is identified in the sandwich
package as “HC1”. The HC stands for Heteroskedasticity-Consistent. Heteroskedasticity is another word for non-constant. The formula for “HC1” is as follows:
\[HC1: \frac{n}{n-k}\hat{\mu}_i^2 \]
where \(\hat{\mu}_i^2\) refers to squared residuals, \(n\) is the number of observations, and \(k\) is the number of coefficients. In our simple model above, \(k = 2\), since we have an intercept and a slope. Let’s modify our formula above to substitute HC1 “meat” in our sandwich:
# HC1 hc1 <- (5/3)*resid(m)^2 # HC1 "meat" hc1*diag(5) ## [,1] [,2] [,3] [,4] [,5] ## [1,] 54.9775 0.000000 0.00000 0.00000 0.00000 ## [2,] 0.0000 3.638417 0.00000 0.00000 0.00000 ## [3,] 0.0000 0.000000 41.39379 0.00000 0.00000 ## [4,] 0.0000 0.000000 0.00000 88.92616 0.00000 ## [5,] 0.0000 0.000000 0.00000 0.00000 42.79414
Notice we no longer have constant variance for each observation. The estimated variance is instead the residual squared multiplied by (5/3). When we use this to estimate “robust” standard errors for our coefficients we get slightly different estimates.
vce_hc1 <- solve(t(X) %*% X) %*% (t(X) %*% (hc1*diag(5)) %*% X) %*% solve(t(X) %*% X) sqrt(diag(vce_hc1)) ## (Intercept) x ## 5.422557 1.428436
Notice the slope standard error actually got smaller. It looks like the HC1 estimator may not be the best choice for such a small sample. The default estimator for the sandwich
package is known as “HC3”
\[HC3: \frac{\hat{\mu}_i^2}{(1 – h_i)^2} \]
where \(h_i\) are the hat values from the hat matrix. A Google search or any textbook on linear modeling can tell you more about hat values and how they’re calculated. For our purposes it suffices to know that they range from 0 to 1, and that larger values are indicative of influential observations. We see then that HC3 is a ratio that will be larger for values with high residuals and relatively high hat values. We can manually calculate the HC3 estimator using the base R resid
and hatvalues
functions as follows:
# HC3 hc3 <- resid(m)^2/(1 - hatvalues(m))^2 # HC3 "meat" hc3*diag(5) ## [,1] [,2] [,3] [,4] [,5] ## [1,] 118.1876 0.000000 0.00000 0.00000 0.0000 ## [2,] 0.0000 4.360667 0.00000 0.00000 0.0000 ## [3,] 0.0000 0.000000 39.54938 0.00000 0.0000 ## [4,] 0.0000 0.000000 0.00000 87.02346 0.0000 ## [5,] 0.0000 0.000000 0.00000 0.00000 721.2525
Notice that the 5th observation has a huge estimated variance of about 721. When we calculate the robust standard errors for the model coefficients we get a much bigger standard error for the slope.
vce_hc3 <- solve(t(X) %*% X) %*% (t(X) %*% (hc3*diag(5)) %*% X) %*% solve(t(X) %*% X) sqrt(diag(vce_hc3)) ## (Intercept) x ## 12.149991 4.734495
Of course we wouldn’t typically calculate robust standard errors by hand like this. We would use the vcovHC
function in the sandwich
package as we demonstrated at the beginning of this post along with the coeftest
function from the lmtest
package. (Or use vce(hc3)
in Stata)
coeftest(m, vcovHC(m, "HC3")) ## ## t test of coefficients: ## ## Estimate Std. Error t value Pr(>|t|) ## (Intercept) -6.7331 12.1500 -0.5542 0.6181 ## x 5.2380 4.7345 1.1063 0.3493
Now the slope coefficient estimate is no longer “significant” since the standard error is larger. This standard error estimate is robust to the influence of the outlying 5th observation.
So when should we use robust standard errors? One flag is seeing large residuals and high leverage (ie, hat values). For instance the following base R diagnostic plot graphs residuals versus hat values. A point in the upper or lower right corners is an observation exhibiting influence on the model. Our 5th observation has a corner all to itself.
plot(m, which = 5)
But it’s important to remember large residuals (or evidence of non-constant variance) could be due to a misspecified model. We may be missing key predictors, interactions, or non-linear effects. Because of this it might be a good idea to think carefully about your model before reflexively deploying robust standard errors. Zeileis (2006), the author of the sandwich
package, also gives two reasons for not using robust standard errors “for every model in every analysis”:
First, the use of sandwich estimators when the model is correctly specified leads to a loss of power. Second, if the model is not correctly specified, the sandwich estimators are only useful if the parameters estimates are still consistent, i.e., if the misspecification does not result in bias.
We can demonstrate each of these points via simulation.
In the first simulation, we generate data with an interaction, fit the correct model, and then calculate both the usual and robust standard errors. We then check how often we correctly reject the null hypothesis of no interaction between x
and g
. This is an estimation of power for this particular hypothesis test.
# function to generate data, fit correct model, and extract p-values of # interaction f1 <- function(n = 50){ # generate data g <- gl(n = 2, k = n/2) x <- rnorm(n, mean = 10, sd = 2) y <- 1.2 + 0.8*(g == "2") + 0.6*x + -0.5*x*(g == "2") + rnorm(n, sd = 1.1) d <- data.frame(y, g, x) # fit correct model m1 <- lm(y ~ g + x + g:x, data = d) sm1 <- summary(m1) sm2 <- coeftest(m1, vcov. = vcovHC(m1)) # get p-values using usual SE and robust ES c(usual = sm1$coefficients[4,4], robust = sm2[4,4]) } # run the function 1000 times r_out <- replicate(n = 1000, expr = f1()) # get proportion of times we correctly reject Null of no interaction at p < 0.05 # (ie, estimate power) apply(r_out, 1, function(x)mean(x < 0.05)) ## usual robust ## 0.835 0.789
The proportion of times we reject the null of no interaction using robust standard errors is lower than simply using the usual standard errors, which means we have a loss of power. (Though admittedly, the loss of power in this simulation is rather small.)
The second simulation is much like the first, except now we fit the wrong model and get biased estimates.
# generate data g <- gl(n = 2, k = 25) x <- rnorm(50, mean = 10, sd = 2) y <- 0.2 + 1.8*(g == "2") + 1.6*x + -2.5*x*(g == "2") + rnorm(50, sd = 1.1) d <- data.frame(y, g, x) # fit the wrong model: a polynomial in x without an interaction with g m1 <- lm(y ~ poly(x, 2), data = d) # use the wrong model to simulate 50 sets of data sim1 <- simulate(m1, nsim = 50) # plot a density curve of the original data plot(density(d$y)) # overlay estimates from the wrong model for(i in 1:50)lines(density(sim1[[i]]), col = "grey80")
We see the simulated data from the wrong model is severely biased and is consistently over- or under-estimating the response. In this case robust standard errors would not be useful because our model is wrong.
Related to this last point, Freedman (2006) expresses skepticism about even using robust standard errors:
If the model is nearly correct, so are the usual standard errors, and robustification is unlikely to help much. On the other hand, if the model is seriously in error, the sandwich may help on the variance side, but the parameters being estimated…are likely to be meaningless – except perhaps as descriptive statistics.
There is much to think about before using robust standard errors. But hopefully you now have a better understanding of what they are and how they’re calculated. Zeileis (2004) provides a deeper and accessible introduction to the sandwich
package, including how to use robust standard errors for addressing suspected autocorrelation.
References
- Freedman DA (2006). “On the So-called ‘Huber Sandwich Estimator’ and ‘Robust Standard Errors’.” Lecture Notes. URL http://www.stat.berkeley.edu/~census/mlesan.pdf.
- R Core Team (2020). R: A language and environment for statistical computing. R Foundation for Statistical Computing, Vienna, Austria. URL https://www.R-project.org/.
- StataCorp. 2019. Stata Statistical Software: Release 16. College Station, TX: StataCorp LLC.
- StataCorp. 2019. Stata 16 Base Reference Manual. College Station, TX: Stata Press.
- Zeileis A, Hothorn T (2002). Diagnostic Checking in Regression Relationships. R News 2(3), 7-10. URL https://CRAN.R-project.org/doc/Rnews/
- Zeileis A (2004). “Econometric Computing with HC and HAC Covariance Matrix Estimators.” Journal of Statistical Software, 11(10), 1-17. doi: 10.18637/jss.v011.i10 (URL: https://doi.org/10.18637/jss.v011.i10).
- Zeileis A (2006). “Object-Oriented Computation of Sandwich Estimators.” Journal of Statistical Software, 16(9), 1-16. doi: 10.18637/jss.v016.i09 (URL: https://doi.org/10.18637/jss.v016.i09).
For questions or clarifications regarding this article, contact the UVA Library StatLab: statlab@virginia.edu
View the entire collection of UVA Library StatLab articles.
Clay FordStatistical Research Consultant
University of Virginia Library
September 27, 2020