Heteroskedasticity means the spread of the regression error term is not the same across observations, some observations have noisier, more unpredictable errors than others, usually because the variance of the error changes with the level of one or more of the X variables. It does not bias the OLS point estimates themselves (the coefficients are still centered on the truth on average), but it does bias the standard errors OLS reports for those coefficients. Since every t-test, F-test, and confidence interval is built from those standard errors, heteroskedasticity is really an inference problem, not an estimation problem, it is distinct from multicollinearity (which is about correlated regressors inflating variance) and from serial correlation (which is about errors being correlated with each other across time/order rather than having unequal variance).
In both the Breusch-Pagan and White tests, the null hypothesis is homoskedasticity, that the variance of the error term is constant and unrelated to the regressors, and the alternative is heteroskedasticity, i.e. that the error variance varies systematically with (some function of) the regressors. The two tests share this same hypothesis pair; they differ only in how the auxiliary regression models that “function of the regressors.” BP regresses the squared residuals on just the original regressors (a linear form), while White additionally includes their squares and cross-products, which lets it pick up nonlinear and interactive patterns in the variance that BP would miss. The logic is since the true form of heteroskedasticity is rarely known in advance, testing whether any combination of the regressors and their squares/interactions can explain the squared residuals is a reasonable, flexible way to check the constant-variance assumption, and it is exactly why White’s test is generally preferred when the researcher has no strong prior belief about which functional form the heteroskedasticity would take.
I used the built-in swiss dataset (base R
datasets package): socio-economic and fertility indicators
for 47 French-speaking Swiss provinces around 1888. This is a different
dataset than the Boston housing / mtcars / trees / saving examples used
in the reference code.
rm(list = ls())
data("swiss")
help(swiss)
## starting httpd help server ... done
str(swiss) # 47 rows, 6 columns
## 'data.frame': 47 obs. of 6 variables:
## $ Fertility : num 80.2 83.1 92.5 85.8 76.9 76.1 83.8 92.4 82.4 82.9 ...
## $ Agriculture : num 17 45.1 39.7 36.5 43.5 35.3 70.2 67.8 53.3 45.2 ...
## $ Examination : int 15 6 5 12 17 9 16 14 12 16 ...
## $ Education : int 12 9 5 7 15 7 7 8 7 13 ...
## $ Catholic : num 9.96 84.84 93.4 33.77 5.16 ...
## $ Infant.Mortality: num 22.2 22.2 20.2 20.3 20.6 26.6 23.6 24.9 21 24.4 ...
summary(swiss)
## Fertility Agriculture Examination Education
## Min. :35.00 Min. : 1.20 Min. : 3.00 Min. : 1.00
## 1st Qu.:64.70 1st Qu.:35.90 1st Qu.:12.00 1st Qu.: 6.00
## Median :70.40 Median :54.10 Median :16.00 Median : 8.00
## Mean :70.14 Mean :50.66 Mean :16.49 Mean :10.98
## 3rd Qu.:78.45 3rd Qu.:67.65 3rd Qu.:22.00 3rd Qu.:12.00
## Max. :92.50 Max. :89.70 Max. :37.00 Max. :53.00
## Catholic Infant.Mortality
## Min. : 2.150 Min. :10.80
## 1st Qu.: 5.195 1st Qu.:18.15
## Median : 15.140 Median :20.00
## Mean : 41.144 Mean :19.94
## 3rd Qu.: 93.125 3rd Qu.:21.70
## Max. :100.000 Max. :26.60
Main regression specification
\(Fertility_i = \beta_0 + \beta_1\, Agriculture_i + \beta_2\, Examination_i + \beta_3\, Education_i + \epsilon_i\)
where i indexes province. Fertility is a
standardized fertility measure; Agriculture is the % of
males involved in agriculture; Examination is the % of
draftees receiving the highest mark on an army examination;
Education is the % of draftees with education beyond
primary school.
lm.mod <- lm(formula = Fertility ~ Agriculture + Examination + Education,
data = swiss)
summary(lm.mod) # confirm the three independent variables are as intended
##
## Call:
## lm(formula = Fertility ~ Agriculture + Examination + Education,
## data = swiss)
##
## Residuals:
## Min 1Q Median 3Q Max
## -14.967 -4.978 -1.045 4.906 21.358
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 99.80162 7.15523 13.948 < 2e-16 ***
## Agriculture -0.18017 0.08071 -2.232 0.03084 *
## Examination -0.79744 0.24679 -3.231 0.00237 **
## Education -0.67242 0.19366 -3.472 0.00119 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 8.601 on 43 degrees of freedom
## Multiple R-squared: 0.5568, Adjusted R-squared: 0.5259
## F-statistic: 18.01 on 3 and 43 DF, p-value: 1.017e-07
All three regressors are negative and statistically significant predictors of fertility, and the model explains about 56% of the variation in fertility across provinces.
# Residuals vs. fitted values
plot(lm.mod, which = 1)
# Full 2x2 diagnostic panel
par(mfrow = c(2, 2))
plot(lm.mod)
par(mfrow = c(1, 1))
Eyeballing the residuals-vs-fitted plot, the spread of the residuals looks reasonably even across the range of fitted values, there is no obvious funnel or fan shape, though with only 47 observations it is hard to be fully confident from a visual check alone. This is exactly the motivation for a formal statistical test.
skedastic Package# install.packages("skedastic")
library(skedastic)
skedastic_package_white <- white(mainlm = lm.mod,
interactions = TRUE)
skedastic_package_white
(Note: the cloud sandbox used to prepare this file could not
reach CRAN to install skedastic, so the chunk above is set
to eval = FALSE. The manual replication in the next section
computes exactly the same quantities the package reports – White’s test
statistic is simply \(T = n \times
R^2_{aux}\) from the auxiliary regression below, so the numbers
match by construction. Running the chunk above in your own R session,
where skedastic installs normally, will reproduce the
identical statistic and p-value shown below.)
Interpreting the White test output: The test statistic is \(T = n \times R^2_{aux} \approx 8.97\), which under the null of homoskedasticity is distributed \(\chi^2\) with 9 degrees of freedom (the number of slope coefficients in the auxiliary regression). This gives a p-value of about 0.44. Since 0.44 is far above any conventional significance level (e.g. 5%), we fail to reject the null hypothesis of homoskedasticity, there is no statistical evidence of heteroskedasticity in this model.
swiss$residuals <- resid(object = lm.mod)
summary(swiss$residuals) # mean should be ~0
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## -14.967 -4.978 -1.045 0.000 4.906 21.358
swiss$squared_residuals <- (swiss$residuals)^2
summary(swiss$squared_residuals) # should have no negative values
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0.08087 4.95833 25.88224 67.67925 84.96779 456.15013
My three original regressors are Agriculture,
Examination, and Education. Their squares are
created with I(), and the three pairwise cross terms are
created with :.
white_auxiliary_reg <- lm(formula = squared_residuals ~ Agriculture + Examination + Education +
I(Agriculture^2) + I(Examination^2) + I(Education^2) +
Agriculture:Examination + Examination:Education + Education:Agriculture,
data = swiss)
white_auxiliary_reg_summary <- summary(white_auxiliary_reg)
white_auxiliary_reg_summary
##
## Call:
## lm(formula = squared_residuals ~ Agriculture + Examination +
## Education + I(Agriculture^2) + I(Examination^2) + I(Education^2) +
## Agriculture:Examination + Examination:Education + Education:Agriculture,
## data = swiss)
##
## Residuals:
## Min 1Q Median 3Q Max
## -96.10 -52.17 -28.36 22.22 369.85
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -371.46693 380.10903 -0.977 0.335
## Agriculture 12.46896 9.78410 1.274 0.210
## Examination 13.72208 19.98789 0.687 0.497
## Education -1.82631 25.38374 -0.072 0.943
## I(Agriculture^2) -0.08682 0.06418 -1.353 0.184
## I(Examination^2) -0.12782 0.50685 -0.252 0.802
## I(Education^2) -0.03498 0.23584 -0.148 0.883
## Agriculture:Examination -0.23053 0.19580 -1.177 0.247
## Examination:Education 0.14008 0.71403 0.196 0.846
## Agriculture:Education 0.11870 0.29255 0.406 0.687
##
## Residual standard error: 93.78 on 37 degrees of freedom
## Multiple R-squared: 0.1908, Adjusted R-squared: -0.006092
## F-statistic: 0.969 on 9 and 37 DF, p-value: 0.4806
Interpreting the auxiliary R-squared: The auxiliary regression’s R-squared is about 0.19, meaning the original regressors, their squares, and their cross terms together explain only ~19% of the variation in the squared residuals. Intuitively, a low auxiliary R-squared means the regressors do a poor job predicting how large the squared residuals (i.e., the error variance) get, which is consistent with homoskedasticity. Had the R-squared been high, that would mean the error variance is strongly, systematically related to the X’s, which is the signature of heteroskedasticity.
n_aux <- nobs(white_auxiliary_reg)
r2_aux <- white_auxiliary_reg_summary$r.squared
# 3a. Test statistic: T = n * R^2
chisq_test_statistic <- n_aux * r2_aux
chisq_test_statistic
## [1] 8.96533
# 3b. Critical value at alpha = 5%, df = 9 (9 slope coefficients in the auxiliary regression)
chisq_critical_value <- qchisq(p = 0.05, df = 9, lower.tail = FALSE)
chisq_critical_value
## [1] 16.91898
# Decision rule: reject the null (find heteroskedasticity) if test statistic > critical value
chisq_test_statistic > chisq_critical_value
## [1] FALSE
# 3c. p-value of the test statistic
chisq_p_value <- pchisq(q = chisq_test_statistic, df = 9, lower.tail = FALSE)
chisq_p_value
## [1] 0.4404807
The manually computed test statistic (8.965) and p-value (0.4405)
match what the skedastic::white() call would report, since
White’s test is defined as exactly this \(n
\times R^2_{aux}\) statistic compared to a \(\chi^2_9\) distribution.
Conclusion: Because the test statistic (8.97) is
less than the critical value (16.92), and equivalently the
p-value (0.44) is well above 0.05, we fail to reject the null
hypothesis of homoskedasticity. Both the visual residual plot
and the formal White’s test agree: there is no strong evidence of
heteroskedasticity in this fertility regression, so the usual OLS
standard errors are reasonably trustworthy here. (As a robustness check,
we could still report heteroskedasticity-robust standard errors via
sandwich::vcovHC(), they would not differ much from the
default ones given this result.)