Tobit Regression
The following is a simple demonstration of tobit regression via maximum likelihood. The issue is one where data is censored such that while we observe the value, it is not the true value, which would extend beyond the range of the observed data. This is very commonly seen in cases where the dependent variable has been given some arbitrary cutoff at the lower or upper end of the range, often resulting in floor or ceiling effects respectively. The conceptual idea is that we are interested in modeling the underlying latent variable that would not have such restriction if it was actually observed.
Censoring with an Upper Limit
Data Setup
Data regards academic aptitude (GRE scores) with will be modeled using reading and math test scores, as well as the type of program the student is enrolled in (academic, general, or vocational). See this for an applied example and more detail.
library(tidyverse)
acad_apt = read_csv("https://stats.idre.ucla.edu/stat/data/tobit.csv") %>%
mutate(prog = factor(prog, labels = c('acad', 'general', 'vocational')))Setup data and initial values.
initmod = lm(apt ~ read + math + prog, data = acad_apt)
X = model.matrix(initmod)
init = c(coef(initmod), log_sigma = log(summary(initmod)$sigma))Function
tobit_ll <- function(par, X, y, ul = -Inf, ll = Inf) {
# this function only takes a lower OR upper limit
# parameters
sigma = exp(par[length(par)])
beta = par[-length(par)]
# create indicator depending on chosen limit
if (!is.infinite(ll)) {
limit = ll
indicator = y > ll
} else {
limit = ul
indicator = y < ul
}
# linear predictor
lp = X %*% beta
# log likelihood
ll = sum(indicator * log((1/sigma)*dnorm((y-lp)/sigma)) ) +
sum((1-indicator) * log(pnorm((lp-limit)/sigma, lower = is.infinite(ll))))
-ll
}Estimation
Estimate via optim.
fit_tobit = optim(
par = init,
tobit_ll,
y = acad_apt$apt,
X = X,
ul = 800,
method = 'BFGS',
control = list(maxit = 2000, reltol = 1e-15)
)
# this would be more akin to the default Stata default approach
# optim(
# par = init,
# tobit_ll,
# y = acad_apt$apt,
# X = X,
# ul = 800,
# control = list(maxit = 16000, reltol = 1e-15)
# )Comparison
Compare to AER package tobit function.
library(survival)
fit_aer = AER::tobit(
apt ~ read + math + prog,
data = acad_apt,
left = -Inf,
right = 800
)| (Intercept) | read | math | proggeneral | progvocational | sigma.log_sigma | logLike | |
|---|---|---|---|---|---|---|---|
| fit_tobit | 209.566 | 2.698 | 5.914 | -12.716 | -46.143 | 65.677 | -1041.063 |
| AER | 209.566 | 2.698 | 5.914 | -12.715 | -46.144 | 65.677 | -1041.063 |
library(survival)
fit_aer = AER::tobit(
apt ~ read + math + prog,
data = acad_apt,
left = -Inf,
right = 800
)AER is actually just using survreg from the survival package. Survival models are usually for modeling time to some event, e.g. death in medical studies, and the censoring comes from the fact that the observed event does not occur for some people. Like our tobit function, an indicator is needed to denote who is or isn’t censored. In survival models, the indicator is for the event itself, and means they are NOT censored. So we’ll reverse the indicator used in the tobit function for survreg.
fit_surv = survreg(Surv(apt, apt < 800, type = 'right') ~ read + math + prog,
data = acad_apt,
dist = 'gaussian')Compare all results.
| (Intercept) | read | math | proggeneral | progvocational | sigma.log_sigma | logLike | |
|---|---|---|---|---|---|---|---|
| fit_tobit | 209.566 | 2.698 | 5.914 | -12.716 | -46.143 | 65.677 | -1041.063 |
| AER | 209.566 | 2.698 | 5.914 | -12.715 | -46.144 | 65.677 | -1041.063 |
| survival | 209.566 | 2.698 | 5.914 | -12.715 | -46.144 | 65.677 | -1041.063 |
Censoring with a Lower Limit
Create a censored data situation for the low end. The scale itself would be censored for anyone scoring a 200, but that basically doesn’t happen. In this data, 15 are less than a score of 500, so we’ll do that.
acad_apt = acad_apt %>%
mutate(apt2 = apt,
apt2 = if_else(apt2 < 500, 500, apt2))Estimate and use AER for comparison.
fit_tobit = optim(
par = init,
tobit_ll,
y = acad_apt$apt2,
X = X,
ll = 400,
method = 'BFGS',
control = list(maxit = 2000, reltol = 1e-15)
)
fit_aer = AER::tobit(apt2 ~ read + math + prog,
data = acad_apt,
left = 400)Comparison
| (Intercept) | read | math | proggeneral | progvocational | sigma.log_sigma | logLike | |
|---|---|---|---|---|---|---|---|
| fit_tobit | 270.408 | 2.328 | 5.086 | -11.331 | -38.606 | 57.024 | -1092.483 |
| AER | 270.409 | 2.328 | 5.085 | -11.331 | -38.606 | 57.024 | -1092.483 |
Source
Original code available at https://github.com/m-clark/Miscellaneous-R-Code/blob/master/ModelFitting/tobit.R