#Estimat effects and SEs
est_effect <- function(formula, m, type, measure, nboot = NULL, boot.type = "perc", robust.type = NULL, conf = .95, ...) {
#formula: outcome model. Can include interactions because marginal effects will be computed after
#m: matchit object, with call intact and with match.data available
#type: type of SE/CI. Options: clusterrobust, robust, boot, blockboot
#measure: effect measure. MD, RD, RR, OR, IRR
#nboot: if bootstrap, how many
#boot.type: if bootstrap, what kind of interval
#robust.type: if robust, what type (HC1, etc.)
#conf: confidence level
A <- list(...)
estimand <- m$estimand
call <- m$call
replace <- isTRUE(m$info$replace)
method <- m$info$method
#Extract matched data
if (replace) {
md <- do.call("get_matches", c(list(m), A[names(A) %in% names(formals(get_matches))]))
}
else {
md <- do.call("match.data", c(list(m), A[names(A) %in% names(formals(match.data))]))
}
#Ensure treatment is 0/1
treat.name <- as.character(m$formula[[2]])
md[[treat.name]] <- binarize(md[[treat.name]])
#Ensure treatment is in model formula
tt.rhs <- delete.response(terms(formula))
if (!treat.name %in% rownames(attr(tt.rhs, "factors"))) {
stop(paste0("The treatment variable, ", treat.name, ", must be on the right-hand side of 'formula'."), call. = FALSE)
}
#Check if covariates are present
covs.present <- any(rownames(attr(tt.rhs, "factors")) != treat.name)
#Process outcome and get type
outcome.mf <- model.frame(update(formula, . ~ 0), data = md)
outcome <- model.response(outcome.mf)
if (inherits(outcome, "Surv")) {
out.type <- "survival"
if (missing(measure)) measure <- "HR"
else measure <- match_arg(measure, "HR")
}
else if ((is.factor(outcome) && nlevels(outcome) == 2) || length(unique(outcome)) == 2) {
out.type <- "binary"
if (missing(measure)) measure <- "RD"
else measure <- match_arg(measure, c("RD", "RR", "OR"))
}
else {
out.type <- "continuous"
if (missing(measure)) measure <- "MD"
else measure <- match_arg(measure, "MD")
}
#Process SE type
if (missing(type)) {
if (method %in% c("subclass", "exact", "cem")) type <- "robust"
else type <- "clusterrobust"
}
else {
type <- match_arg(type, c("clusterrobust", "robust", "boot", "blockboot"))
}
if (type %in% c("boot", "blockboot")) {
if (missing(nboot) || is.null(nboot)) stop(paste0("An argument to 'nboot' must be supplied when type = \"", type, "\"."), call. = FALSE)
}
else nboot <- NULL
#Check compatibility and assign glm functions
if (out.type == "continuous") {
fit.fun <- function(f, d, w) {
environment(f) <- environment()
lm(f, data = d, weights = w)
}
}
else if (out.type == "binary") {
if (is.null(nboot) && covs.present) stop("Bootstrapping must be used when including covariates in the model for a binary outcome.\n\tSet 'type' to \"blockboot\" or \"boot\" and supply an argument to 'nboot'.", call. = FALSE)
fit.fun <- function(f, d, w) {
environment(f) <- environment()
if (is.null(nboot)) {
if (measure == "RD") lm(f, data = d, weights = w) #Cheung (2007)
else if (measure == "RR") glm(f, data = d, weights = w, family = quasipoisson(link = "log")) #Chen et al (2018)
else if (measure == "OR") glm(f, data = d, weights = w, family = quasibinomial(link = "logit"))
}
else {
glm(f, data = d, weights = w, family = quasibinomial(link = "logit"))
}
}
}
else if (out.type == "survival") {
if (covs.present) stop("Covariates cannot be included in the outcome model with survival outcomes.", call. = FALSE)
fit.fun <- function(f, d, w) {
environment(f) <- environment()
if (replace && is.null(nboot)) {
#Austin & Cafri's (2020) SE estimator
fs <- survival::coxph(f, data = d, robust = TRUE, weights = w,
cluster = data[[attr(d, "subclass")]])
Vs <- fs$var
ks <- nlevels(data[[attr(d, "subclass")]])
fi <- survival::coxph(f, data = d, robust = TRUE, weights = w,
cluster = data[[attr(d, "id")]])
Vi <- fi$var
ki <- length(unique(data[[attr(d, "id")]]))
fc <- survival::coxph(f, data = d, robust = TRUE, weights = w)
Vc <- fc$var
kc <- nrow(d)
#Compute the variance
V <- (ks/(ks-1))*Vs + (ki/(ki-1))*Vi - (kc/(kc-1))*Vc
#Sneak it back into the fit object
fc$var <- V
}
else {
fc <- survival::coxph(f, data = d, robust = is.null(nboot), weights = w,
cluster = if (type == "clusterrobust") d[[attr(d, "subclass")]])
}
return(fc)
}
}
if (is.null(nboot)) {
w1 <- md[[attr(md, "weights")]]
fit <- fit.fun(formula, md, w1)
if (out.type == "continuous") {
beta <- coef(fit)
if (type == "robust") sigma <- sandwich::vcovHC(fit, type = robust.type)
else if (type == "clusterrobust") {
if (replace) sigma <- sandwich::vcovCL(fit, cluster = md[c(attr(md, "subclass"), attr(md, "id"))], type = robust.type)
else sigma <- sandwich::vcovCL(fit, cluster = md[[attr(md, "subclass")]], type = robust.type)
}
if (estimand == "ATT") {
md <- md[md[[treat.name]] == 1,]
} else if (estimand == "ATC") {
md <- md[md[[treat.name]] == 0,]
}
md[[treat.name]] <- 1
alpha1 <- md[[attr(md, "weights")]] %*% model.matrix(formula, md) / sum(md[[attr(md, "weights")]])
md[[treat.name]] <- 0
alpha0 <- md[[attr(md, "weights")]] %*% model.matrix(formula, md) / sum(md[[attr(md, "weights")]])
alpha_tau <- alpha1 - alpha0
alpha <- rbind(alpha1, alpha0, alpha_tau)
est <- alpha %*% beta
vc <- alpha %*% sigma %*% t(alpha)
se <- sqrt(diag(vc))
df <- fit$df.residual
}
else if (out.type == "binary") {
beta <- coef(fit)
if (replace) sigma <- sandwich::vcovCL(fit, cluster = md[c(attr(md, "subclass"), attr(md, "id"))], type = robust.type)
else sigma <- sandwich::vcovCL(fit, cluster = md[[attr(md, "subclass")]], type = robust.type)
alpha <- matrix(c(1,1,
1,0,
0,1), nrow = 3, byrow = TRUE)
est <- alpha %*% beta
vc <- alpha %*% sigma %*% t(alpha)
se <- sqrt(diag(vc))
df <- Inf
}
else if (out.type == "survival") {
if (!is.null(robust.type)) warning("'robust.type' is ignored with survival outcomes.", call. = FALSE, immediate. = TRUE)
est <- coef(fit)[treat.name]
vc <- vcov(fit)[treat.name, treat.name, drop = FALSE]
se <- sqrt(diag(vc))
df <- Inf
}
#Compute effect measures on link scale
l.effects <- cbind(est,
est + qt(.5*(1-conf), df) * se,
est + qt(1 - .5*(1-conf), df) * se,
se,
tval <- est/se,
2 * pt(abs(tval), df, lower.tail = FALSE))
}
else {
if (!is.null(robust.type)) warning("'robust.type' is ignored when using bootstrapping.", call. = FALSE, immediate. = TRUE)
if (missing(boot.type) || is.null(boot.type)) {
warning("'boot.type' not specificed. Using \"perc\".", call. = FALSE, immediate. = TRUE)
boot.type <- "perc"
}
else {
if (identical(boot.type, "stud")) stop("Studentized bootstrap CIs are not allowed. Set 'boot.type' to something other than \"stud\".", call. = FALSE)
boot.type <- match_arg(boot.type, c("norm", "basic", "perc", "bca"))
}
fun1 <- function(x) switch(measure, MD = x, RD = x, RR = log(x), OR = log(x/(1-x)))
if (type == "boot") {
if (!is.null(m$model)) {
env <- attributes(terms(m$model))$.Environment
} else {
env <- parent.frame()
}
data <- eval(call$data, envir = env)
if (boot.type == "bca" && nboot < nrow(data)) {
stop(paste0("'nboot' cannot be less than the number of rows of the original dataset (", nrow(data),
") when type = \"boot\" and boot.type = \"bca\". Increase 'nboot' or change 'boot.type'."), call. = FALSE)
}
estfun <- function(data, i) {
boot_call <- call
boot_call[["data"]] <- data[i,]
m_boot <- eval(boot_call)
md_boot <- do.call("match.data", c(list(m_boot), A[names(A) %in% names(formals(match.data))]))
fit <- fit.fun(formula, md_boot, md_boot[[attr(md_boot, "weights")]])
if (out.type == "survival") {
return(coef(fit)[treat.name])
}
else {
if (estimand == "ATT") {
md_boot <- md_boot[md_boot[[treat.name]] == 1,]
} else if (estimand == "ATC") {
md_boot <- md_boot[md_boot[[treat.name]] == 0,]
}
md_boot[[treat.name]] <- 1
M1 <- weighted.mean(predict(fit, newdata = md_boot, type = "response"), md_boot[[attr(md_boot, "weights")]])
md_boot[[treat.name]] <- 0
M0 <- weighted.mean(predict(fit, newdata = md_boot, type = "response"), md_boot[[attr(md_boot, "weights")]])
return(c(fun1(M1), fun1(M0), fun1(M1) - fun1(M0)))
}
}
boot.out <- do.call(boot::boot, c(list(data, estfun, R = nboot), A[names(A) %in% names(formals(boot::boot))]))
}
else if (type == "blockboot") {
pairs <- levels(md[[attr(md, "subclass")]])
pairs.num <- seq_along(pairs)
if (boot.type == "bca" && nboot < length(pairs)) {
stop(paste0("'nboot' cannot be less than the number of matched pairs/subclass (", length(pairs),
") when type = \"blockboot\" and boot.type = \"bca\". Increase 'nboot' or change 'boot.type'."), call. = FALSE)
}
estfun <- function(data, i) {
#Compute number of times each pair is present
numreps <- setNames(tabulateC(data[i], length(pairs)), pairs)
#For each pair p, copy corresponding md row indices numreps[p] times
ids <- unlist(lapply(pairs[numreps > 0], function(p) rep(which(md[[attr(md, "subclass")]] == p), numreps[p])))
#Subset md with block bootstrapped ids
md_boot <- md[ids,]
fit <- fit.fun(formula, md_boot, md_boot[[attr(md_boot, "weights")]])
if (out.type == "survival") {
return(coef(fit)[treat.name])
}
else {
if (estimand == "ATT") {
md_boot <- md_boot[md_boot[[treat.name]] == 1,]
} else if (estimand == "ATC") {
md_boot <- md_boot[md_boot[[treat.name]] == 0,]
}
md_boot[[treat.name]] <- 1
M1 <- weighted.mean(predict(fit, newdata = md_boot, type = "response"), md_boot[[attr(md_boot, "weights")]])
md_boot[[treat.name]] <- 0
M0 <- weighted.mean(predict(fit, newdata = md_boot, type = "response"), md_boot[[attr(md_boot, "weights")]])
return(c(fun1(M1), fun1(M0), fun1(M1) - fun1(M0)))
}
}
boot.out <- do.call(boot::boot, c(list(pairs.num, estfun, R = nboot), A[names(A) %in% names(formals(boot::boot))]))
}
est <- boot.out$t0
vc <- cov(boot.out$t)
se <- sqrt(diag(vc))
ci <- t(vapply(seq_along(est), function(x) {
bc <- boot::boot.ci(boot.out, conf = conf, type = boot.type, index = x)
drop(bc[[4]][,-seq_len(length(bc[[4]]) - 2)])
}, numeric(2)))
l.effects <- cbind(est, ci, se, tval <- est/se, 2 * pnorm(abs(tval), lower.tail = FALSE))
df <- Inf
}
dimnames(l.effects) <- list(switch(measure,
MD = c("E[Y1]", "E[Y0]", "E[Y1] - E[Y0]"),
RD = c("P(Y1=1)", "P(Y0=1)", "RD"),
RR = c("log(P(Y1=1))", "log(P(Y0=1))", "log(RR)"),
OR = c("logit(P(Y1=1))", "logit(P(Y0=1))", "log(OR)"),
HR = c("log(HR)")),
c("Estimate", "CI lower", "CI upper", "Std. Error",
if (is.finite(df)) "t value" else "z value",
if (is.finite(df)) "Pr(>|t|)" else "Pr(>|z|)"))
effects <- switch(measure,
MD = l.effects[,1:3],
RD = l.effects[,1:3],
RR = exp(l.effects[,1:3]),
OR = rbind(exp(l.effects[1:2,1:3])/(1 + exp(l.effects[1:2,1:3])), exp(l.effects[3,1:3])),
HR = exp(l.effects[,1:3, drop = FALSE]))
if (!is.null(effects)) {
dimnames(effects) <- list(switch(measure,
MD = c("E[Y1]", "E[Y0]", "E[Y1] - E[Y0]"),
HR = "HR",
c("P(Y1=1)", "P(Y0=1)", measure)),
c("Estimate",
"CI lower", "CI upper"))
}
dimnames(vc) <- list(rownames(l.effects), rownames(l.effects))
out <- list(effects = effects, l.effects = l.effects, vcov = vc)
attr(out, "info") <- list(out.type = out.type, covs.present = covs.present,
replace = replace, type = type,
boot.type = boot.type, robust.type = robust.type,
estimand = estimand, df = df)
class(out) <- "eff_matchit"
out
}
print.eff_matchit <- function(x, digits = max(3, getOption("digits") - 3), ...) {
cat(paste("\nMarginal Effect Estimates:\n\n", sep = ""))
printCoefmat(x$effects, digits = digits, has.Pvalue = FALSE, right = TRUE, cs.ind = 1:3,
tst.ind = NULL, ...)
cat(paste("\nMarginal Effect Estimates (Link Scale):\n\n", sep = ""))
printCoefmat(x$l.effects, digits = digits, cs.ind = 1:4, tst.ind = which(endsWith(colnames(x$effects), " value")),
has.Pvalue = TRUE, right = TRUE, signif.stars = FALSE, ...)
cat("\n")
#Print info
# info <- attr(x, "info")
# if (info) 0
# "SEs and CIs estimated on the link scale using HC1 cluster-robust variance"
# "SEs estimated on link scale using block bootstrap; percentile CIs used"
invisible(x)
}
coef.eff_matchit <- function(object, link = TRUE, ...) {
if (link) setNames(object$l.effects[,"Estimate"], rownames(object$l.effects))
else setNames(object$effects[,"Estimate"], rownames(object$effects))
}
vcov.eff_matchit <- function(object, ...) {
object$vcov
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.