Nothing
#' Optimized regression discontinuity design
#'
#' Optimized estimation and inference of treamtment effects identified
#' via regression discontinuities
#'
#' @param X The running variables.
#' @param Y The outcomes. If null, only optimal weights are computed.
#' @param W Treatment assignments, typically of the form 1(X >= c).
#' @param max.second.derivative A bound on the second derivative of mu_w(x) = E[Y(w) | X = x].
#' @param estimation.point Point "c" at which CATE is to be estimated. If estimation.point = NULL,
#' we estimate a weighted CATE, with weights chosen to minimize MSE,
#' as in Section 4.1 of Imbens and Wager (2017).
#' @param sigma.sq The irreducible noise level. If null, estimated from the data.
#' @param alpha Coverage probability of confidence intervals.
#' @param lambda.mult Optional multplier that can be used to over- or under-penalize variance.
#' @param bin.width Bin width for discrete approximation.
#' @param num.bucket Number of bins for discrete approximation. Can only be used if bin.width = NULL.
#' @param use.homoskedatic.variance Whether confidence intervals should be built assuming homoskedasticity.
#' @param use.spline Whether non-parametric components should be modeled as quadratic splines
#' in order to reduce the number of optimization parameters, and potentially
#' improving computational performance.
#' @param spline.df Number of degrees of freedom (per running variable) used for spline computation.
#' @param try.elnet.for.sigma.sq Whether an elastic net on a spline basis should be used for estimating sigma^2.
#' @param optimizer Which optimizer to use? Mosek is a commercial solver, but free
#' academic licenses are available. Needs to be installed separately.
#' ECOS is an open-source interior-point solver for conic problems,
#' made available via the CVXR wrapper.
#' Quadprog is the default R solver; it may be slow on large problems, but
#' is very accurate on small problems.
#' SCS is an open-source "operator splitting" solver that implements a first order
#' method for solving very large cone programs to modest accuracy. The speed of SCS may
#' be helpful for prototyping; however, the results may be noticeably less accurate.
#' SCS is also accessed via the CVXR wrapper.
#' The option "auto" uses a heuristic to choose.
#' @param verbose whether the optimizer should print progress information
#'
#' @return A trained optrdd object.
#'
#' @references Domahidi, A., Chu, E., & Boyd, S. (2013, July).
#' ECOS: An SOCP solver for embedded systems.
#' In Control Conference (ECC), 2013 European (pp. 3071-3076). IEEE.
#'
#' @references Imbens, G., & Wager, S. (2017).
#' Optimized Regression Discontinuity Designs.
#' arXiv preprint arXiv:1705.01677.
#'
#' @references O’Donoghue, B., Chu, E., Parikh, N., & Boyd, S. (2016).
#' Conic optimization via operator splitting and homogeneous self-dual embedding.
#' Journal of Optimization Theory and Applications, 169(3), 1042-1068.
#'
#' @examples
#' # Simple regression discontinuity with discrete X
#' n = 4000; threshold = 0
#' X = sample(seq(-4, 4, by = 8/41.5), n, replace = TRUE)
#' W = as.numeric(X >= threshold)
#' Y = 0.4 * W + 1 / (1 + exp(2 * X)) + 0.2 * rnorm(n)
#' # using 0.4 for max.second.derivative would have been enough
#' out.1 = optrdd(X=X, Y=Y, W=W, max.second.derivative = 0.5, estimation.point = threshold)
#' print(out.1); plot(out.1, xlim = c(-1.5, 1.5))
#'
#' # Now, treatment is instead allocated in a neighborhood of 0
#' thresh.low = -1; thresh.high = 1
#' W = as.numeric(thresh.low <= X & X <= thresh.high)
#' Y = 0.2 * (1 + X) * W + 1 / (1 + exp(2 * X)) + rnorm(n)
#' # This estimates CATE at specifically chosen points
#' out.2 = optrdd(X=X, Y=Y, W=W, max.second.derivative = 0.5, estimation.point = thresh.low)
#' print(out.2); plot(out.2, xlim = c(-2.5, 2.5))
#' out.3 = optrdd(X=X, Y=Y, W=W, max.second.derivative = 0.5, estimation.point = thresh.high)
#' print(out.3); plot(out.3, xlim = c(-2.5, 2.5))
#' # This estimates a weighted CATE, with lower variance
#' out.4 = optrdd(X=X, Y=Y, W=W, max.second.derivative = 0.5)
#' print(out.4); plot(out.4, xlim = c(-2.5, 2.5))
#'
#' \dontrun{
#' # RDD with multivariate running variable.
#' X = matrix(runif(n*2, -1, 1), n, 2)
#' W = as.numeric(X[,1] < 0 | X[,2] < 0)
#' Y = X[,1]^2/3 + W * (1 + X[,2]) + rnorm(n)
#' out.5 = optrdd(X=X, Y=Y, W=W, max.second.derivative = 1)
#' print(out.5); plot(out.5)
#' out.6 = optrdd(X=X, Y=Y, W=W, max.second.derivative = 1, estimation.point = c(0, 0.5))
#' print(out.6); plot(out.6)}
#'
#' @export
optrdd = function(X,
Y = NULL,
W,
max.second.derivative,
estimation.point = NULL,
sigma.sq = NULL,
alpha = 0.95,
lambda.mult = 1,
bin.width = NULL,
num.bucket = NULL,
use.homoskedatic.variance = FALSE,
use.spline = TRUE,
spline.df = NULL,
try.elnet.for.sigma.sq = FALSE,
optimizer = c("auto", "mosek", "ECOS", "quadprog", "SCS"),
verbose = TRUE) {
n = length(W)
if (class(W) == "logical") W = as.numeric(W)
if (!all(W %in% c(0, 1))) stop("The treatment assignment W must be binary.")
if (!is.null(Y) & (length(Y) != n)) { stop("Y and W must have same length.") }
if (is.null(dim(X))) { X = matrix(X, ncol = 1) }
if (nrow(X) != n) { stop("The number of rows of X and the length of W must match") }
if (length(max.second.derivative) != 1) { stop("max.second.derivative must be of length 1.") }
if (!is.null(bin.width) & !is.null(num.bucket)) { stop("Only one of bin.width or num.bucket may be used.") }
nvar = ncol(X)
if (nvar >= 3) { stop("Not yet implemented for 3 or more running variables.") }
cate.at.pt = !is.null(estimation.point)
if (is.null(estimation.point)) { estimation.point = colMeans(X) }
univariate.monotone = (nvar == 1) &&
((max(X[W==0, 1]) <= min(X[W==1, 1])) || (max(X[W==1, 1]) <= min(X[W==0, 1])))
mosek_available = requireNamespace("Rmosek", quietly = TRUE)
# Naive initialization for sigma.sq if needed
if (is.null(sigma.sq)) {
if (is.null(Y)) {
warning("Setting noise level to 1 as default...")
sigma.sq = 1
} else {
Y.hat = stats::predict(stats::lm(Y ~ X * W))
sigma.sq = mean((Y - Y.hat)^2) * length(W) / (length(W) - 2 - 2 * nvar)
if (try.elnet.for.sigma.sq){
if(ncol(X) > 1) {
stop("Elastic net for sigma squared not implemented with more than 1 running variable.")
}
linear.params = 1 + 2 * ncol(X)
elnet.df = 7
ridge.mat = cbind(W, X, W * X, matrix(0, length(W), 2 * elnet.df))
ridge.mat[W==0, linear.params + 1:elnet.df] = splines::ns(X[W==0,], df = elnet.df)
ridge.mat[W==1, linear.params + elnet.df + 1:elnet.df] = splines::ns(X[W==1,], df = elnet.df)
elnet = glmnet::cv.glmnet(ridge.mat, Y,
penalty.factor = c(rep(0, linear.params),
rep(1, 2 * elnet.df)),
keep = TRUE, alpha = 0.5)
elnet.hat = elnet$fit.preval[,!is.na(colSums(elnet$fit.preval)),drop=FALSE][, elnet$lambda == elnet$lambda.1se]
sigma.sq.elnet = mean((elnet.hat - Y)^2)
sigma.sq = min(sigma.sq, sigma.sq.elnet)
}
}
}
optimizer = match.arg(optimizer)
if (optimizer == "auto") {
if (nvar == 1 &&
use.spline &&
(univariate.monotone || !cate.at.pt) &&
length(unique(c(X))) <= 100 &&
max.second.derivative / sigma.sq <= 4) {
optimizer = "quadprog"
} else {
if (mosek_available) {
optimizer = "mosek"
} else {
optimizer = "ECOS"
}
}
}
if (optimizer == "mosek") {
if (!mosek_available) {
optimizer = "ECOS"
warning("The mosek optimizer is not installed; using ECOS instead.")
}
# if (!requireNamespace("Rmosek", quietly = TRUE)) {
# optimizer = "quadprog"
# if (nvar >= 2) {
# op = options("warn")
# on.exit(options(op))
# options(warn=1)
# warning(paste("The mosek optimizer is not installed; using quadprog instead.",
# "This may be very slow with more than one running variable."))
# } else {
# warning(paste("The mosek optimizer is not installed; using quadprog instead."))
# }
# }
}
if (optimizer == "SCS") {
warning(paste("SCS is a fast an free optimizer, but doesn't solve the problem exactly.",
"Resulting confidence intervals may be needlessly long.",
"It is recommended to also try MOSEK."))
}
# Create discrete grid on which to optimize, and assign each training example
# to a cell.
if (nvar == 1) {
if (is.null(bin.width)) {
if (is.null(num.bucket)) { num.bucket = 2000 }
bin.width = (max(X[,1]) - min(X[,1])) / num.bucket
}
breaks = seq(min(X[,1]) - bin.width/2, max(X[,1]) + bin.width, by = bin.width)
xx.grid = breaks[-1] - bin.width/2
num.bucket = length(xx.grid)
xx.grid = matrix(xx.grid, ncol = 1)
xx.centered = matrix(xx.grid - estimation.point, ncol = 1)
zeroed.idx = max(which(xx.centered < 0)) + c(0, 1)
idx.to.bucket = as.numeric(cut(X[,1], breaks = breaks))
} else if (nvar == 2) {
if (is.null(bin.width)) {
if (is.null(num.bucket)) {
if (optimizer == "quadprog") {
num.bucket = 900
warning(paste("Using coarse discrete approximation of size 30x30",
"to make quadprog run faster (i.e., num.bucket = 900.)"))
} else {
num.bucket = 10000
}
}
bin.width = sqrt((max(X[,1]) - min(X[,1])) * (max(X[,2]) - min(X[,2])) / num.bucket)
}
breaks1 = seq(min(X[,1]) - bin.width/2, max(X[,1]) + bin.width, by = bin.width)
breaks2 = seq(min(X[,2]) - bin.width/2, max(X[,2]) + bin.width, by = bin.width)
xx1 = breaks1[-1] - bin.width/2
xx2 = breaks2[-1] - bin.width/2
xx.grid = expand.grid(xx1, xx2)
xx.centered = t(t(xx.grid) - estimation.point)
num.bucket = nrow(xx.grid)
z1 = max(which(xx1 < estimation.point[1])) + c(0, 1)
z2 = max(which(xx2 < estimation.point[2])) + c(0, 1)
zeroed.idx = as.matrix(expand.grid(z1 - 1, z2 - 1)) %*% c(1, length(xx1))
idx.1 = as.numeric(cut(X[,1], breaks = breaks1))
idx.2 = as.numeric(cut(X[,2], breaks = breaks2))
idx.to.bucket = sapply(1:nrow(X), function(iter) idx.1[iter] + (idx.2[iter] - 1) * length(xx1))
} else {
stop("Not yet implemented for 3 or more running variables.")
}
# Define matrix of constraints.
# D2 is a raw curvature matrix, not accounting for bin.width.
if (nvar == 1) {
D2 = Matrix::bandSparse(n=num.bucket-2, m=num.bucket, k = c(0, 1, 2),
diag = list(rep(1, num.bucket), rep(-2, num.bucket))[c(1, 2, 1)])
} else if (nvar == 2) {
all.idx = expand.grid(1:length(xx1), 1:length(xx2))
# remove corners
all.idx = all.idx[!(all.idx[,1] %in% c(1, length(xx1)) & all.idx[,2] %in% c(1, length(xx2))),]
D2.entries = do.call(rbind, sapply(1:nrow(all.idx), function(i12) {
i1 = all.idx[i12,1]
i2 = all.idx[i12,2]
edge.1 = i1 %in% c(1, length(xx1))
edge.2 = i2 %in% c(1, length(xx2))
rbind(
if (!edge.1) {
cbind(j=(i1 - 1):(i1 + 1) + (i2 - 1) * length(xx1),
x=c(1, -2, 1))
} else { numeric() },
if (!edge.2) {
cbind(j=i1 + ((i2 - 2):i2) * length(xx1),
x=c(1, -2, 1))
} else { numeric() },
if (!(edge.1 | edge.2)) {
cbind(j = c((i1 - 1):(i1 + 1) + ((i2 - 2):i2) * length(xx1),
(i1 - 1):(i1 + 1) + (i2:(i2 - 2)) * length(xx1)),
x = c(c(1/2, -1, 1/2), c(1/2, -1, 1/2)))
} else { numeric() })
}))
D2.i = c(t(matrix(rep(1:(nrow(D2.entries)/3), 3), ncol = 3)))
D2 = Matrix::sparseMatrix(i=D2.i, j=D2.entries[,1], x=D2.entries[,2])
} else {
stop("Not yet implemented for 3 or more running variables.")
}
# Construct a (weighted) histogram representing the X and Y values.
fact = factor(idx.to.bucket, levels = as.character(1:num.bucket))
bucket.map = Matrix::sparse.model.matrix(~fact + 0, transpose = TRUE)
X.counts = as.numeric(bucket.map %*% rep(1, n))
W.counts = as.numeric(bucket.map %*% W)
realized.idx.0 = which(X.counts > W.counts)
realized.idx.1 = which(W.counts > 0)
num.realized.0 = length(realized.idx.0)
num.realized.1 = length(realized.idx.1)
# These matrices map non-parametric dual parameters f_w(x) to buckets
# where gamma needs to be evaluated
selector.0 = Matrix::Diagonal(num.bucket, 1)[realized.idx.0,]
selector.1 = Matrix::Diagonal(num.bucket, 1)[realized.idx.1,]
# We force centering.matrix %*% f_w(x) = 0, to ensure identification of f_w(x)
centering.matrix = Matrix::sparseMatrix(dims = c(length(zeroed.idx), num.bucket),
i = 1:length(zeroed.idx),
j = zeroed.idx,
x = rep(1, length(zeroed.idx)))
# Computational performance can be improved by representing non-parametric
# components in a quadratic spline basis.
if (use.spline) {
if (is.null(spline.df)) {
if (optimizer == "mosek" || optimizer == "SCS" || optimizer == "ECOS") {
spline.df = c(100, 45 - 15 * cate.at.pt)[nvar]
} else {
spline.df = c(40, 10)[nvar]
}
}
if ((nvar == 1 && spline.df > nrow(xx.centered) / 2) |
(nvar == 2 && spline.df > min(length(xx1), length(xx2)) * 0.7)) {
use.spline = FALSE
}
}
if (use.spline) {
if (nvar == 1) {
basis.mat.raw = splines::bs(xx.grid, degree = 2, df=spline.df, intercept = TRUE)
class(basis.mat.raw) = "matrix"
basis.mat = Matrix::Matrix(basis.mat.raw)
} else if (nvar == 2) {
basis.mat.1 = splines::bs(xx.grid[,1], degree = 2, df=spline.df, intercept = TRUE)
basis.mat.2 = splines::bs(xx.grid[,2], degree = 2, df=spline.df, intercept = TRUE)
basis.mat = Matrix::sparse.model.matrix(~ basis.mat.1:basis.mat.2 + 0)
} else {
stop("Not yet implemented for 3 or more running variables.")
}
D2 = D2 %*% basis.mat
selector.0 = selector.0 %*% basis.mat
selector.1 = selector.1 %*% basis.mat
centering.matrix = centering.matrix %*% basis.mat
num.df = ncol(basis.mat)
} else {
num.df = num.bucket
}
# If we are in one dimension and have a threshold RDD, it is
# enough to estimate a single nuisance function f0.
change.slope = cate.at.pt
two.fun = cate.at.pt & !univariate.monotone
# We now prepare inputs to a numerical optimizer. We seek to
# solve a discretized version of equation (18) from Imbens and Wager (2017).
# The parameters to the problem are ordered as (G(0), G(1), lambda, f0, f1).
num.lambda = 3 + nvar * (1 + change.slope)
num.params = num.realized.0 + num.realized.1 + num.lambda + (1 + two.fun) * num.df
# The quadratic component of the objective is 1/2 sum_j Dmat.diagonal_j * params_j^2
Dmat.diagonal = c((X.counts[realized.idx.0] - W.counts[realized.idx.0]) / 2 / sigma.sq,
(W.counts[realized.idx.1]) / 2 / sigma.sq,
lambda.mult / max.second.derivative^2 / 2,
rep(0, num.lambda - 1 + (1 + two.fun) * num.df))
# The linear component of the objective is sum(dvec * params)
dvec = c(rep(0, num.realized.0 + num.realized.1 + 1), 1, -1,
rep(0, num.lambda - 3 + (1 + two.fun) * num.df))
# We now impose constrains on Amat %*% params.
# The first meq constrains are equality constraints (Amat %*% params = 0);
# the remaining ones are inequalities (Amat %*% params >= 0).
Amat = rbind(
# Defines G(0) in terms of the other problem parameters (equality constraint)
cbind(Matrix::Diagonal(num.realized.0, -1),
Matrix::Matrix(0, num.realized.0, num.realized.1),
0, 0, 1, xx.centered[realized.idx.0,],
if(change.slope) { -xx.centered[realized.idx.0,] } else { numeric() },
selector.0,
if(two.fun) { Matrix::Matrix(0, num.realized.0, num.df) } else { numeric() }),
# Defines G(1) in terms of the other problem parameters (equality constraint)
cbind(Matrix::Matrix(0, num.realized.1, num.realized.0),
Matrix::Diagonal(num.realized.1, -1),
0, 1, 0, xx.centered[realized.idx.1,],
if(change.slope) { xx.centered[realized.idx.1,] } else { numeric() },
if(two.fun) {
cbind(Matrix::Matrix(0, num.realized.1, num.df), selector.1)
} else {
selector.1
}),
# Ensure that f_w(c), f'_w(c) = 0
cbind(Matrix::Matrix(0, length(zeroed.idx), num.realized.0 + num.realized.1 + num.lambda),
centering.matrix,
Matrix::Matrix(0, length(zeroed.idx), as.numeric(two.fun) * num.df)),
if(two.fun) {
cbind(Matrix::Matrix(0, length(zeroed.idx), num.realized.0 + num.realized.1 + num.lambda + num.df),
centering.matrix)
} else { numeric() },
# Bound the second derivative of f_0 by lambda_1
cbind(Matrix::Matrix(0, 2 * nrow(D2), num.realized.0 + num.realized.1),
bin.width^2,
matrix(0, 2 * nrow(D2), num.lambda - 1),
rbind(D2, -D2),
if (two.fun) { Matrix::Matrix(0, 2 * nrow(D2), num.df) } else { numeric() }),
# Bound the second derivative of f_1 by lambda_1
if (two.fun) {
cbind(Matrix::Matrix(0, 2 * nrow(D2), num.realized.0 + num.realized.1),
bin.width^2,
matrix(0, 2 * nrow(D2), num.lambda - 1 + num.df),
rbind(D2, -D2))
})
meq = num.realized.0 + num.realized.1 + length(zeroed.idx) * (1 + two.fun)
bvec = rep(0, nrow(Amat))
gamma.0 = rep(0, num.bucket)
gamma.1 = rep(0, num.bucket)
if (optimizer == "quadprog") {
if (verbose) {
print(paste0("Running quadrprog with problem of size: ",
dim(Amat)[1], " x ", dim(Amat)[2], "..."))
}
# For quadprog, we need Dmat to be positive definite, which is why we add a small number to the diagonal.
# The conic implementation via mosek does not have this issue.
soln = quadprog::solve.QP(Matrix::Diagonal(num.params, Dmat.diagonal + 0.000000001),
-dvec,
Matrix::t(Amat),
bvec,
meq=meq)
gamma.0[realized.idx.0] = - soln$solution[1:num.realized.0] / sigma.sq / 2
gamma.1[realized.idx.1] = - soln$solution[num.realized.0 + 1:num.realized.1] / sigma.sq / 2
t.hat = soln$solution[num.realized.0 + num.realized.1 + 1] / (2 * max.second.derivative^2)
} else if (optimizer == "SCS" || optimizer == "ECOS") {
if (verbose && optimizer == "SCS") {
print(paste0("Running CVXR/SCS with problem of size: ",
dim(Amat)[1], " x ", dim(Amat)[2], "..."))
}
if (verbose && optimizer == "ECOS") {
print(paste0("Running CVXR/ECOS with problem of size: ",
dim(Amat)[1], " x ", dim(Amat)[2], "..."))
}
xx = CVXR::Variable(ncol(Amat))
objective = sum(Dmat.diagonal/2 * xx^2 + dvec * xx)
contraints = list(
Amat[1:meq,] %*% xx == bvec[1:meq],
Amat[(meq+1):nrow(Amat),] %*% xx >= bvec[(meq+1):nrow(Amat)]
)
cvx.problem = CVXR::Problem(CVXR::Minimize(objective), contraints)
cvx.output = solve(cvx.problem, solver = optimizer, verbose = verbose)
if (cvx.output$status != "optimal") {
warning(paste0("CVXR returned with status: ",
cvx.output$status,
". For better results, try another optimizer (MOSEK is recommended)."))
}
result = cvx.output$getValue(xx)
gamma.0[realized.idx.0] = - result[1:num.realized.0] / sigma.sq / 2
gamma.1[realized.idx.1] = - result[num.realized.0 + 1:num.realized.1] / sigma.sq / 2
t.hat = result[num.realized.0 + num.realized.1 + 1] / (2 * max.second.derivative^2)
} else if (optimizer == "mosek") {
# We need to rescale our optimization parameters, such that Dmat has only
# ones and zeros on the diagonal; i.e.,
A.natural = Amat %*% Matrix::Diagonal(ncol(Amat), x=1/sqrt(Dmat.diagonal + as.numeric(Dmat.diagonal == 0)))
mosek.problem <- list()
# The A matrix relates parameters to constraints, via
# blc <= A * { params } <= buc, and blx <= params <= bux
# The conic fomulation adds two additional parameters to the problem, namely
# a parameter "S" and "ONE", that are characterized by a second-order cone constraint
# S * ONE >= 1/2 {params}' Dmat {params},
# and the equality constraint ONE = 1
mosek.problem$A <- cbind(A.natural, Matrix::Matrix(0, nrow(A.natural), 2))
mosek.problem$bc <- rbind(blc = rep(0, nrow(A.natural)), buc = c(rep(0, meq), rep(Inf, nrow(A.natural) - meq)))
mosek.problem$bx <- rbind(blx = c(rep(-Inf, ncol(A.natural)), 0, 1), bux = c(rep(Inf, ncol(A.natural)), Inf, 1))
# This is the cone constraint
mosek.problem$cones <- cbind(list("RQUAD", c(ncol(Amat) + 1, ncol(Amat) + 2, which(Dmat.diagonal != 0))))
# We seek to minimize c * {params}
mosek.problem$sense <- "min"
mosek.problem$c <- c(dvec, 1, 0)
#mosek.problem$dparam= list(BASIS_TOL_S=1.0e-9, BASIS_TOL_X=1.0e-9)
if (verbose) {
mosek.out = Rmosek::mosek(mosek.problem)
} else {
mosek.out = Rmosek::mosek(mosek.problem, opts=list(verbose=0))
}
if (mosek.out$response$code != 0) {
warning(paste("MOSEK returned with status",
mosek.out$response$msg,
"For better results, try another optimizer."))
}
# We now also need to re-adjust for "natural" scaling
gamma.0[realized.idx.0] = - mosek.out$sol$itr$xx[1:num.realized.0] / sigma.sq / 2 /
sqrt(Dmat.diagonal[1:num.realized.0])
gamma.1[realized.idx.1] = - mosek.out$sol$itr$xx[num.realized.0 + 1:num.realized.1] / sigma.sq / 2 /
sqrt(Dmat.diagonal[num.realized.0 + 1:num.realized.1])
t.hat = mosek.out$sol$itr$xx[num.realized.0 + num.realized.1 + 1] / (2 * max.second.derivative^2) /
sqrt(Dmat.diagonal[num.realized.0 + num.realized.1 + 1])
} else {
stop("Optimizer choice not valid.")
}
# Now map the x-wise functions into a weight for each observation
gamma = rep(0, length(W))
gamma[W==0] = as.numeric(Matrix::t(bucket.map[,which(W==0)]) %*% gamma.0)
gamma[W==1] = as.numeric(Matrix::t(bucket.map[,which(W==1)]) %*% gamma.1)
# Patch up numerical inaccuracies
gamma[W==0] = -gamma[W==0] / sum(gamma[W==0])
gamma[W==1] = gamma[W==1] / sum(gamma[W==1])
# Compute the worst-case imbalance...
max.bias = max.second.derivative * t.hat
# If outcomes are provided, also compute confidence intervals for tau.
if (!is.null(Y)) {
# The point estimate
tau.hat = sum(gamma * Y)
if (use.homoskedatic.variance) {
se.hat.tau = sqrt(sum(gamma^2 * sigma.sq))
} else {
# A heteroskedaticity-robust variance estimate.
# Weight the regression towards areas used in estimation, so we are not
# too vulnerable to curvature effects far from the boundary.
regr.df = data.frame(X=X, W=W, Y=Y, gamma.sq = gamma^2)
regr.df = regr.df[regr.df$gamma.sq != 0,]
Y.fit = stats::lm(Y ~ X * W, data = regr.df, weights = regr.df$gamma.sq)
self.influence = stats::lm.influence(Y.fit)$hat
Y.resid.sq = (regr.df$Y - stats::predict(Y.fit))^2
se.hat.tau = sqrt(sum(Y.resid.sq * regr.df$gamma.sq / (1 - self.influence)))
if (!try.elnet.for.sigma.sq & se.hat.tau^2 < 0.8 * sum(regr.df$gamma.sq) * sigma.sq) {
warning(paste("Implicit estimate of sigma^2 may be too pessimistic,",
"resulting in valid but needlessly long confidence intervals.",
"Try the option `try.elnet.for.sigma.sq = TRUE` for potentially improved performance."))
}
}
# Confidence intervals that account for both bias and variance
tau.plusminus = get.plusminus(max.bias, se.hat.tau, alpha)
} else {
tau.hat = NULL
se.hat.tau = sqrt(sigma.sq * sum(gamma^2))
tau.plusminus = get.plusminus(max.bias, se.hat.tau, alpha)
}
ret = list(tau.hat=tau.hat,
tau.plusminus=tau.plusminus,
alpha=alpha,
max.bias = max.bias,
sampling.se=se.hat.tau,
gamma=gamma,
gamma.fun.0 = data.frame(xx=xx.grid[realized.idx.0,],
gamma=gamma.0[realized.idx.0]),
gamma.fun.1 = data.frame(xx=xx.grid[realized.idx.1,],
gamma=gamma.1[realized.idx.1]))
class(ret) = "optrdd"
return(ret)
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.