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)
= read_csv("https://stats.idre.ucla.edu/stat/data/tobit.csv") %>%
acad_apt mutate(prog = factor(prog, labels = c('acad', 'general', 'vocational')))
Setup data and initial values.
= lm(apt ~ read + math + prog, data = acad_apt)
initmod = model.matrix(initmod)
X = c(coef(initmod), log_sigma = log(summary(initmod)$sigma)) init
Function
<- function(par, X, y, ul = -Inf, ll = Inf) {
tobit_ll
# this function only takes a lower OR upper limit
# parameters
= exp(par[length(par)])
sigma = par[-length(par)]
beta
# create indicator depending on chosen limit
if (!is.infinite(ll)) {
= ll
limit = y > ll
indicator else {
} = ul
limit = y < ul
indicator
}
# linear predictor
= X %*% beta
lp
# log likelihood
= sum(indicator * log((1/sigma)*dnorm((y-lp)/sigma)) ) +
ll sum((1-indicator) * log(pnorm((lp-limit)/sigma, lower = is.infinite(ll))))
-ll
}
Estimation
Estimate via optim.
= optim(
fit_tobit 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)
= AER::tobit(
fit_aer ~ read + math + prog,
apt 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)
= AER::tobit(
fit_aer ~ read + math + prog,
apt 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.
= survreg(Surv(apt, apt < 800, type = 'right') ~ read + math + prog,
fit_surv 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.
= optim(
fit_tobit par = init,
tobit_ll,y = acad_apt$apt2,
X = X,
ll = 400,
method = 'BFGS',
control = list(maxit = 2000, reltol = 1e-15)
)
= AER::tobit(apt2 ~ read + math + prog,
fit_aer 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