## ----setup, include = FALSE------------------------------------------------------------- knitr::opts_chunk$set(collapse = TRUE, comment = "#>", message = FALSE) library(estimatr) options(digits = 4) ## --------------------------------------------------------------------------------------- CHECKS <- list() check <- function(label, ours, theirs, tol = 1e-10) { gap <- max(abs(ours - theirs) / pmax(abs(theirs), 1)) # Two jobs: record the gap in the running list for the final table, and # return a one-row data frame so the calling chunk prints its own result. CHECKS[[label]] <<- gap data.frame(gap = sprintf("%.1e", gap), holds = gap < tol) } ## --------------------------------------------------------------------------------------- set.seed(20260826) N <- 100 d <- data.frame( z = rbinom(N, 1, 0.5), x = rnorm(N), w = runif(N, 0.5, 2), g = rep(1:10, each = 10), cl = rep(1:20, each = 5), inst = rnorm(N) ) d$en <- d$inst + rnorm(N, 0, 0.5) d$y <- 1 + 0.5 * d$z + 2 * d$x + d$en + rnorm(N) ## --------------------------------------------------------------------------------------- check("lm_robust()", coef(lm_robust(y ~ z + x, data = d)), coef(lm(y ~ z + x, data = d))) ## --------------------------------------------------------------------------------------- check("lm_robust(weights = )", coef(lm_robust(y ~ z + x, data = d, weights = w)), coef(lm(y ~ z + x, data = d, weights = w))) ## --------------------------------------------------------------------------------------- hc_vcov <- function(fit, type) { X <- model.matrix(fit) e <- residuals(fit) bread <- solve(crossprod(X)) h <- rowSums((X %*% bread) * X) n <- nrow(X) k <- ncol(X) adj <- switch(type, HC0 = e^2, HC1 = e^2 * n / (n - k), HC2 = e^2 / (1 - h), HC3 = e^2 / (1 - h)^2 ) bread %*% crossprod(X * sqrt(adj)) %*% bread } ## --------------------------------------------------------------------------------------- fit_lm <- lm(y ~ z + x, data = d) check("lm_robust(se_type = 'classical')", lm_robust(y ~ z + x, data = d, se_type = "classical")$vcov, vcov(fit_lm)) do.call(rbind, lapply(c("HC0", "HC1", "HC2", "HC3"), function(ty) { cbind(se_type = ty, check(paste0("lm_robust(se_type = '", ty, "')"), lm_robust(y ~ z + x, data = d, se_type = ty)$vcov, hc_vcov(fit_lm, ty))) })) ## --------------------------------------------------------------------------------------- cr_vcov <- function(fit, cluster, stata = FALSE) { X <- model.matrix(fit) e <- residuals(fit) bread <- solve(crossprod(X)) meat <- Reduce(`+`, lapply(split(seq_len(nrow(X)), cluster), function(i) { tcrossprod(crossprod(X[i, , drop = FALSE], e[i])) })) v <- bread %*% meat %*% bread if (!stata) return(v) S <- length(unique(cluster)) v * (S / (S - 1)) * ((nrow(X) - 1) / (nrow(X) - ncol(X))) } ## --------------------------------------------------------------------------------------- check("lm_robust(clusters = )", lm_robust(y ~ z + x, data = d, clusters = cl, se_type = "CR0")$vcov, cr_vcov(fit_lm, d$cl)) check("lm_robust(se_type = 'stata')", lm_robust(y ~ z + x, data = d, clusters = cl, se_type = "stata")$vcov, cr_vcov(fit_lm, d$cl, stata = TRUE)) ## --------------------------------------------------------------------------------------- absorbed <- lm_robust(y ~ z + x, data = d, fixed_effects = ~ g) dummies <- lm_robust(y ~ z + x + factor(g), data = d) keep <- c("z", "x") check("lm_robust(fixed_effects = )", c(coef(absorbed)[keep], absorbed$std.error[keep]), c(coef(dummies)[keep], dummies$std.error[keep])) ## --------------------------------------------------------------------------------------- lm_robust(mpg ~ hp, data = mtcars, weights = wt, se_type = "HC2")$std.error ## --------------------------------------------------------------------------------------- dd <- d dd$x_c <- dd$x - mean(dd$x) lin <- lm_lin(y ~ z, covariates = ~ x, data = d) byhand <- lm_robust(y ~ z * x_c, data = dd) check("lm_lin()", c(coef(lin)[["z"]], lin$std.error[["z"]]), c(coef(byhand)[["z"]], byhand$std.error[["z"]])) ## --------------------------------------------------------------------------------------- tsls_coef <- function(y, X, Z) { xhat <- Z %*% solve(crossprod(Z), crossprod(Z, X)) as.vector(solve(crossprod(xhat), crossprod(xhat, y))) } check("iv_robust()", unname(coef(iv_robust(y ~ en + x | inst + x, data = d))), tsls_coef(d$y, model.matrix(~ en + x, d), model.matrix(~ inst + x, d))) ## --------------------------------------------------------------------------------------- fit <- lm_robust(y ~ z + x, data = d) lh <- lh_robust(y ~ z + x, data = d, linear_hypothesis = "z + x = 0") a <- c(0, 1, 1) check("lh_robust()", c(lh$lh$coefficients[[1]], lh$lh$std.error[[1]]), c(sum(a * coef(fit)), sqrt(drop(t(a) %*% fit$vcov %*% a)))) ## --------------------------------------------------------------------------------------- dim_fit <- difference_in_means(y ~ z, data = d) ols <- lm_robust(y ~ z, data = d, se_type = "HC2") check("difference_in_means()", c(dim_fit$coefficients[["z"]], dim_fit$std.error[["z"]]), c(ols$coefficients[["z"]], ols$std.error[["z"]])) ## --------------------------------------------------------------------------------------- blocked <- data.frame(bl = rep(1:10, each = 10), z = rep(rep(0:1, each = 5), times = 10)) blocked$y <- rnorm(100) + 0.3 * blocked$z difference_in_means(y ~ z, data = blocked, blocks = bl)$design pairs <- data.frame(bl = rep(1:50, each = 2), z = rep(c(0, 1), 50)) pairs$y <- rnorm(100) + 0.3 * pairs$z difference_in_means(y ~ z, data = pairs, blocks = bl)$design # Both kinds of block in one design: 1.x applied the matched-pairs estimator # to all of it, after a warning. hybrid <- rbind(blocked, transform(pairs, bl = bl + 100)) difference_in_means(y ~ z, data = hybrid, blocks = bl)$design ## --------------------------------------------------------------------------------------- ht_estimate <- function(y, z, pr) { mean(y * z / pr) - mean(y * (1 - z) / (1 - pr)) } pr <- rep(0.5, N) check("horvitz_thompson()", horvitz_thompson(y ~ z, data = d, condition_prs = pr)$coefficients[[1]], ht_estimate(d$y, d$z, pr)) ## --------------------------------------------------------------------------------------- Y1 <- d$y[d$z == 1] / 0.5 Y0 <- d$y[d$z == 0] / 0.5 check("horvitz_thompson() variance, simple randomization", horvitz_thompson(y ~ z, data = d, condition_prs = pr)$std.error[[1]], sqrt((sum(Y1^2) + sum(Y0^2)) / N^2)) ## ----eval = requireNamespace("randomizr", quietly = TRUE), warning = FALSE-------------- library(randomizr) set.seed(2) decl <- declare_ra(blocks = rep(c("a", "b", "c", "d"), each = 50), prob = 0.4) Z <- conduct_ra(decl) dat_ht <- data.frame(Y = rnorm(200) + 0.5 * Z, Z = Z) # The design-aware variance horvitz_thompson(Y ~ Z, data = dat_ht, condition_prs = decl)$std.error # The conservative bound, from the marginals alone horvitz_thompson(Y ~ Z, data = dat_ht, condition_prs = c("0" = 0.6, "1" = 0.4))$std.error ## ----echo = FALSE----------------------------------------------------------------------- knitr::kable( data.frame( Promise = names(CHECKS), `Largest relative gap` = sprintf("%.1e", unlist(CHECKS)), Holds = unlist(CHECKS) < 1e-10, check.names = FALSE ), row.names = FALSE ) ## --------------------------------------------------------------------------------------- stopifnot(all(unlist(CHECKS) < 1e-10))