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