Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Imports:
Formula,
rlang (>= 0.2.0)
LinkingTo: Rcpp, RcppEigen
RoxygenNote: 6.0.1
RoxygenNote: 6.1.0
LazyData: true
Suggests:
RcppEigen,
Expand Down
2 changes: 1 addition & 1 deletion R/estimatr_iv_robust.R
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
#' @param se_type The sort of standard error sought. If `clusters` is
#' not specified the options are "HC0", "HC1" (or "stata", the equivalent),
#' "HC2" (default), "HC3", or
#' "classical". If `clusters` is specified the options are "CR0", "CR2" (default), or "stata". Can also specify "none", which may speed up estimation of the coefficients.
#' "classical". If `clusters` is specified the options are "CR0", "stata", "CR2" (default), or "CR3". Can also specify "none", which may speed up estimation of the coefficients.
#' @param ci logical. Whether to compute and return p-values and confidence
#' intervals, TRUE by default.
#' @param alpha The significance level, 0.05 by default.
Expand Down
3 changes: 2 additions & 1 deletion R/estimatr_lm_robust.R
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
#' @param se_type The sort of standard error sought. If `clusters` is
#' not specified the options are "HC0", "HC1" (or "stata", the equivalent),
#' "HC2" (default), "HC3", or
#' "classical". If `clusters` is specified the options are "CR0", "CR2" (default), or "stata". Can also specify "none", which may speed up estimation of the coefficients.
#' "classical". If `clusters` is specified the options are "CR0", "stata",
#' "CR2" (default), or "CR3". Can also specify "none", which may speed up estimation of the coefficients.
#' @param ci logical. Whether to compute and return p-values and confidence
#' intervals, TRUE by default.
#' @param alpha The significance level, 0.05 by default.
Expand Down
16 changes: 7 additions & 9 deletions R/helper_lm_robust_fit.R
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ lm_robust_fit <- function(y,

se_type <- check_se_type(se_type, clustered)

if (weighted && se_type == "CR2" && fes) {
if (weighted && se_type %in% c("CR2", "CR3") && fes) {
stop(
"Cannot use `fixed_effects` with weighted CR2 estimation at the moment. ",
"Cannot use `fixed_effects` with weighted CR2 or CR3 estimation at the moment. ",
"Try setting `se_type` = \"stata\""
)
}
Expand Down Expand Up @@ -177,7 +177,7 @@ lm_robust_fit <- function(y,

# For CR2 need X weighted by weights again
# so that instead of having X * sqrt(W) we have X * W
if (se_type == "CR2") {
if (se_type %in% c("CR2", "CR3")) {
data[["X"]] <- data[["weights"]] * data[["X"]]
if (fes) {
data[["femat"]] <- data[["weights"]] * data[["femat"]]
Expand All @@ -186,8 +186,6 @@ lm_robust_fit <- function(y,

}



# Also need second stage residuals for fstat
if (iv_stage[[1]] == 2) {
fit_vals[["fitted.values.iv"]] <- as.matrix(data[["X"]] %*% fit$beta_hat)
Expand All @@ -200,14 +198,14 @@ lm_robust_fit <- function(y,
if (se_type != "none") {

vcov_fit <- lm_variance(
X = if (se_type %in% c("HC2", "HC3", "CR2") && fes)
X = if (se_type %in% c("HC2", "HC3", "CR2", "CR3") && fes)
cbind(data[["X"]], data[["femat"]])
else data[["X"]],
Xunweighted = if (se_type %in% c("HC2", "HC3", "CR2") && fes && weighted)
Xunweighted = if (se_type %in% c("HC2", "HC3", "CR2", "CR3") && fes && weighted)
cbind(data[["Xunweighted"]], data[["fematunweighted"]])
else data[["Xunweighted"]],
XtX_inv = fit$XtX_inv,
ei = if (se_type == "CR2" && weighted)
ei = if (se_type %in% c("CR2", "CR3") && weighted)
fit_vals[["ei.unweighted"]]
else fit_vals[["ei"]],
weight_mean = data[["weight_mean"]],
Expand Down Expand Up @@ -388,7 +386,7 @@ lm_robust_fit <- function(y,
check_se_type <- function(se_type, clustered) {

# Allowable se_types with clustering
cl_se_types <- c("CR0", "CR2", "stata")
cl_se_types <- c("CR0", "CR2", "CR3", "CR1s", "stata")
rob_se_types <- c("HC0", "HC1", "HC2", "HC3", "classical", "stata")

# Parse cluster variable
Expand Down
5 changes: 2 additions & 3 deletions R/helper_starprep.R
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ commarobust <- function(model,
XtX_inv <- data[["weight_mean"]] * XtX_inv

# Need unweighted resid and need to reweight X
if (se_type == "CR2") {
if (se_type %in% c("CR2", "CR3")) {
eiunweighted <-
as.matrix(data[["yunweighted"]] - data[["Xunweighted"]] %*% coefs)
data[["X"]] <- data[["weights"]] * data[["X"]]
Expand All @@ -104,7 +104,7 @@ commarobust <- function(model,
X = data[["X"]],
Xunweighted = data[["Xunweighted"]],
XtX_inv = XtX_inv,
ei = if (se_type == "CR2" && weighted) eiunweighted else ei,
ei = if (se_type %in% c("CR2", "CR3") && weighted) eiunweighted else ei,
weight_mean = data[["weight_mean"]],
cluster = data[["cluster"]],
J = data[["J"]],
Expand All @@ -114,7 +114,6 @@ commarobust <- function(model,
fe_rank = 0
)


## Build return_list
return_list <- list(
coefficients = as.matrix(coef(model)),
Expand Down
4 changes: 2 additions & 2 deletions man/difference_in_means.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions man/extract.lm_robust.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions man/horvitz_thompson.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion man/iv_robust.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions man/lm_lin.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion man/lm_robust.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions man/lm_robust_fit.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 35 additions & 16 deletions src/lm_robust_helper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ List lm_variance(Eigen::Map<Eigen::MatrixXd>& X,
const int n(X.rows()), r(XtX_inv.cols()), ny(ei.cols());
// Rcout << "X:" << std::endl << X << std::endl;
int r_fe = r + fe_rank;
const bool clustered = ((se_type == "stata") || (se_type == "CR0") || (se_type == "CR2"));
const bool clustered = ((se_type == "stata") || (se_type == "CR0") || (se_type == "CR2") || (se_type == "CR3"));
const int npars = r * ny;
int sandwich_size = n;
if (clustered) {
Expand Down Expand Up @@ -291,7 +291,7 @@ List lm_variance(Eigen::Map<Eigen::MatrixXd>& X,
}

Eigen::MatrixXd meatXtX_inv;
if ((se_type == "HC2") || (se_type == "HC3") || (se_type == "CR2")) {
if ((se_type == "HC2") || (se_type == "HC3") || (se_type == "CR2") || (se_type == "CR3")) {
if (X.cols() > r) {
meatXtX_inv = getMeatXtX(X, XtX_inv);
r_fe = meatXtX_inv.cols();
Expand Down Expand Up @@ -324,7 +324,6 @@ List lm_variance(Eigen::Map<Eigen::MatrixXd>& X,
}
// Rcout << "temp_omega:" << std::endl << temp_omega << std::endl;


for (int m = 0; m < ny; m++) {
if (ny > 1) {
// Preserve signs for off-diagonal vcov blocks in mlm
Expand All @@ -337,25 +336,28 @@ List lm_variance(Eigen::Map<Eigen::MatrixXd>& X,
} else {
// clustered

if (se_type == "CR2") {
if ((se_type == "CR2") || (se_type == "CR3")) {
Xoriginal.resize(n, r);
if (Xunweighted.isNotNull()) {
Xoriginal = Rcpp::as<Eigen::Map<Eigen::MatrixXd> >(Xunweighted);
} else {
Xoriginal = X;
}

H1s.resize(r_fe, r_fe*J);
H2s.resize(r_fe, r_fe*J);
H3s.resize(r_fe, r_fe*J);
P_diags.resize(r_fe, J);
if (se_type == "CR2") {
H1s.resize(r_fe, r_fe*J);
H2s.resize(r_fe, r_fe*J);
H3s.resize(r_fe, r_fe*J);
P_diags.resize(r_fe, J);

M_U_ct = meatXtX_inv.llt().matrixL();
MUWTWUM = meatXtX_inv * X.leftCols(r_fe).transpose() * X.leftCols(r_fe) * meatXtX_inv;
Omega_ct = MUWTWUM.llt().matrixL();
M_U_ct = meatXtX_inv.llt().matrixL();
MUWTWUM = meatXtX_inv * X.leftCols(r_fe).transpose() * X.leftCols(r_fe) * meatXtX_inv;
Omega_ct = MUWTWUM.llt().matrixL();
}
}

Eigen::Map<Eigen::ArrayXi> clusters = Rcpp::as<Eigen::Map<Eigen::ArrayXi> >(cluster);
// Rcout << "clusters: "<< std::endl << clusters << std::endl;

double current_cluster = clusters(0);
int clust_num = 0;
Expand Down Expand Up @@ -426,16 +428,34 @@ List lm_variance(Eigen::Map<Eigen::MatrixXd>& X,
H2s.block(0, p_pos, r_fe, r_fe) = ME * X.block(start_pos, 0, len, r_fe) * M_U_ct;
H3s.block(0, p_pos, r_fe, r_fe) = MEU * Omega_ct;
}

} else if (se_type == "CR3") {

Eigen::CompleteOrthogonalDecomposition<Eigen::MatrixXd> At(
Eigen::MatrixXd::Identity(len, len) -
Xoriginal.block(start_pos, 0, len, r_fe) *
meatXtX_inv *
X.block(start_pos, 0, len, r_fe).transpose()
);

if (Xunweighted.isNotNull()) {
// We require the transpose in both cases, but in the unweighted
// case `A` is symmetric and is not needed to be transposed first
// Solve against the identity matrix and then transpose in order
// to allow the more common unweighted case to be simpler
At_WX_inv = At.solve(Eigen::MatrixXd::Identity(len, len)).transpose() * X.block(start_pos, 0, len, r_fe);
} else {
At_WX_inv = At.solve(X.block(start_pos, 0, len, r_fe));
}

}

if (ny > 1) {

// Stack residuals for this cluster from each model
// Rcout << "len: " << len << std::endl;
Eigen::MatrixXd ei_block = ei.block(start_pos, 0, len, ny);
Eigen::Map<const Eigen::MatrixXd> ei_long(ei_block.data(), 1, len*ny);

if (se_type == "CR2") {
if ((se_type == "CR2") || (se_type == "CR3")) {
half_meat.block(clust_num, 0, 1, npars) =
ei_long *
Kr(Eigen::MatrixXd::Identity(ny, ny), At_WX_inv.leftCols(r));
Expand All @@ -447,7 +467,7 @@ List lm_variance(Eigen::Map<Eigen::MatrixXd>& X,

} else {

if (se_type == "CR2") {
if ((se_type == "CR2") || (se_type == "CR3")) {
half_meat.row(clust_num) =
ei.block(start_pos, 0, len, 1).transpose() *
At_WX_inv.leftCols(r);
Expand All @@ -456,7 +476,6 @@ List lm_variance(Eigen::Map<Eigen::MatrixXd>& X,
ei.block(start_pos, 0, len, 1).transpose() *
X.block(start_pos, 0, len, r);
}

}
if (i < n) {
current_cluster = clusters(i);
Expand Down
13 changes: 13 additions & 0 deletions tests/testthat/helper-return-cleaners.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# This fn removes calls from function returns to make testing easier
rmcall <- function(obj) {
if (!is.null(obj[["call"]])) {
obj[["call"]] <- NULL
}
return(obj)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rmcall <- function(obj) { structure(obj, call=NULL) }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't work as call is an object in a list not an attribute.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah - right, different from DeclareDesign. You can still remove the null check I think.

}

# This fn casts tibbles as data.frames for equivalency tests
# TODO implement this everywhere
expect_equivalent_tbl <- function(obj1, obj2) {
expect_equivalent(as.data.frame(obj1), as.data.frame(obj2))
}
7 changes: 0 additions & 7 deletions tests/testthat/helper-rm-call.R

This file was deleted.

4 changes: 4 additions & 0 deletions tests/testthat/helper-se-types.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# se_types for various files

se_types <- c("classical", "HC0", "HC1", "HC2", "HC3")
cr_se_types <- c("CR0", "stata", "CR2", "CR3")
Loading