tests/testthat/test-iv-robust.R

context("Estimator - iv_robust")

N <- 20
dat <- data.frame(
  y = rnorm(N),
  x = rnorm(N),
  x2 = rnorm(N),
  z2 = rnorm(N),
  w = runif(N),
  clust = sample(letters[1:3], size = N, replace = TRUE)
)
dat$z <- dat$x * 0.5 + rnorm(N)
dat$x1_c <- dat$x

test_that("iv_robust warnings and errors are correct", {
  expect_warning(
    ivro <- iv_robust(mpg ~ hp + cyl | am, data = mtcars, se_type = "HC0"),
    "More regressors than instruments"
  )

  expect_error(
    iv_robust(mpg ~ hp + cyl, data = mtcars),
    "Must specify a `formula` with both regressors and instruments."
  )
})

test_that("iv_robust matches AER + ivpack", {

  skip_if_not_installed("AER")
  skip_if_not_installed("ivpack")
  skip("ivpack is not available")

  ivco <- iv_robust(y ~ x | z, data = dat, se_type = "classical")
  ivfit <- AER::ivreg(y ~ x | z, data = dat)
  ivo <- summary(ivfit)

  expect_equivalent(
    as.matrix(tidy(ivco)[, c("estimate", "std.error", "statistic", "p.value")]),
    coef(ivo)[, 1:4]
  )
  # Same as stata if you specify `small` as a stata option
  # which applies the N / N-k finite sample correction

  expect_equivalent(
    ivfit$fitted.values,
    ivco$fitted.values
  )

  # Stata defaults to HC0 as well, but does HC1 with `small`
  ivro <- iv_robust(y ~ x | z, data = dat, se_type = "HC0")
  capture_output(ivpackrob <- ivpack::robust.se(ivfit))

  expect_equivalent(
    as.matrix(tidy(ivro)[, c("estimate", "std.error", "statistic", "p.value")]),
    ivpackrob[, 1:4]
  )

  expect_equivalent(
    ivfit$fitted.values,
    ivro$fitted.values
  )

  # "Stata" clustered SEs are CR0, but they are the same as below with `small`
  ivclusto <- iv_robust(y ~ x | z, data = dat, se_type = "stata", clusters = clust)
  capture_output(ivpackclust <- ivpack::cluster.robust.se(ivfit, dat$clust))

  # Our p-values are bigger (ivpack is be using less conservative DF, we use J - 1 which
  # is what stata uses for clusters w/ `small` and in OLS)
  expect_equivalent(
    as.matrix(tidy(ivclusto)[, c("estimate", "std.error")]),
    ivpackclust[, c(1, 2)]
  )

  expect_equivalent(
    ivfit$fitted.values,
    ivclusto$fitted.values
  )

  # Weighting classical
  ivw <- AER::ivreg(y ~ x | z, data = dat, weights = w)
  ivcw <- iv_robust(y ~ x | z, data = dat, weights = w, se_type = "classical")
  ivregsum <- summary(ivcw)

  expect_equivalent(
    as.matrix(tidy(ivcw)[, c("estimate", "std.error", "statistic", "p.value")]),
    coef(ivregsum)[, 1:4]
  )

  expect_equivalent(
    ivw$fitted.values,
    ivcw$fitted.values
  )

  # HC0 weighted
  ivrw <- iv_robust(y ~ x | z, data = dat, weights = w, se_type = "HC0")
  capture_output(ivpackrobw <- ivpack::robust.se(ivw))

  expect_equivalent(
    as.matrix(tidy(ivrw)[, c("estimate", "std.error", "statistic", "p.value")]),
    ivpackrobw[, 1:4]
  )

  expect_equivalent(
    ivrw$fitted.values,
    ivcw$fitted.values
  )

  # Stata weighted
  ivclrw <- iv_robust(y ~ x | z, data = dat, clusters = clust, weights = w, se_type = "stata")
  ivclw <- AER::ivreg(y ~ x | z, data = dat, weights = w)
  capture_output(ivclwse <- ivpack::cluster.robust.se(ivclw, clusterid = dat$clust))

  expect_equivalent(
    as.matrix(tidy(ivclrw)[1:2, c("estimate", "std.error")]),
    ivclwse[, c(1, 2)]
  )

  expect_equivalent(
    ivclrw$fitted.values,
    ivclw$fitted.values
  )

  # Rank-deficiency
  # HC0
  ivdefr <- iv_robust(y ~ x + x1_c| z + z2, data = dat, se_type = "HC0")
  ivdef <- AER::ivreg(y ~ x + x1_c| z + z2, data = dat)
  capture_output(ivdefse <- ivpack::robust.se(ivdef))

  expect_equal(
    coef(ivdefr),
    coef(ivdef)
  )

  expect_equivalent(
    as.matrix(tidy(ivdefr)[1:2, c("estimate", "std.error", "statistic", "p.value")]),
    ivdefse[, 1:4]
  )

  expect_equivalent(
    ivdefr$fitted.values,
    ivdef$fitted.values
  )

  # # Does not work if instrument is collinear with other instrument
  # ivdefri <- iv_robust(y ~ z + z2| x + x1_c, data = dat, se_type = "HC0")
  # ivdefi <- AER::ivreg(y ~ z + z2| x + x1_c, data = dat)
  # ivdefsei <- ivpack::robust.se(ivdefi)
  #
  # # No longer equal!
  # expect_equal(
  #   coef(ivdefri),
  #   coef(ivdefi)
  # )

  # expect_equivalent(
  #   as.matrix(tidy(ivdefri)[1:2, c("estimate", "std.error", "statistic", "p.value")]),
  #   ivdefsei[, 1:4]
  # )

  # Stata
  ivdefclr <- iv_robust(y ~ x + x1_c | z + z2, data = dat, clusters = clust, se_type = "stata")
  ivdefcl <- AER::ivreg(y ~ x + x1_c | z + z2, data = dat)
  capture_output(ivdefclse <- ivpack::cluster.robust.se(ivdefcl, clusterid = dat$clust))

  expect_equal(
    coef(ivdefclr),
    coef(ivdefcl)
  )

  expect_equivalent(
    as.matrix(tidy(ivdefclr)[1:2, c("estimate", "std.error")]),
    ivdefclse[, c(1, 2)]
  )

  expect_equivalent(
    ivdefclr$fitted.values,
    ivdefcl$fitted.values
  )

  # HC0 Weighted
  ivdefrw <- iv_robust(y ~ x + x1_c| z + z2, weights = w, data = dat, se_type = "HC0")
  ivdefw <- AER::ivreg(y ~ x + x1_c| z + z2, weights = w, data = dat)
  capture_output(ivdefsew <- ivpack::robust.se(ivdefw))

  expect_equal(
    coef(ivdefrw),
    coef(ivdefw)
  )

  expect_equivalent(
    as.matrix(tidy(ivdefrw)[1:2, c("estimate", "std.error", "statistic", "p.value")]),
    ivdefsew[, 1:4]
  )

  expect_equivalent(
    ivdefrw$fitted.values,
    ivdefw$fitted.values
  )

  # Stata weighted
  ivdefclr <- iv_robust(y ~ x + x1_c | z + z2, data = dat, weights = w, clusters = clust, se_type = "stata")
  ivdefcl <- AER::ivreg(y ~ x + x1_c | z + z2, data = dat, weights = w)
  capture_output(ivdefclse <- ivpack::cluster.robust.se(ivdefcl, clusterid = dat$clust))

  expect_equal(
    coef(ivdefclr),
    coef(ivdefcl)
  )

  expect_equivalent(
    as.matrix(tidy(ivdefclr)[1:2, c("estimate", "std.error")]),
    ivdefclse[, c(1, 2)]
  )

  expect_equivalent(
    ivdefclr$fitted.values,
    ivdefcl$fitted.values
  )

  # F-stat fails properly with blocks of size 1
  set.seed(42)
  N <- 20
  dat <- data.frame(y = rnorm(N), x = rnorm(N), z = rnorm(N), bl = sample(letters, size = N, replace = T))
  ivr <- iv_robust(y ~ bl + x | bl + z, data = dat, se_type = "stata")
  expect_equivalent(
    ivr$fstatistic[1],
    NA_integer_
  )

})

test_that("iv_robust matches AER + clubSandwich", {
  skip_if_not_installed("AER")
  skip_if_not_installed("clubSandwich")
  skip_on_cran()

  # ClubSandwich IV tests
  for (se_type in cr_se_types) {

    ivfit <- AER::ivreg(y ~ x | z, data = dat)
    ivfitw <- AER::ivreg(y ~ x | z, weights = w, data = dat)

    # Standard IV models
    ivcr <- iv_robust(y ~ x | z, data = dat, clusters = clust, se_type = se_type)
    clubsand <- clubSandwich::coef_test(ivfit,
                                        vcov = ifelse(se_type == "stata", "CR1S", se_type),
                                        cluster = dat$clust,
                                        test = ifelse(se_type == "CR2", "Satterthwaite", "naive-t"))

    clubsand <- as.data.frame(clubsand)

    cols <- c("estimate", "std.error", "statistic", "df", "p.value")

    expect_equivalent(
      as.matrix(tidy(ivcr)[, cols]),
      as.matrix(clubsand[,-1])
    )

    expect_equivalent(
      ivfit$fitted.values,
      ivcr$fitted.values
    )

    # Weighted IV models
    ivcrw <- iv_robust(y ~ x | z, data = dat, clusters = clust, weights = w, se_type = se_type)
    clubsandw <- clubSandwich::coef_test(ivfitw,
                                         vcov = ifelse(se_type == "stata", "CR1S", se_type),
                                         cluster = dat$clust,
                                         test = ifelse(se_type == "CR2", "Satterthwaite", "naive-t"))

    clubsandw <- as.data.frame(clubsandw)

    expect_equivalent(
      as.matrix(tidy(ivcrw)[, cols]),
      as.matrix(clubsandw[,-1])
    )

    expect_equivalent(
      ivfitw$fitted.values,
      ivcrw$fitted.values
    )

    # Rank-deficiency
    ivfit_rd <- AER::ivreg(y ~ x + x1_c | z + z2, data = dat)
    ivcr_rd <- iv_robust(y ~ x + x1_c | z + z2, data = dat, clusters = clust, se_type = se_type)
    clubsand_rd <- clubSandwich::coef_test(ivfit_rd,
                                           vcov = ifelse(se_type == "stata", "CR1S", se_type),
                                           cluster = dat$clust,
                                           test = ifelse(se_type == "CR2", "Satterthwaite", "naive-t"))

    clubsand_rd <- as.data.frame(clubsand_rd)

    expect_equivalent(
      na.omit(as.matrix(tidy(ivcr_rd)[, cols])),
      na.omit(as.matrix(clubsand_rd[,-1]))
    )

    expect_equivalent(
      ivfit_rd$fitted.values,
      ivcr_rd$fitted.values
    )

    # Rank-deficient, weighted
    ivfitw_rd <- AER::ivreg(y ~ x + x1_c | z + z2, weights = w, data = dat)
    ivcrw_rd <- iv_robust(y ~ x + x1_c | z + z2, data = dat, weights = w, clusters = clust, se_type = se_type)
    clubsandw_rd <- clubSandwich::coef_test(ivfitw_rd,
                                            vcov = ifelse(se_type == "stata", "CR1S", se_type),
                                            cluster = dat$clust,
                                            test = ifelse(se_type == "CR2", "Satterthwaite", "naive-t"))

    clubsandw_rd <- as.data.frame(clubsandw_rd)

    expect_equivalent(
      na.omit(as.matrix(tidy(ivcrw_rd)[, cols])),
      na.omit(as.matrix(clubsandw_rd[,-1]))
    )

    expect_equivalent(
      ivfitw_rd$fitted.values,
      ivcrw_rd$fitted.values
    )
  }

})

test_that("iv_robust different specifications work", {
  skip_if_not_installed("AER")
  skip_if_not_installed("ivpack")
  skip("ivpack not available")

  # More instruments than endog. regressors
  ivro <- iv_robust(mpg ~ wt | hp + cyl, data = mtcars, se_type = "HC0")
  ivo <- AER::ivreg(mpg ~ wt | hp + cyl, data = mtcars)
  capture_output(ivpo <- ivpack::robust.se(ivo))
  expect_equivalent(
    as.matrix(tidy(ivro)[, c("estimate", "std.error", "statistic", "p.value")]),
    ivpo[, 1:4]
  )

  # . notation for multiple exog, doesnt work!
  # ivro <- iv_robust(mpg ~ wt + hp + vs | . - vs + cyl, data = mtcars, se_type = "HC0")
  # ivo <- AER::ivreg(mpg ~ wt + hp + vs | . - vs + cyl, data = mtcars)
  # ivpo <- ivpack::robust.se(ivo)
  # expect_equivalent(
  #   as.matrix(tidy(ivro)[, c("estimate", "std.error", "statistic", "p.value")]),
  #   ivpo[, 1:4]
  # )

  # . notation in general
  ivro <- iv_robust(mpg ~ .| ., data = mtcars, se_type = "HC0")
  ivo <- AER::ivreg(mpg ~ . | ., data = mtcars)
  capture_output(ivpo <- ivpack::robust.se(ivo))

  expect_equivalent(
    as.matrix(tidy(ivro)[, c("estimate", "std.error", "statistic", "p.value")]),
    ivpo[, 1:4]
  )

})

test_that("S3 methods", {
  skip_if_not_installed("AER")


  ivo <- AER::ivreg(mpg ~ hp + cyl | wt + gear, data = mtcars)
  ivro <- iv_robust(mpg ~ hp + cyl | wt + gear, data = mtcars, se_type = "classical")

  expect_equal(
    vcov(ivro),
    vcov(ivo)
  )

  expect_is(
    tidy(ivro),
    "data.frame"
  )

  expect_equal(
    nrow(tidy(ivro)),
    3
  )

  summary(ivro)

  siv <- capture_output(
    summary(ivro),
    print = TRUE
  )

  expect_true(
    grepl(
      "iv\\_robust\\(formula = mpg \\~ hp \\+ cyl \\| wt \\+ gear, data = mtcars,",
      siv
    )
  )

  expect_true(
    grepl(
      "F\\-statistic\\: 33\\.73 on 2 and 29 DF,  p\\-value\\: 2\\.706e\\-08",
      siv
    )
  )

  capture_output(
    expect_equivalent(
      coef(summary(ivro)),
      print(ivro)
    )
  )

  expect_equivalent(
    ivro$fstatistic,
    summary(ivo)$waldtest[-2]
  )

  expect_equal(
    predict(ivro, newdata = mtcars),
    predict(ivo)
  )

  glance_ivro <- glance(ivro)

  expect_equal(nrow(glance_ivro), 1)

  expect_equal(
    colnames(glance(ivro)),
    c("r.squared", "adj.r.squared", "df.residual", "nobs", "se_type",
      "statistic", "p.value", "statistic.weakinst", "p.value.weakinst",
      "statistic.endogeneity", "p.value.endogeneity", "statistic.overid",
      "p.value.overid")
  )

  # no intercept
  ivo <- AER::ivreg(mpg ~ hp + cyl + 0 | wt + gear, data = mtcars)
  ivro <- iv_robust(mpg ~ hp + cyl + 0 | wt + gear, data = mtcars, se_type = "classical")

  expect_equivalent(
    ivro$fstatistic,
    summary(ivo)$waldtest[-2]
  )

})

test_that("IV diagnostics", {

  skip_if_not_installed("AER")

  # Load stata diagnostics
  stata_diags <- read.table(
    "stata-iv-diagnostics.txt",
    col.names = c("formula", "weights", "options", "diag", "df1", "df2", "val", "pval"),
    sep = ";",
    na = ".",
    stringsAsFactors = FALSE
  )

  formulae <- c(
    "(hp = wt)" = mpg ~ hp | wt,
    # mpg ~ 0 + hp | wt,
    "(hp = wt)0" = mpg ~ 0 + hp | 0 + wt,
    "(hp am = wt gear)" = mpg ~ hp + am | wt + gear,
    # mpg ~ 0 + hp + am | wt + gear,
    "(hp am = wt gear)0" = mpg ~ 0 + hp + am | 0 + wt + gear,
    "gear (hp = wt)" = mpg ~ hp + gear | wt + gear,
    # mpg ~ 0 + hp + gear | wt + gear,
    "gear (hp = wt)0" = mpg ~ 0 + hp + gear | 0 + wt + gear,
    "gear (hp = wt am)" = mpg ~ hp + gear | wt + gear + am,
    # mpg ~ 0 + hp + gear | wt + gear + am,
    # mpg ~ hp + gear | 0 + wt + gear + am,
    "gear (hp = wt am)0" = mpg ~ 0 + hp + gear | 0 + wt + gear + am
  )

  for (f_n in names(formulae)) {
    f <- formulae[[f_n]]
    ivro <- iv_robust(f, data = mtcars, se_type = "classical", diagnostics = TRUE)
    aer_ivro <- summary(AER::ivreg(f, data = mtcars), diagnostics = TRUE)

    # Sargan stat seems to be wrong for AER for this model (-ve critical value)
    if (f_n == "gear (hp = wt am)0") {
      expect_equivalent(
        build_ivreg_diagnostics_mat(ivro)[1:2, ],
        aer_ivro[["diagnostics"]][1:2, ]
      )
    } else {
      expect_equivalent(
        build_ivreg_diagnostics_mat(ivro),
        aer_ivro[["diagnostics"]]
      )
    }

    stata_diag <- subset(
      stata_diags,
      formula == gsub("0", "", f_n) & (grepl("noconstant", options) == grepl("0", f_n))
    )

    expect_equivalent(
      build_ivreg_diagnostics_mat(ivro, stata = TRUE),
      as.matrix(stata_diag[
        grepl("small", stata_diag$options) & nchar(stata_diag$weights) == 0,
        c("df1", "df2", "val", "pval")
      ]),
      tolerance = 1e-6
    )

    # With weights, don't match `overid` test, as we don't report it
    ivrow <- iv_robust(f, data = mtcars, se_type = "classical", weights = drat, diagnostics = TRUE)
    ivrow_diag_mat <- build_ivreg_diagnostics_mat(ivrow, stata = TRUE)
    expect_equivalent(ivrow_diag_mat[nrow(ivrow_diag_mat), ], rep(NA_real_, 4))

    expect_equivalent(
      ivrow_diag_mat[-nrow(ivrow_diag_mat), ],
      as.matrix(stata_diag[
        grepl("small", stata_diag$options) & nchar(stata_diag$weights) > 0 & stata_diag$diag != "overid",
        c("df1", "df2", "val", "pval")
      ]),
      tolerance = 1e-6
    )

    ivro_hc1 <- iv_robust(f, data = mtcars, se_type = "HC1", diagnostics = TRUE)
    ivrow_hc1 <- iv_robust(f, data = mtcars, se_type = "HC1", weights = drat, diagnostics = TRUE)

    expect_equivalent(
      build_ivreg_diagnostics_mat(ivro_hc1, stata = TRUE),
      as.matrix(stata_diag[
        grepl("rob", stata_diag$options) & nchar(stata_diag$weights) == 0,
        c("df1", "df2", "val", "pval")
      ]),
      tolerance = 1e-6
    )

    # Again, no overid test reported with weights
    ivrow_hc1_diag_mat <- build_ivreg_diagnostics_mat(ivrow_hc1, stata = TRUE)
    expect_equivalent(ivrow_hc1_diag_mat[nrow(ivrow_hc1_diag_mat), ], rep(NA_real_, 4))

    expect_equivalent(
      ivrow_hc1_diag_mat[-nrow(ivrow_hc1_diag_mat), ],
      as.matrix(stata_diag[
        grepl("rob", stata_diag$options) & nchar(stata_diag$weights) > 0 & stata_diag$diag != "overid",
        c("df1", "df2", "val", "pval")
      ]),
      tolerance = 1e-6
    )

    # tolerance higher here due to larger values in general
    ivro_crs <- iv_robust(f, data = mtcars, se_type = "stata", clusters = cyl, diagnostics = TRUE)
    ivro_crs_diag_mat <- build_ivreg_diagnostics_mat(ivro_crs, stata = TRUE)

    ivrow_crs <- iv_robust(f, data = mtcars, se_type = "stata", clusters = cyl, weights = drat, diagnostics = TRUE)
    ivrow_crs_diag_mat <- build_ivreg_diagnostics_mat(ivrow_crs, stata = TRUE)
    expect_equivalent(ivrow_crs_diag_mat[nrow(ivrow_crs_diag_mat), ], rep(NA_real_, 4))

    expect_equivalent(
      ivro_crs_diag_mat[-nrow(ivro_crs_diag_mat), ],
      as.matrix(stata_diag[
        grepl("cluster", stata_diag$options) & nchar(stata_diag$weights) == 0 & stata_diag$diag != "overid",
        c("df1", "df2", "val", "pval")
      ]),
      tolerance = 1e-3
    )

    # Stata doesn't report overid test with clusters
    expect_equivalent(
      ivrow_crs_diag_mat[-nrow(ivrow_crs_diag_mat), ],
      as.matrix(stata_diag[
        grepl("cluster", stata_diag$options) & nchar(stata_diag$weights) > 0 & stata_diag$diag != "overid",
        c("df1", "df2", "val", "pval")
        ]),
      tolerance = 1e-3
    )

    # Sanity check unmatched diagnostics
    for (se_type in se_types) {
      ivro <- iv_robust(f, data = mtcars, se_type = se_type, diagnostics = TRUE)
      diagnostic_mat <- build_ivreg_diagnostics_mat(ivro)
      expect_true(
        all(diagnostic_mat[1:2, ] > 0) & all(diagnostic_mat[3, ] >= 0 | is.na(diagnostic_mat[3, ]))
      )
    }

    # Test default se_type
    ivro <- iv_robust(f, data = mtcars, diagnostics = TRUE)
    diagnostic_mat <- build_ivreg_diagnostics_mat(ivro)
    expect_true(
      all(diagnostic_mat[1:2, ] > 0) & all(diagnostic_mat[3, ] >= 0 | is.na(diagnostic_mat[3, ]))
    )

    for (cr_se_type in cr_se_types) {
      ivro <- iv_robust(f, data = mtcars, se_type = cr_se_type, clusters = cyl, diagnostics = TRUE)
      diagnostic_mat <- build_ivreg_diagnostics_mat(ivro)
      expect_true(
        all(diagnostic_mat[1:2, ] > 0) & all(diagnostic_mat[3, ] >= 0 | is.na(diagnostic_mat[3, ]))
      )
    }

  }

})
DeclareDesign/DDestimate documentation built on April 1, 2024, 1:24 a.m.