diff --git a/.gitignore b/.gitignore index 90e6405..d66d03b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,6 @@ src/*.dll inst/doc dev/1-s2.0-S0014292116300630-mmc1 dev/armadillo-codes +dev/*.rds README.html pkgdown diff --git a/DESCRIPTION b/DESCRIPTION index f325983..afd1596 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -2,7 +2,7 @@ Package: capybara Type: Package Title: Fast and Memory Efficient Fitting of Linear Models With High-Dimensional Fixed Effects -Version: 0.5.2 +Version: 0.6.0 Authors@R: c( person( given = "Mauricio", diff --git a/NAMESPACE b/NAMESPACE index bba62c5..6dbe056 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -73,8 +73,8 @@ importFrom(stats,rgamma) importFrom(stats,rlogis) importFrom(stats,rnorm) importFrom(stats,rpois) -importFrom(stats,sd) importFrom(stats,terms) +importFrom(stats,var) importFrom(stats,vcov) importFrom(utils,combn) useDynLib(capybara, .registration = TRUE) diff --git a/NEWS.md b/NEWS.md index 8d5b44f..1c31911 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,10 @@ +# capybara 0.6.0 + +* Moves all the heavy computation to C++ using Armadillo and it exports the + results to R. Previously, there were multiple data copies between R and C++ + that added overhead to the computations. +* The previous versions returned MX by default, now it has to be specified. + # capybara 0.5.2 * Uses an O(n log(n)) algorithm to compute the Kendall correlation for the diff --git a/R/apes.R b/R/apes.R index e4c09f4..5e2b44a 100644 --- a/R/apes.R +++ b/R/apes.R @@ -13,27 +13,27 @@ #' #' @param object an object of class \code{"bias_corr"} or \code{"feglm"}; #' currently restricted to \code{\link[stats]{binomial}}. -#' @param n.pop unsigned integer indicating a finite population correction for +#' @param n_pop unsigned integer indicating a finite population correction for #' the estimation of the covariance matrix of the average partial effects #' proposed by Cruz-Gonzalez, Fernández-Val, and Weidner (2017). The correction #' factor is computed as follows: -#' \eqn{(n^{\ast} - n) / (n^{\ast} - 1)}{(n.pop - n) / (n.pop - 1)}, -#' where \eqn{n^{\ast}}{n.pop} and \eqn{n}{n} are the sizes of the entire +#' \eqn{(n^{\ast} - n) / (n^{\ast} - 1)}{(n_pop - n) / (n_pop - 1)}, +#' where \eqn{n^{\ast}}{n_pop} and \eqn{n}{n} are the sizes of the entire #' population and the full sample size. Default is \code{NULL}, which refers to #' a factor of zero and a covariance obtained by the delta method. -#' @param panel.structure a string equal to \code{"classic"} or \code{"network"} +#' @param panel_structure a string equal to \code{"classic"} or \code{"network"} #' which determines the structure of the panel used. \code{"classic"} denotes #' panel structures where for example the same cross-sectional units are #' observed several times (this includes pseudo panels). \code{"network"} #' denotes panel structures where for example bilateral trade flows are #' observed for several time periods. Default is \code{"classic"}. -#' @param sampling.fe a string equal to \code{"independence"} or +#' @param sampling_fe a string equal to \code{"independence"} or #' \code{"unrestricted"} which imposes sampling assumptions about the #' unobserved effects. \code{"independence"} imposes that all unobserved #' effects are independent sequences. \code{"unrestricted"} does not impose any #' sampling assumptions. Note that this option only affects the optional finite #' population correction. Default is \code{"independence"}. -#' @param weak.exo logical indicating if some of the regressors are assumed to +#' @param weak_exo logical indicating if some of the regressors are assumed to #' be weakly exogenous (e.g. predetermined). If object is of class #' \code{"bias_corr"}, the option will be automatically set to \code{TRUE} if #' the chosen bandwidth parameter is larger than zero. Note that this option @@ -83,10 +83,10 @@ #' @export apes <- function( object = NULL, - n.pop = NULL, - panel.structure = c("classic", "network"), - sampling.fe = c("independence", "unrestricted"), - weak.exo = FALSE) { + n_pop = NULL, + panel_structure = c("classic", "network"), + sampling_fe = c("independence", "unrestricted"), + weak_exo = FALSE) { # Check validity of 'object' if (is.null(object)) { stop("'object' has to be specified.", call. = FALSE) @@ -95,22 +95,22 @@ apes <- function( } # Extract prior information if available or check validity of - # 'panel.structure' + # 'panel_structure' bias_corr <- inherits(object, "bias_corr") if (bias_corr) { - panel.structure <- object[["panel.structure"]] + panel_structure <- object[["panel_structure"]] L <- object[["bandwidth"]] if (L > 0L) { - weak.exo <- TRUE + weak_exo <- TRUE } else { - weak.exo <- FALSE + weak_exo <- FALSE } } else { - panel.structure <- match.arg(panel.structure) + panel_structure <- match.arg(panel_structure) } - # Check validity of 'sampling.fe' - sampling.fe <- match.arg(sampling.fe) + # Check validity of 'sampling_fe' + sampling_fe <- match.arg(sampling_fe) # Extract model information beta <- object[["coefficients"]] @@ -119,11 +119,11 @@ apes <- function( eps <- .Machine[["double.eps"]] family <- object[["family"]] formula <- object[["formula"]] - lvls.k <- object[["lvls.k"]] + lvls_k <- object[["lvls_k"]] nt <- object[["nobs"]][["nobs"]] - nt.full <- object[["nobs"]][["nobs.full"]] - k <- length(lvls.k) - k.vars <- names(lvls.k) + nt.full <- object[["nobs"]][["nobs_full"]] + k <- length(lvls_k) + k_vars <- names(lvls_k) p <- length(beta) # Check if binary choice model @@ -134,11 +134,11 @@ apes <- function( } # Check if provided object matches requested panel structure - if (panel.structure == "classic") { + if (panel_structure == "classic") { if (!(k %in% c(1L, 2L))) { stop( paste( - "panel.structure == 'classic' expects a one- or two-way fixed", + "panel_structure == 'classic' expects a one- or two-way fixed", "effects model." ), call. = FALSE @@ -148,7 +148,7 @@ apes <- function( if (!(k %in% c(2L, 3L))) { stop( paste( - "panel.structure == 'network' expects a two- or three-way fixed", + "panel_structure == 'network' expects a two- or three-way fixed", "effects model." ), call. = FALSE @@ -156,11 +156,11 @@ apes <- function( } } - # Check validity of 'n.pop' + # Check validity of 'n_pop' # Note: Default option is no adjustment i.e. only delta method covariance - if (!is.null(n.pop)) { - n.pop <- as.integer(n.pop) - if (n.pop < nt.full) { + if (!is.null(n_pop)) { + n_pop <- as.integer(n_pop) + if (n_pop < nt.full) { warning( paste( "Size of the entire population is lower than the full sample size.", @@ -170,7 +170,7 @@ apes <- function( ) adj <- 0.0 } else { - adj <- (n.pop - nt.full) / (n.pop - 1L) + adj <- (n_pop - nt.full) / (n_pop - 1L) } } else { adj <- 0.0 @@ -187,7 +187,7 @@ apes <- function( binary <- apply(X, 2L, function(x) all(x %in% c(0.0, 1.0))) # Generate auxiliary list of indexes for different sub panels - k.list <- get_index_list_(k.vars, data) + k_list <- get_index_list_(k_vars, data) # Compute derivatives and weights eta <- object[["eta"]] @@ -205,10 +205,10 @@ apes <- function( } # Center regressor matrix (if required) - if (control[["keep.mx"]]) { + if (control[["keep_mx"]]) { MX <- object[["MX"]] } else { - MX <- center_variables_(X, NA_real_, w, k.list, control[["center.tol"]], 100000L, FALSE) + MX <- center_variables_r_(X, w, k_list, control[["center_tol"]], 10000L) } # Compute average partial effects, derivatives, and Jacobian @@ -243,7 +243,7 @@ apes <- function( # Compute projection and residual projection of \Psi Psi <- -Delta1 / w - MPsi <- center_variables_(Psi, NA_real_, w, k.list, control[["center.tol"]], 100000L, FALSE) + MPsi <- center_variables_r_(Psi, w, k_list, control[["center_tol"]], 10000L) PPsi <- Psi - MPsi rm(Delta1, Psi) @@ -264,28 +264,28 @@ apes <- function( } # Compute bias terms for requested bias correction - if (panel.structure == "classic") { + if (panel_structure == "classic") { # Compute \hat{B} and \hat{D} - b <- group_sums_(Delta2 + PPsi * z, w, k.list[[1L]]) / (2.0 * nt) + b <- group_sums_(Delta2 + PPsi * z, w, k_list[[1L]]) / (2.0 * nt) if (k > 1L) { - b <- (b + group_sums_(Delta2 + PPsi * z, w, k.list[[2L]])) / (2.0 * nt) + b <- (b + group_sums_(Delta2 + PPsi * z, w, k_list[[2L]])) / (2.0 * nt) } # Compute spectral density part of \hat{B} if (L > 0L) { - b <- (b - group_sums_spectral_(MPsi * w, v, w, L, k.list[[1L]])) / nt + b <- (b - group_sums_spectral_(MPsi * w, v, w, L, k_list[[1L]])) / nt } } else { # Compute \hat{D}_{1}, \hat{D}_{2}, and \hat{B} - b <- group_sums_(Delta2 + PPsi * z, w, k.list[[1L]]) / (2.0 * nt) - b <- (b + group_sums_(Delta2 + PPsi * z, w, k.list[[2L]])) / (2.0 * nt) + b <- group_sums_(Delta2 + PPsi * z, w, k_list[[1L]]) / (2.0 * nt) + b <- (b + group_sums_(Delta2 + PPsi * z, w, k_list[[2L]])) / (2.0 * nt) if (k > 2L) { - b <- (b + group_sums_(Delta2 + PPsi * z, w, k.list[[3L]])) / (2.0 * nt) + b <- (b + group_sums_(Delta2 + PPsi * z, w, k_list[[3L]])) / (2.0 * nt) } # Compute spectral density part of \hat{B} if (k > 2L && L > 0L) { - b <- (b - group_sums_spectral_(MPsi * w, v, w, L, k.list[[3L]])) / nt + b <- (b - group_sums_spectral_(MPsi * w, v, w, L, k_list[[3L]])) / nt } } rm(Delta2) @@ -296,33 +296,33 @@ apes <- function( rm(eta, w, z, MPsi) # Compute covariance matrix - Gamma <- gamma_(MX, object[["Hessian"]], J, PPsi, v, nt.full) - V <- crossprod_(Gamma, NA_real_, FALSE, FALSE) + Gamma <- gamma_(MX, object[["hessian"]], J, PPsi, v, nt.full) + V <- crossprod(Gamma) if (adj > 0.0) { # Simplify covariance if sampling assumptions are imposed - if (sampling.fe == "independence") { - V <- V + adj * group_sums_var_(Delta, k.list[[1L]]) + if (sampling_fe == "independence") { + V <- V + adj * group_sums_var_(Delta, k_list[[1L]]) if (k > 1L) { - V <- V + adj * (group_sums_var_(Delta, k.list[[2L]]) - crossprod_(Delta, NA_real_, FALSE, FALSE)) + V <- V + adj * (group_sums_var_(Delta, k_list[[2L]]) - crossprod(Delta)) } - if (panel.structure == "network") { + if (panel_structure == "network") { if (k > 2L) { - V <- V + adj * (group_sums_var_(Delta, k.list[[3L]]) - - crossprod_(Delta, NA_real_, FALSE, FALSE)) + V <- V + adj * (group_sums_var_(Delta, k_list[[3L]]) - + crossprod(Delta)) } } } # Add covariance in case of weak exogeneity - if (weak.exo) { - if (panel.structure == "classic") { - C <- group_sums_cov_(Delta, Gamma, k.list[[1L]]) + if (weak_exo) { + if (panel_structure == "classic") { + C <- group_sums_cov_(Delta, Gamma, k_list[[1L]]) V <- V + adj * (C + t(C)) rm(C) } else { if (k > 2L) { - C <- group_sums_cov_(Delta, Gamma, k.list[[3L]]) + C <- group_sums_cov_(Delta, Gamma, k_list[[3L]]) V <- V + adj * (C + t(C)) rm(C) } @@ -338,9 +338,9 @@ apes <- function( reslist <- list( delta = delta, vcov = V, - panel.structure = panel.structure, - sampling.fe = sampling.fe, - weak.exo = weak.exo + panel_structure = panel_structure, + sampling_fe = sampling_fe, + weak_exo = weak_exo ) # Update result list diff --git a/R/bias_corr.R b/R/bias_corr.R index a728833..1fa900a 100644 --- a/R/bias_corr.R +++ b/R/bias_corr.R @@ -16,7 +16,7 @@ #' weakly exogenous regressors, e.g. lagged outcome variables, we suggest to #' choose a bandwidth between one and four. Note that the order of factors to #' be partialed out is important for bandwidths larger than zero. -#' @param panel.structure a string equal to \code{"classic"} or \code{"network"} +#' @param panel_structure a string equal to \code{"classic"} or \code{"network"} #' which determines the structure of the panel used. \code{"classic"} denotes #' panel structures where for example the same cross-sectional units are #' observed several times (this includes pseudo panels). \code{"network"} @@ -59,7 +59,7 @@ bias_corr <- function( object = NULL, L = 0L, - panel.structure = c("classic", "network")) { + panel_structure = c("classic", "network")) { # Check validity of 'object' if (is.null(object)) { stop("'object' has to be specified.", call. = FALSE) @@ -67,21 +67,21 @@ bias_corr <- function( stop("'bias_corr' called on a non-'feglm' object.", call. = FALSE) } - # Check validity of 'panel.structure' - panel.structure <- match.arg(panel.structure) + # Check validity of 'panel_structure' + panel_structure <- match.arg(panel_structure) # Extract model information - beta.uncorr <- object[["coefficients"]] + beta_uncorr <- object[["coefficients"]] control <- object[["control"]] data <- object[["data"]] eps <- .Machine[["double.eps"]] family <- object[["family"]] formula <- object[["formula"]] - lvls.k <- object[["lvls.k"]] - nms.sp <- names(beta.uncorr) + lvls_k <- object[["lvls_k"]] + nms.sp <- names(beta_uncorr) nt <- object[["nobs"]][["nobs"]] - k.vars <- names(lvls.k) - k <- length(lvls.k) + k_vars <- names(lvls_k) + k <- length(lvls_k) # Check if binary choice model if (family[["family"]] != "binomial") { @@ -92,7 +92,7 @@ bias_corr <- function( } # Check if the number of FEs is > 3 - if (length(lvls.k) > 3) { + if (length(lvls_k) > 3) { stop( "bias_corr() only supports models with up to three-way fixed effects.", call. = FALSE @@ -100,11 +100,11 @@ bias_corr <- function( } # Check if provided object matches requested panel structure - if (panel.structure == "classic") { + if (panel_structure == "classic") { if (!(k %in% c(1L, 2L))) { stop( paste( - "panel.structure == 'classic' expects a one- or two-way fixed", + "panel_structure == 'classic' expects a one- or two-way fixed", "effect model." ), call. = FALSE @@ -114,7 +114,7 @@ bias_corr <- function( if (!(k %in% c(2L, 3L))) { stop( paste( - "panel.structure == 'network' expects a two- or three-way fixed", + "panel_structure == 'network' expects a two- or three-way fixed", "effects model." ), call. = FALSE @@ -129,17 +129,17 @@ bias_corr <- function( wt <- object[["weights"]] # Generate auxiliary list of indexes for different sub panels - k.list <- get_index_list_(k.vars, data) + k_list <- get_index_list_(k_vars, data) # Compute derivatives and weights eta <- object[["eta"]] mu <- family[["linkinv"]](eta) - mu.eta <- family[["mu.eta"]](eta) + mu_eta <- family[["mu.eta"]](eta) v <- wt * (y - mu) - w <- wt * mu.eta + w <- wt * mu_eta z <- wt * partial_mu_eta_(eta, family, 2L) if (family[["link"]] != "logit") { - h <- mu.eta / family[["variance"]](mu) + h <- mu_eta / family[["variance"]](mu) v <- h * v w <- h * w z <- h * z @@ -147,71 +147,71 @@ bias_corr <- function( } # Center regressor matrix (if required) - if (control[["keep.mx"]]) { + if (control[["keep_mx"]]) { MX <- object[["MX"]] } else { - MX <- center_variables_(X, NA_real_, w, k.list, control[["center.tol"]], 100000L, FALSE) + MX <- center_variables_r_(X, w, k_list, control[["center_tol"]], 10000L) } # Compute bias terms for requested bias correction - if (panel.structure == "classic") { + if (panel_structure == "classic") { # Compute \hat{B} and \hat{D} - b <- as.vector(group_sums_(MX * z, w, k.list[[1L]])) / 2.0 / nt + b <- as.vector(group_sums_(MX * z, w, k_list[[1L]])) / 2.0 / nt if (k > 1L) { - b <- b + as.vector(group_sums_(MX * z, w, k.list[[2L]])) / 2.0 / nt + b <- b + as.vector(group_sums_(MX * z, w, k_list[[2L]])) / 2.0 / nt } # Compute spectral density part of \hat{B} if (L > 0L) { - b <- (b + group_sums_spectral_(MX * w, v, w, L, k.list[[1L]])) / nt + b <- (b + group_sums_spectral_(MX * w, v, w, L, k_list[[1L]])) / nt } } else { # Compute \hat{D}_{1}, \hat{D}_{2}, and \hat{B} - b <- group_sums_(MX * z, w, k.list[[1L]]) / (2.0 * nt) - b <- (b + group_sums_(MX * z, w, k.list[[2L]])) / (2.0 * nt) + b <- group_sums_(MX * z, w, k_list[[1L]]) / (2.0 * nt) + b <- (b + group_sums_(MX * z, w, k_list[[2L]])) / (2.0 * nt) if (k > 2L) { - b <- (b + group_sums_(MX * z, w, k.list[[3L]])) / (2.0 * nt) + b <- (b + group_sums_(MX * z, w, k_list[[3L]])) / (2.0 * nt) } # Compute spectral density part of \hat{B} if (k > 2L && L > 0L) { - b <- (b + group_sums_spectral_(MX * w, v, w, L, k.list[[3L]])) / nt + b <- (b + group_sums_spectral_(MX * w, v, w, L, k_list[[3L]])) / nt } } # Compute bias-corrected structural parameters - beta <- solve_bias_(beta.uncorr, object[["Hessian"]], nt, -b) + beta <- beta_uncorr - solve(object[["hessian"]] / nt, b) names(beta) <- nms.sp # Update \eta and first- and second-order derivatives - eta <- feglm_offset_(object, solve_y_(X, beta)) + eta <- feglm_offset_(object, X %*% beta) mu <- family[["linkinv"]](eta) - mu.eta <- family[["mu.eta"]](eta) + mu_eta <- family[["mu.eta"]](eta) v <- wt * (y - mu) - w <- wt * mu.eta + w <- wt * mu_eta if (family[["link"]] != "logit") { - h <- mu.eta / family[["variance"]](mu) + h <- mu_eta / family[["variance"]](mu) v <- h * v w <- h * w rm(h) } # Update centered regressor matrix - MX <- center_variables_(X, NA_real_, w, k.list, control[["center.tol"]], 100000L, FALSE) + MX <- center_variables_r_(X, w, k_list, control[["center_tol"]], 10000L) colnames(MX) <- nms.sp - # Update Hessian - H <- crossprod_(MX, w, TRUE, TRUE) + # Update hessian + H <- crossprod(MX * sqrt(w)) dimnames(H) <- list(nms.sp, nms.sp) # Update result list object[["coefficients"]] <- beta object[["eta"]] <- eta - if (control[["keep.mx"]]) object[["MX"]] <- MX - object[["Hessian"]] <- H - object[["coefficients.uncorr"]] <- beta.uncorr - object[["bias.term"]] <- b - object[["panel.structure"]] <- panel.structure + if (control[["keep_mx"]]) object[["MX"]] <- MX + object[["hessian"]] <- H + object[["coefficients_uncorr"]] <- beta_uncorr + object[["bias_term"]] <- b + object[["panel_structure"]] <- panel_structure object[["bandwidth"]] <- L # Add additional class to result list diff --git a/R/capybara-package.R b/R/capybara-package.R index 3bd6236..1981179 100644 --- a/R/capybara-package.R +++ b/R/capybara-package.R @@ -18,7 +18,7 @@ #' @importFrom MASS negative.binomial theta.ml #' @importFrom rlang sym := #' @importFrom stats as.formula binomial model.matrix na.omit gaussian poisson -#' pnorm printCoefmat rgamma rlogis rnorm rpois terms vcov predict sd +#' pnorm printCoefmat rgamma rlogis rnorm rpois terms vcov predict var #' complete.cases #' @importFrom utils combn #' @useDynLib capybara, .registration = TRUE diff --git a/R/cpp11.R b/R/cpp11.R index 8bf0895..68b05aa 100644 --- a/R/cpp11.R +++ b/R/cpp11.R @@ -1,7 +1,7 @@ # Generated by cpp11: do not edit by hand -center_variables_ <- function(V_r, v_sum_r, w_r, klist, tol, maxiter, sum_v) { - .Call(`_capybara_center_variables_`, V_r, v_sum_r, w_r, klist, tol, maxiter, sum_v) +center_variables_r_ <- function(V_r, w_r, klist, tol, maxiter) { + .Call(`_capybara_center_variables_r_`, V_r, w_r, klist, tol, maxiter) } get_alpha_ <- function(p_r, klist, tol) { @@ -24,52 +24,12 @@ group_sums_cov_ <- function(M_r, N_r, jlist) { .Call(`_capybara_group_sums_cov_`, M_r, N_r, jlist) } -crossprod_ <- function(x, w, weighted, root_weights) { - .Call(`_capybara_crossprod_`, x, w, weighted, root_weights) +feglm_fit_ <- function(beta_r, eta_r, y_r, x_r, wt_r, theta, family, control, k_list) { + .Call(`_capybara_feglm_fit_`, beta_r, eta_r, y_r, x_r, wt_r, theta, family, control, k_list) } -gamma_ <- function(mx, hessian, j, ppsi, v, nt_full) { - .Call(`_capybara_gamma_`, mx, hessian, j, ppsi, v, nt_full) -} - -inv_ <- function(h) { - .Call(`_capybara_inv_`, h) -} - -rank_ <- function(x) { - .Call(`_capybara_rank_`, x) -} - -solve_bias_ <- function(beta_uncorr, hessian, nt, b) { - .Call(`_capybara_solve_bias_`, beta_uncorr, hessian, nt, b) -} - -solve_y_ <- function(a, x) { - .Call(`_capybara_solve_y_`, a, x) -} - -sandwich_ <- function(a, b) { - .Call(`_capybara_sandwich_`, a, b) -} - -update_beta_eta_ <- function(old, upd, param) { - .Call(`_capybara_update_beta_eta_`, old, upd, param) -} - -update_nu_ <- function(y, mu, mu_eta) { - .Call(`_capybara_update_nu_`, y, mu, mu_eta) -} - -solve_beta_ <- function(mx, mnu, wtilde, weighted) { - .Call(`_capybara_solve_beta_`, mx, mnu, wtilde, weighted) -} - -solve_eta_ <- function(mx, mnu, nu, beta) { - .Call(`_capybara_solve_eta_`, mx, mnu, nu, beta) -} - -solve_eta2_ <- function(yadj, myadj, offset, eta) { - .Call(`_capybara_solve_eta2_`, yadj, myadj, offset, eta) +feglm_offset_fit_ <- function(eta_r, y_r, offset_r, wt_r, family, control, k_list) { + .Call(`_capybara_feglm_offset_fit_`, eta_r, y_r, offset_r, wt_r, family, control, k_list) } kendall_cor_ <- function(m) { diff --git a/R/feglm.R b/R/feglm.R index 60fe7e6..37dce94 100644 --- a/R/feglm.R +++ b/R/feglm.R @@ -21,10 +21,10 @@ #' details of family functions. #' @param weights an optional string with the name of the 'prior weights' #' variable in \code{data}. -#' @param beta.start an optional vector of starting values for the structural +#' @param beta_start an optional vector of starting values for the structural #' parameters in the linear predictor. Default is #' \eqn{\boldsymbol{\beta} = \mathbf{0}}{\beta = 0}. -#' @param eta.start an optional vector of starting values for the linear +#' @param eta_start an optional vector of starting values for the linear #' predictor. #' @param control a named list of parameters for controlling the fitting #' process. See \code{\link{feglm_control}} for details. @@ -68,8 +68,8 @@ feglm <- function( data = NULL, family = gaussian(), weights = NULL, - beta.start = NULL, - eta.start = NULL, + beta_start = NULL, + eta_start = NULL, control = NULL) { # Check validity of formula ---- check_formula_(formula) @@ -88,32 +88,32 @@ feglm <- function( # Generate model.frame lhs <- NA # just to avoid global variable warning - nobs.na <- NA - nobs.full <- NA + nobs_na <- NA + nobs_full <- NA model_frame_(data, formula, weights) # Ensure that model response is in line with the chosen model ---- check_response_(data, lhs, family) # Get names of the fixed effects variables and sort ---- - k.vars <- attr(terms(formula, rhs = 2L), "term.labels") - k <- length(k.vars) + k_vars <- attr(terms(formula, rhs = 2L), "term.labels") + k <- length(k_vars) # Generate temporary variable ---- tmp.var <- temp_var_(data) # Drop observations that do not contribute to the log likelihood ---- - data <- drop_by_link_type_(data, lhs, family, tmp.var, k.vars, control) + data <- drop_by_link_type_(data, lhs, family, tmp.var, k_vars, control) # Transform fixed effects and clusters to factors ---- - data <- transform_fe_(data, formula, k.vars) + data <- transform_fe_(data, formula, k_vars) # Determine the number of dropped observations ---- nt <- nrow(data) - nobs <- nobs_(nobs.full, nobs.na, nt) + nobs <- nobs_(nobs_full, nobs_na, nt) # Extract model response and regressor matrix ---- - nms.sp <- NA + nms_sp <- NA p <- NA model_response_(data, formula) @@ -131,44 +131,41 @@ feglm <- function( check_weights_(wt) # Compute and check starting guesses ---- - start_guesses_(beta.start, eta.start, y, X, beta, nt, wt, p, family) + start_guesses_(beta_start, eta_start, y, X, beta, nt, wt, p, family) # Get names and number of levels in each fixed effects category ---- - nms.fe <- lapply(select(data, all_of(k.vars)), levels) - lvls.k <- vapply(nms.fe, length, integer(1)) + nms_fe <- lapply(select(data, all_of(k_vars)), levels) + lvls_k <- vapply(nms_fe, length, integer(1)) # Generate auxiliary list of indexes for different sub panels ---- - k.list <- get_index_list_(k.vars, data) + k_list <- get_index_list_(k_vars, data) # Fit generalized linear model ---- + if (is.integer(y)) { y <- as.numeric(y) } fit <- feglm_fit_( - beta, eta, y, X, wt, k.list, family, control + beta, eta, y, X, wt, 0.0, family[["family"]], control, k_list ) y <- NULL X <- NULL eta <- NULL - # Add names to beta, Hessian, and MX (if provided) ---- - names(fit[["coefficients"]]) <- nms.sp - if (control[["keep.mx"]]) { - colnames(fit[["MX"]]) <- nms.sp + # Add names to beta, hessian, and MX (if provided) ---- + names(fit[["coefficients"]]) <- nms_sp + if (control[["keep_mx"]]) { + colnames(fit[["MX"]]) <- nms_sp } - dimnames(fit[["Hessian"]]) <- list(nms.sp, nms.sp) - - # Generate result list ---- - reslist <- c( - fit, list( - nobs = nobs, - lvls.k = lvls.k, - nms.fe = nms.fe, - formula = formula, - data = data, - family = family, - control = control - ) - ) + dimnames(fit[["hessian"]]) <- list(nms_sp, nms_sp) + + # Add to fit list ---- + fit[["nobs"]] <- nobs + fit[["lvls_k"]] <- lvls_k + fit[["nms_fe"]] <- nms_fe + fit[["formula"]] <- formula + fit[["data"]] <- data + fit[["family"]] <- family + fit[["control"]] <- control # Return result list ---- - structure(reslist, class = "feglm") + structure(fit, class = "feglm") } diff --git a/R/feglm_control.R b/R/feglm_control.R index 838baef..31288f9 100644 --- a/R/feglm_control.R +++ b/R/feglm_control.R @@ -3,28 +3,28 @@ #' @description Set and change parameters used for fitting \code{\link{feglm}}. #' Termination conditions are similar to \code{\link[stats]{glm}}. #' -#' @param dev.tol tolerance level for the first stopping condition of the +#' @param dev_tol tolerance level for the first stopping condition of the #' maximization routine. The stopping condition is based on the relative change #' of the deviance in iteration \eqn{r} and can be expressed as follows: #' \eqn{|dev_{r} - dev_{r - 1}| / (0.1 + |dev_{r}|) < tol}{|dev - devold| / #' (0.1 + |dev|) < tol}. The default is \code{1.0e-08}. -#' @param center.tol tolerance level for the stopping condition of the centering +#' @param center_tol tolerance level for the stopping condition of the centering #' algorithm. The stopping condition is based on the relative change of the #' centered variable similar to the \code{'lfe'} package. The default is #' \code{1.0e-08}. -#' @param iter.max unsigned integer indicating the maximum number of iterations +#' @param iter_max unsigned integer indicating the maximum number of iterations #' in the maximization routine. The default is \code{25L}. #' @param limit unsigned integer indicating the maximum number of iterations of #' \code{\link[MASS]{theta.ml}}. The default is \code{10L}. #' @param trace logical indicating if output should be produced in each #' iteration. Default is \code{FALSE}. -#' @param drop.pc logical indicating to drop observations that are perfectly +#' @param drop_pc logical indicating to drop observations that are perfectly #' classified/separated and hence do not contribute to the log-likelihood. This #' option is useful to reduce the computational costs of the maximization #' problem and improves the numerical stability of the algorithm. Note that #' dropping perfectly separated observations does not affect the estimates. #' The default is \code{TRUE}. -#' @param keep.mx logical indicating if the centered regressor matrix should be +#' @param keep_mx logical indicating if the centered regressor matrix should be #' stored. The centered regressor matrix is required for some covariance #' estimators, bias corrections, and average partial effects. This option saves #' some computation time at the cost of memory. The default is \code{TRUE}. @@ -33,24 +33,24 @@ #' #' @seealso \code{\link{feglm}} feglm_control <- function( - dev.tol = 1.0e-08, - center.tol = 1.0e-08, - iter.max = 25L, + dev_tol = 1.0e-08, + center_tol = 1.0e-08, + iter_max = 25L, limit = 10L, trace = FALSE, - drop.pc = TRUE, - keep.mx = TRUE) { + drop_pc = TRUE, + keep_mx = FALSE) { # Check validity of tolerance parameters - if (dev.tol <= 0.0 || center.tol <= 0.0) { + if (dev_tol <= 0.0 || center_tol <= 0.0) { stop( "All tolerance parameters should be greater than zero.", call. = FALSE ) } - # Check validity of 'iter.max' - iter.max <- as.integer(iter.max) - if (iter.max < 1L) { + # Check validity of 'iter_max' + iter_max <- as.integer(iter_max) + if (iter_max < 1L) { stop( "Maximum number of iterations should be at least one.", call. = FALSE @@ -65,12 +65,12 @@ feglm_control <- function( # Return list with control parameters list( - dev.tol = dev.tol, - center.tol = center.tol, - iter.max = iter.max, + dev_tol = dev_tol, + center_tol = center_tol, + iter_max = iter_max, limit = limit, trace = as.logical(trace), - drop.pc = as.logical(drop.pc), - keep.mx = as.logical(keep.mx) + drop_pc = as.logical(drop_pc), + keep_mx = as.logical(keep_mx) ) } diff --git a/R/feglm_offset.R b/R/feglm_offset.R new file mode 100644 index 0000000..b46ca70 --- /dev/null +++ b/R/feglm_offset.R @@ -0,0 +1,43 @@ +# Efficient offset algorithm to update the linear predictor ---- + +feglm_offset_ <- function(object, offset) { + # Check validity of 'object' + if (!inherits(object, "feglm")) { + stop("'feglm_offset_' called on a non-'feglm' object.") + } + + # Extract required quantities from result list + control <- object[["control"]] + data <- object[["data"]] + wt <- object[["weights"]] + family <- object[["family"]] + formula <- object[["formula"]] + lvls_k <- object[["lvls_k"]] + nt <- object[["nobs"]][["nobs"]] + k_vars <- names(lvls_k) + + # Extract dependent variable + y <- data[[1L]] + + # Extract control arguments + center_tol <- control[["center_tol"]] + dev_tol <- control[["dev_tol"]] + iter_max <- control[["iter_max"]] + + # Generate auxiliary list of indexes to project out the fixed effects + k_list <- get_index_list_(k_vars, data) + + # Compute starting guess for eta + if (family[["family"]] == "binomial") { + eta <- rep(family[["linkfun"]](sum(wt * (y + 0.5) / 2.0) / sum(wt)), nt) + } else if (family[["family"]] %in% c("Gamma", "inverse.gaussian")) { + eta <- rep(family[["linkfun"]](sum(wt * y) / sum(wt)), nt) + } else { + eta <- rep(family[["linkfun"]](sum(wt * (y + 0.1)) / sum(wt)), nt) + } + + # Return eta + if (is.integer(y)) { y <- as.numeric(y) } + feglm_offset_fit_(eta, y, offset, wt, family[["family"]], control, + k_list) +} diff --git a/R/felm.R b/R/felm.R index 8eecdba..c939d0b 100644 --- a/R/felm.R +++ b/R/felm.R @@ -35,7 +35,7 @@ felm <- function(formula = NULL, data = NULL, weights = NULL) { names(reslist)[which(names(reslist) == "eta")] <- "fitted.values" - # reslist[["Hessian"]] <- NULL + # reslist[["hessian"]] <- NULL reslist[["family"]] <- NULL reslist[["deviance"]] <- NULL diff --git a/R/fenegbin.R b/R/fenegbin.R index 3211a96..7f690eb 100644 --- a/R/fenegbin.R +++ b/R/fenegbin.R @@ -2,7 +2,7 @@ #' effects #' @description A routine that uses the same internals as \code{\link{feglm}}. #' @inheritParams feglm -#' @param init.theta an optional initial value for the theta parameter (see +#' @param init_theta an optional initial value for the theta parameter (see #' \code{\link[MASS]{glm.nb}}). #' @param link the link function. Must be one of \code{"log"}, \code{"sqrt"}, or #' \code{"identity"}. @@ -22,9 +22,9 @@ fenegbin <- function( formula = NULL, data = NULL, weights = NULL, - beta.start = NULL, - eta.start = NULL, - init.theta = NULL, + beta_start = NULL, + eta_start = NULL, + init_theta = NULL, link = c("log", "identity", "sqrt"), control = NULL) { # Check validity of formula ---- @@ -44,36 +44,36 @@ fenegbin <- function( # Generate model.frame lhs <- NA # just to avoid global variable warning - nobs.na <- NA - nobs.full <- NA + nobs_na <- NA + nobs_full <- NA model_frame_(data, formula, weights) # Check starting guess of theta ---- - family <- init_theta_(init.theta, link) - rm(init.theta) + family <- init_theta_(init_theta, link) + rm(init_theta) # Ensure that model response is in line with the chosen model ---- check_response_(data, lhs, family) # Get names of the fixed effects variables and sort ---- - k.vars <- attr(terms(formula, rhs = 2L), "term.labels") - k <- length(k.vars) + k_vars <- attr(terms(formula, rhs = 2L), "term.labels") + k <- length(k_vars) # Generate temporary variable ---- tmp.var <- temp_var_(data) # Drop observations that do not contribute to the log likelihood ---- - data <- drop_by_link_type_(data, lhs, family, tmp.var, k.vars, control) + data <- drop_by_link_type_(data, lhs, family, tmp.var, k_vars, control) # Transform fixed effects and clusters to factors ---- - data <- transform_fe_(data, formula, k.vars) + data <- transform_fe_(data, formula, k_vars) # Determine the number of dropped observations ---- nt <- nrow(data) - nobs <- nobs_(nobs.full, nobs.na, nt) + nobs <- nobs_(nobs_full, nobs_na, nt) # Extract model response and regressor matrix ---- - nms.sp <- NA + nms_sp <- NA p <- NA model_response_(data, formula) @@ -91,29 +91,23 @@ fenegbin <- function( check_weights_(wt) # Compute and check starting guesses ---- - start_guesses_(beta.start, eta.start, y, X, beta, nt, wt, p, family) + start_guesses_(beta_start, eta_start, y, X, beta, nt, wt, p, family) # Get names and number of levels in each fixed effects category ---- - nms.fe <- lapply(select(data, all_of(k.vars)), levels) - lvls.k <- vapply(nms.fe, length, integer(1)) + nms_fe <- lapply(select(data, all_of(k_vars)), levels) + lvls_k <- vapply(nms_fe, length, integer(1)) # Generate auxiliary list of indexes for different sub panels ---- - k.list <- get_index_list_(k.vars, data) + k_list <- get_index_list_(k_vars, data) # Extract control arguments ---- - tol <- control[["dev.tol"]] + tol <- control[["dev_tol"]] limit <- control[["limit"]] - iter.max <- control[["iter.max"]] + iter_max <- control[["iter_max"]] trace <- control[["trace"]] # Initial negative binomial fit ---- - fit <- feglm_fit_( - beta, eta, y, X, wt, k.list, family, control - ) - beta <- fit[["coefficients"]] - eta <- fit[["eta"]] - dev <- fit[["deviance"]] theta <- suppressWarnings( theta.ml( y = y, @@ -124,17 +118,21 @@ fenegbin <- function( ) ) + fit <- feglm_fit_( + beta, eta, y, X, wt, theta, family[["family"]], control, k_list + ) + + beta <- fit[["coefficients"]] + eta <- fit[["eta"]] + dev <- fit[["deviance"]] + # Alternate between fitting glm and \theta ---- conv <- FALSE - for (iter in seq.int(iter.max)) { + for (iter in seq.int(iter_max)) { # Fit negative binomial model dev.old <- dev theta.old <- theta family <- negative.binomial(theta, link) - fit <- feglm_fit_(beta, eta, y, X, wt, k.list, family, control) - beta <- fit[["coefficients"]] - eta <- fit[["eta"]] - dev <- fit[["deviance"]] theta <- suppressWarnings( theta.ml( y = y, @@ -144,6 +142,10 @@ fenegbin <- function( trace = trace ) ) + fit <- feglm_fit_(beta, eta, y, X, wt, theta, family[["family"]], control, k_list) + beta <- fit[["coefficients"]] + eta <- fit[["eta"]] + dev <- fit[["deviance"]] # Progress information if (trace) { @@ -171,12 +173,12 @@ fenegbin <- function( # Information if convergence failed ---- if (!conv && trace) cat("Algorithm did not converge.\n") - # Add names to beta, Hessian, and MX (if provided) ---- - names(fit[["coefficients"]]) <- nms.sp - if (control[["keep.mx"]]) { - colnames(fit[["MX"]]) <- nms.sp + # Add names to beta, hessian, and MX (if provided) ---- + names(fit[["coefficients"]]) <- nms_sp + if (control[["keep_mx"]]) { + colnames(fit[["MX"]]) <- nms_sp } - dimnames(fit[["Hessian"]]) <- list(nms.sp, nms.sp) + dimnames(fit[["hessian"]]) <- list(nms_sp, nms_sp) # Generate result list ---- reslist <- c( @@ -185,8 +187,8 @@ fenegbin <- function( iter.outer = iter, conv.outer = conv, nobs = nobs, - lvls.k = lvls.k, - nms.fe = nms.fe, + lvls_k = lvls_k, + nms_fe = nms_fe, formula = formula, data = data, family = family, diff --git a/R/fepoisson.R b/R/fepoisson.R index 3a9ad24..0c6a03e 100644 --- a/R/fepoisson.R +++ b/R/fepoisson.R @@ -18,35 +18,11 @@ fepoisson <- function( formula = NULL, data = NULL, weights = NULL, - beta.start = NULL, - eta.start = NULL, + beta_start = NULL, + eta_start = NULL, control = NULL) { feglm( formula = formula, data = data, weights = weights, family = poisson(), - beta.start = beta.start, eta.start = eta.start, control = control + beta_start = beta_start, eta_start = eta_start, control = control ) } - -# fequasipoisson <- function( -# formula = NULL, -# data = NULL, -# weights = NULL, -# beta.start = NULL, -# eta.start = NULL, -# control = NULL) { -# # Fit the model using standard Poisson assumptions -# fit <- feglm( -# formula = formula, data = data, weights = weights, family = poisson(), -# beta.start = beta.start, eta.start = eta.start, control = control -# ) - -# # Estimate the dispersion parameter (phi) -# fitted_values <- predict(object, type = "response") -# residuals <- unlist(object$data[, 1], use.names = FALSE) - fitted_values -# phi <- sum((residuals^2) / fitted_values) / fit$df.residual? - -# # Adjust model diagnostics for Quasi Poisson -# fit$std.errors <- sqrt(phi) * fit$std.errors - -# return(fit) -# } diff --git a/R/fixed_effects.R b/R/fixed_effects.R index ffcbd07..b127902 100644 --- a/R/fixed_effects.R +++ b/R/fixed_effects.R @@ -4,7 +4,7 @@ #' function has to be applied to our solution to get meaningful estimates of #' the fixed effects. #' @param object an object of class \code{"feglm"}. -#' @param alpha.tol tolerance level for the stopping condition. The algorithm is +#' @param alpha_tol tolerance level for the stopping condition. The algorithm is #' stopped at iteration \eqn{i} if \eqn{||\boldsymbol{\alpha}_{i} - #' \boldsymbol{\alpha}_{i - 1}||_{2} < tol ||\boldsymbol{\alpha}_{i - 1}|| #' {2}}{||\Delta \alpha|| < tol ||\alpha_old||}. Default is \code{1.0e-08}. @@ -23,7 +23,7 @@ #' #' fixed_effects(mod) #' @export -fixed_effects <- function(object = NULL, alpha.tol = 1.0e-08) { +fixed_effects <- function(object = NULL, alpha_tol = 1.0e-08) { # Check validity of 'object' if (is.null(object)) { stop("'object' has to be specified.", call. = FALSE) @@ -39,31 +39,31 @@ fixed_effects <- function(object = NULL, alpha.tol = 1.0e-08) { beta <- object[["coefficients"]] data <- object[["data"]] formula <- object[["formula"]] - lvls.k <- object[["lvls.k"]] - nms.fe <- object[["nms.fe"]] - k.vars <- names(lvls.k) - k <- length(lvls.k) + lvls_k <- object[["lvls_k"]] + nms_fe <- object[["nms_fe"]] + k_vars <- names(lvls_k) + k <- length(lvls_k) eta <- object[["eta"]] # Extract regressor matrix X <- model.matrix(formula, data, rhs = 1L)[, -1L, drop = FALSE] - nms.sp <- attr(X, "dimnames")[[2L]] + nms_sp <- attr(X, "dimnames")[[2L]] attr(X, "dimnames") <- NULL # Generate auxiliary list of indexes for different sub panels - k.list <- get_index_list_(k.vars, data) + k_list <- get_index_list_(k_vars, data) # Recover fixed effects by alternating the solutions of normal equations - pie <- eta - solve_y_(X, beta) - fe.list <- as.list(get_alpha_(pie, k.list, alpha.tol)) + pie <- eta - X %*% beta + fe_list <- as.list(get_alpha_(pie, k_list, alpha_tol)) # Assign names to the different fixed effects categories for (i in seq.int(k)) { - colnames(fe.list[[i]]) <- k.vars[i] - rownames(fe.list[[i]]) <- nms.fe[[i]] + colnames(fe_list[[i]]) <- k_vars[i] + rownames(fe_list[[i]]) <- nms_fe[[i]] } - names(fe.list) <- k.vars + names(fe_list) <- k_vars # Return list of estimated fixed effects - fe.list + fe_list } diff --git a/R/generics_glance.R b/R/generics_glance.R index 3c80200..7dbdf70 100644 --- a/R/generics_glance.R +++ b/R/generics_glance.R @@ -9,10 +9,10 @@ glance.feglm <- function(x, ...) { summary(x), data.frame( deviance = deviance, - null.deviance = null.deviance, - nobs.full = nobs["nobs.full"], - nobs.na = nobs["nobs.na"], - nobs.pc = nobs["nobs.pc"], + null_deviance = null_deviance, + nobs_full = nobs["nobs_full"], + nobs_na = nobs["nobs_na"], + nobs_pc = nobs["nobs_pc"], nobs = nobs["nobs"] ) ) @@ -29,9 +29,9 @@ glance.felm <- function(x, ...) { tibble( r.squared = r.squared, adj.r.squared = adj.r.squared, - nobs.full = nobs["nobs.full"], - nobs.na = nobs["nobs.na"], - nobs.pc = nobs["nobs.pc"], + nobs_full = nobs["nobs_full"], + nobs_na = nobs["nobs_na"], + nobs_pc = nobs["nobs_pc"], nobs = nobs["nobs"] ) ) diff --git a/R/generics_print.R b/R/generics_print.R index 5d6823e..257c16b 100644 --- a/R/generics_print.R +++ b/R/generics_print.R @@ -137,8 +137,8 @@ summary_nobs_ <- function(x) { cat( "\nNumber of observations:", paste0("Full ", x[["nobs"]][["nobs"]], ";"), - paste0("Missing ", x[["nobs"]][["nobs.na"]], ";"), - paste0("Perfect classification ", x[["nobs"]][["nobs.pc"]]), "\n" + paste0("Missing ", x[["nobs"]][["nobs_na"]], ";"), + paste0("Perfect classification ", x[["nobs"]][["nobs_pc"]]), "\n" ) } @@ -171,7 +171,7 @@ print.feglm <- function(x, digits = max(3L, getOption("digits") - 3L), ...) { cat( sub("\\(.*\\)", "", x[["family"]][["family"]]), " - ", x[["family"]][["link"]], " link", - ", l= [", paste0(x[["lvls.k"]], collapse = ", "), "]\n\n", + ", l= [", paste0(x[["lvls_k"]], collapse = ", "), "]\n\n", sep = "" ) print(x[["coefficients"]], digits = digits) diff --git a/R/generics_summary.R b/R/generics_summary.R index ed8fe7c..7469acd 100644 --- a/R/generics_summary.R +++ b/R/generics_summary.R @@ -37,10 +37,10 @@ summary.feglm <- function( res <- list( cm = cm, deviance = object[["deviance"]], - null.deviance = object[["null.deviance"]], + null_deviance = object[["null_deviance"]], iter = object[["iter"]], nobs = object[["nobs"]], - lvls.k = object[["lvls.k"]], + lvls_k = object[["lvls_k"]], formula = object[["formula"]], family = object[["family"]] ) @@ -87,7 +87,7 @@ summary.felm <- function( e_sq <- (y - object[["fitted.values"]])^2 tss <- sum(w * ydemeaned_sq) rss <- sum(w * e_sq) - n <- unname(object[["nobs"]]["nobs.full"]) + n <- unname(object[["nobs"]]["nobs_full"]) k <- length(object[["coefficients"]]) + sum(vapply(object[["nms.fe"]], length, integer(1))) @@ -95,7 +95,7 @@ summary.felm <- function( res <- list( cm = cm, nobs = object[["nobs"]], - lvls.k = object[["lvls.k"]], + lvls_k = object[["lvls_k"]], formula = object[["formula"]], r.squared = 1 - (rss / tss), adj.r.squared = 1 - (rss / tss) * ((n - 1) / (n - k)) diff --git a/R/helpers.R b/R/helpers.R index 308ce71..57bde2d 100644 --- a/R/helpers.R +++ b/R/helpers.R @@ -10,6 +10,36 @@ check_factor_ <- function(x) { # Higher-order partial derivatives ---- +second_order_derivative_ <- function(eta, f, family) { + link <- family[["link"]] + linkinv_eta <- family[["linkinv"]](eta) + + if (link == "logit") { + return(f * (1.0 - 2.0 * linkinv_eta)) + } else if (link == "probit") { + return(-eta * f) + } else if (link == "cloglog") { + return(f * (1.0 - exp(eta))) + } else { + return(-2.0 * eta / (1.0 + eta^2) * f) + } +} + +third_order_derivative_ <- function(eta, f, family) { + link <- family[["link"]] + linkinv_eta <- family[["linkinv"]](eta) + + if (link == "logit") { + return(f * ((1.0 - 2.0 * linkinv_eta)^2 - 2.0 * f)) + } else if (link == "probit") { + return((eta^2 - 1.0) * f) + } else if (link == "cloglog") { + return(f * (1.0 - exp(eta)) * (2.0 - exp(eta)) - f) + } else { + return((6.0 * eta^2 - 2.0) / (1.0 + eta^2)^2 * f) + } +} + partial_mu_eta_ <- function(eta, family, order) { # Safeguard eta if necessary if (family[["link"]] != "logit") { @@ -19,27 +49,9 @@ partial_mu_eta_ <- function(eta, family, order) { f <- family[["mu.eta"]](eta) if (order == 2L) { - # Second-order derivative - if (family[["link"]] == "logit") { - f * (1.0 - 2.0 * family[["linkinv"]](eta)) - } else if (family[["link"]] == "probit") { - -eta * f - } else if (family[["link"]] == "cloglog") { - f * (1.0 - exp(eta)) - } else { - -2.0 * eta / (1.0 + eta^2) * f - } + return(second_order_derivative_(eta, f, family)) } else { - # Third-order derivative - if (family[["link"]] == "logit") { - f * ((1.0 - 2.0 * family[["linkinv"]](eta))^2 - 2.0 * f) - } else if (family[["link"]] == "probit") { - (eta^2 - 1.0) * f - } else if (family[["link"]] == "cloglog") { - f * (1.0 - exp(eta)) * (2.0 - exp(eta)) - f - } else { - (6.0 * eta^2 - 2.0) / (1.0 + eta^2)^2 * f - } + return(third_order_derivative_(eta, f, family)) } } @@ -92,6 +104,12 @@ check_family_ <- function(family) { } else if (startsWith(family[["family"]], "Negative Binomial")) { stop("Please use 'fenegbin' instead.", call. = FALSE) } + + if (family[["family"]] == "binomial" && family[["link"]] != "logit") { + stop("The current version only supports logit in the binomial family. + This is because I had to rewrite the links in C++ to use those with Armadillo. + Send me a Pull Request or open an issue if you need Probit.", call. = FALSE) + } } update_formula_ <- function(formula) { @@ -112,17 +130,17 @@ model_frame_ <- function(data, formula, weights) { lhs <- names(data)[[1L]] - nobs.full <- nrow(data) + nobs_full <- nrow(data) data <- na.omit(data) - nobs.na <- nobs.full - nrow(data) - nobs.full <- nrow(data) + nobs_na <- nobs_full - nrow(data) + nobs_full <- nrow(data) assign("data", data, envir = parent.frame()) assign("lhs", lhs, envir = parent.frame()) - assign("nobs.na", nobs.na, envir = parent.frame()) - assign("nobs.full", nobs.full, envir = parent.frame()) + assign("nobs_na", nobs_na, envir = parent.frame()) + assign("nobs_full", nobs_full, envir = parent.frame()) } check_response_ <- function(data, lhs, family) { @@ -167,7 +185,7 @@ check_response_ <- function(data, lhs, family) { drop_by_link_type_ <- function(data, lhs, family, tmp.var, k.vars, control) { if (family[["family"]] %in% c("binomial", "poisson")) { - if (control[["drop.pc"]]) { + if (control[["drop_pc"]]) { repeat { # Drop observations that do not contribute to the log-likelihood ncheck <- nrow(data) @@ -206,11 +224,11 @@ transform_fe_ <- function(data, formula, k.vars) { data } -nobs_ <- function(nobs.full, nobs.na, nt) { +nobs_ <- function(nobs_full, nobs_na, nt) { c( - nobs.full = nobs.full, - nobs.na = nobs.na, - nobs.pc = nobs.full - nt, + nobs_full = nobs_full, + nobs_na = nobs_na, + nobs_pc = nobs_full - nt, nobs = nt ) } @@ -218,18 +236,18 @@ nobs_ <- function(nobs.full, nobs.na, nt) { model_response_ <- function(data, formula) { y <- data[[1L]] X <- model.matrix(formula, data, rhs = 1L)[, -1L, drop = FALSE] - nms.sp <- attr(X, "dimnames")[[2L]] + nms_sp <- attr(X, "dimnames")[[2L]] attr(X, "dimnames") <- NULL p <- ncol(X) assign("y", y, envir = parent.frame()) assign("X", X, envir = parent.frame()) - assign("nms.sp", nms.sp, envir = parent.frame()) + assign("nms_sp", nms_sp, envir = parent.frame()) assign("p", p, envir = parent.frame()) } check_linear_dependence_ <- function(X, p) { - if (rank_(X) < p) { + if (qr(X)$rank < p) { stop("Linear dependent terms detected.", call. = FALSE) } } @@ -247,7 +265,7 @@ init_theta_ <- function(init.theta, link) { if (is.null(init.theta)) { family <- poisson(link) } else { - # Validity of input argument (beta.start) + # Validity of input argument (beta_start) if (length(init.theta) != 1L) { stop("'init.theta' has to be a scalar.", call. = FALSE) } else if (init.theta <= 0.0) { @@ -260,23 +278,23 @@ init_theta_ <- function(init.theta, link) { } start_guesses_ <- function( - beta.start, eta.start, y, X, beta, nt, wt, p, family) { - if (!is.null(beta.start) || !is.null(eta.start)) { - # If both are specified, ignore eta.start - if (!is.null(beta.start) && !is.null(eta.start)) { + beta_start, eta_start, y, X, beta, nt, wt, p, family) { + if (!is.null(beta_start) || !is.null(eta_start)) { + # If both are specified, ignore eta_start + if (!is.null(beta_start) && !is.null(eta_start)) { warning( - "'beta.start' and 'eta.start' are specified. Ignoring 'eta.start'.", + "'beta_start' and 'eta_start' are specified. Ignoring 'eta_start'.", call. = FALSE ) } # Compute and check starting guesses - if (!is.null(beta.start)) { - # Validity of input argument (beta.start) - if (length(beta.start) != p) { + if (!is.null(beta_start)) { + # Validity of input argument (beta_start) + if (length(beta_start) != p) { stop( paste( - "Length of 'beta.start' has to be equal to the number of", + "Length of 'beta_start' has to be equal to the number of", "structural parameters." ), call. = FALSE @@ -284,14 +302,14 @@ start_guesses_ <- function( } # Set starting guesses - beta <- beta.start - eta <- solve_y_(X, beta) + beta <- beta_start + eta <- X %*% beta } else { - # Validity of input argument (eta.start) - if (length(eta.start) != nt) { + # Validity of input argument (eta_start) + if (length(eta_start) != nt) { stop( paste( - "Length of 'eta.start' has to be equal to the number of", + "Length of 'eta_start' has to be equal to the number of", "observations." ), call. = FALSE @@ -300,7 +318,7 @@ start_guesses_ <- function( # Set starting guesses beta <- numeric(p) - eta <- eta.start + eta <- eta_start } } else { # Compute starting guesses if not user specified @@ -317,3 +335,73 @@ start_guesses_ <- function( assign("beta", beta, envir = parent.frame()) assign("eta", eta, envir = parent.frame()) } + +# Generate auxiliary list of indexes for different sub panels ---- + +get_index_list_ <- function(k.vars, data) { + indexes <- seq.int(0L, nrow(data) - 1L) + lapply(k.vars, function(x, indexes, data) { + split(indexes, data[[x]]) + }, indexes = indexes, data = data) +} + +# Compute score matrix ---- + +get_score_matrix_ <- function(object) { + # Extract required quantities from result list + control <- object[["control"]] + data <- object[["data"]] + eta <- object[["eta"]] + wt <- object[["weights"]] + family <- object[["family"]] + + # Update weights and dependent variable + y <- data[[1L]] + mu <- family[["linkinv"]](eta) + mu.eta <- family[["mu.eta"]](eta) + w <- (wt * mu.eta^2) / family[["variance"]](mu) + nu <- (y - mu) / mu.eta + + # Center regressor matrix (if required) + if (control[["keep_mx"]]) { + MX <- object[["MX"]] + } else { + # Extract additional required quantities from result list + formula <- object[["formula"]] + k.vars <- names(object[["lvls_k"]]) + + # Generate auxiliary list of indexes to project out the fixed effects + k.list <- get_index_list_(k.vars, data) + + # Extract regressor matrix + X <- model.matrix(formula, data, rhs = 1L)[, -1L, drop = FALSE] + nms_sp <- attr(X, "dimnames")[[2L]] + attr(X, "dimnames") <- NULL + + # Center variables + MX <- center_variables_r_(X, w, k.list, control[["center_tol"]], 10000L) + colnames(MX) <- nms_sp + } + + # Return score matrix + MX * (nu * w) +} + +# Suitable name for a temporary variable ---- + +temp_var_ <- function(data) { + repeat { + tmp.var <- paste0(sample(letters, 5L, replace = TRUE), collapse = "") + if (!(tmp.var %in% colnames(data))) { + break + } + } + tmp.var +} + +# Gamma computation (APES) ---- + +gamma_ <- function(MX, H, J, PPsi, v, nt) { + inv_nt <- 1.0 / nt + (MX %*% solve(H * inv_nt, J) - PPsi) * v * inv_nt +} diff --git a/R/internals.R b/R/internals.R deleted file mode 100644 index 68279b5..0000000 --- a/R/internals.R +++ /dev/null @@ -1,325 +0,0 @@ -# Transform factor ---- - -check_factor_ <- function(x) { - if (is.factor(x)) { - droplevels(x) - } else { - factor(x) - } -} - -# Fitting algorithm (similar to lm.fit) ---- - -felm_fit_ <- function(y, X, wt, k.list, control) { - # Extract control arguments - center.tol <- control[["center.tol"]] - epsilon <- max(1.0e-07, .Machine[["double.eps"]]) - keep.mx <- control[["keep.mx"]] - - # Generate temporary variables - nt <- length(y) - MX <- X - - # Centering variables - MX <- center_variables_(MX, NA_real_, wt, k.list, center.tol, 10000L, FALSE) - - # Compute the OLS estimate - # beta <- as.vector(qr.solve(MX, y, epsilon)) - beta <- solve_beta_(MX, y, NA_real_, FALSE) - - # Generate result list - reslist <- list( - coefficients = beta - ) - - # Update result list - if (keep.mx) reslist[["MX"]] <- MX - - # Return result list - reslist -} - -# Fitting algorithm (similar to glm.fit) ---- - -feglm_fit_ <- function(beta, eta, y, X, wt, k.list, family, control) { - # Extract control arguments - center.tol <- control[["center.tol"]] - dev.tol <- control[["dev.tol"]] - epsilon <- max(min(1.0e-07, dev.tol / 1000.0), .Machine[["double.eps"]]) - iter.max <- control[["iter.max"]] - trace <- control[["trace"]] - keep.mx <- control[["keep.mx"]] - - # Compute initial quantities for the maximization routine - nt <- length(y) - mu <- family[["linkinv"]](eta) - dev <- sum(family[["dev.resids"]](y, mu, wt)) - null.dev <- sum(family[["dev.resids"]](y, mean(y), wt)) - - # Generate temporary variables - Mnu <- as.matrix(numeric(nt)) - MX <- X - - # Start maximization of the log-likelihood - conv <- FALSE - for (iter in seq.int(iter.max)) { - # Store \eta, \beta, and deviance of the previous iteration - eta.old <- eta - beta.old <- beta - dev.old <- dev - - # Compute weights and dependent variable - mu.eta <- family[["mu.eta"]](eta) - w <- (wt * mu.eta^2) / family[["variance"]](mu) - nu <- (y - mu) / mu.eta - - # Centering variables - Mnu <- center_variables_(Mnu, nu, w, k.list, center.tol, 10000L, TRUE) - MX <- center_variables_(MX, NA_real_, w, k.list, center.tol, 10000L, FALSE) - - # Compute update step and update eta - # beta.upd <- as.vector(qr.solve(MX * w.tilde, Mnu * w.tilde, epsilon)) - # eta.upd <- nu - as.vector(Mnu - MX %*% beta.upd) - beta.upd <- solve_beta_(MX, Mnu, w, TRUE) - eta.upd <- solve_eta_(MX, Mnu, nu, beta.upd) - - # Step-halving with three checks - # 1. finite deviance - # 2. valid \eta and \mu - # 3. improvement as in glm2 - rho <- 1.0 - - for (inner.iter in seq.int(50L)) { - # eta <- eta.old + rho * eta.upd - # beta <- beta.old + rho * beta.upd - eta <- update_beta_eta_(eta.old, eta.upd, rho) - beta <- update_beta_eta_(beta.old, beta.upd, rho) - mu <- family[["linkinv"]](eta) - dev <- sum(family[["dev.resids"]](y, mu, wt)) - dev.crit <- is.finite(dev) - val.crit <- family[["valideta"]](eta) && family[["validmu"]](mu) - imp.crit <- (dev - dev.old) / (0.1 + abs(dev)) <= -dev.tol - if (dev.crit && val.crit && imp.crit) break - rho <- rho * 0.5 - } - - # Check if step-halving failed (deviance and invalid \eta or \mu) - if (!dev.crit || !val.crit) { - stop("Inner loop failed; cannot correct step size.", call. = FALSE) - } - - # Stop if we do not improve - if (!imp.crit) { - eta <- eta.old - beta <- beta.old - dev <- dev.old - mu <- family[["linkinv"]](eta) - } - - # Progress information - if (trace) { - cat( - "Deviance=", format(dev, digits = 5L, nsmall = 2L), "Iterations -", - iter, "\n" - ) - cat("Estimates=", format(beta, digits = 3L, nsmall = 2L), "\n") - } - - # Check convergence - dev.crit <- abs(dev - dev.old) / (0.1 + abs(dev)) - if (trace) cat("Stopping criterion=", dev.crit, "\n") - if (dev.crit < dev.tol) { - if (trace) cat("Convergence\n") - conv <- TRUE - break - } - - # Update starting guesses for acceleration - Mnu <- Mnu - nu - } - - # Information if convergence failed - if (!conv && trace) cat("Algorithm did not converge.\n") - - # Update weights and dependent variable - mu.eta <- family[["mu.eta"]](eta) - w <- (wt * mu.eta^2) / family[["variance"]](mu) - - # Center variables - MX <- center_variables_(X, NA_real_, w, k.list, center.tol, 10000L, FALSE) - # Recompute Hessian - H <- crossprod_(MX, w, TRUE, TRUE) - - # Generate result list - reslist <- list( - coefficients = beta, - eta = eta, - weights = wt, - Hessian = H, - deviance = dev, - null.deviance = null.dev, - conv = conv, - iter = iter - ) - - # Update result list - if (keep.mx) reslist[["MX"]] <- MX - - # Return result list - reslist -} - -# Efficient offset algorithm to update the linear predictor ---- - -feglm_offset_ <- function(object, offset) { - # Check validity of 'object' - if (!inherits(object, "feglm")) { - stop("'feglm_offset_' called on a non-'feglm' object.") - } - - # Extract required quantities from result list - control <- object[["control"]] - data <- object[["data"]] - wt <- object[["weights"]] - family <- object[["family"]] - formula <- object[["formula"]] - lvls.k <- object[["lvls.k"]] - nt <- object[["nobs"]][["nobs"]] - k.vars <- names(lvls.k) - - # Extract dependent variable - y <- data[[1L]] - - # Extract control arguments - center.tol <- control[["center.tol"]] - dev.tol <- control[["dev.tol"]] - iter.max <- control[["iter.max"]] - - # Generate auxiliary list of indexes to project out the fixed effects - k.list <- get_index_list_(k.vars, data) - - # Compute starting guess for \eta - if (family[["family"]] == "binomial") { - eta <- rep(family[["linkfun"]](sum(wt * (y + 0.5) / 2.0) / sum(wt)), nt) - } else if (family[["family"]] %in% c("Gamma", "inverse.gaussian")) { - eta <- rep(family[["linkfun"]](sum(wt * y) / sum(wt)), nt) - } else { - eta <- rep(family[["linkfun"]](sum(wt * (y + 0.1)) / sum(wt)), nt) - } - - # Compute initial quantities for the maximization routine - mu <- family[["linkinv"]](eta) - dev <- sum(family[["dev.resids"]](y, mu, wt)) - Myadj <- as.matrix(numeric(nt)) - - # Start maximization of the log-likelihood - for (iter in seq.int(iter.max)) { - # Store \eta, \beta, and deviance of the previous iteration - eta.old <- eta - dev.old <- dev - - # Compute weights and dependent variable - mu.eta <- family[["mu.eta"]](eta) - w <- (wt * mu.eta^2) / family[["variance"]](mu) - yadj <- (y - mu) / mu.eta + eta - offset - - # Centering dependent variable and compute \eta update - Myadj <- center_variables_(Myadj, yadj, w, k.list, center.tol, 10000L, TRUE) - # eta.upd <- yadj - drop(Myadj) + offset - eta - eta.upd <- solve_eta2_(yadj, Myadj, offset, eta) - - # Step-halving with three checks - # 1. finite deviance - # 2. valid \eta and \mu - # 3. improvement as in glm2 - rho <- 1.0 - for (inner.iter in seq.int(50L)) { - # eta <- eta.old + rho * eta.upd - eta <- update_beta_eta_(eta.old, eta.upd, rho) - mu <- family[["linkinv"]](eta) - dev <- sum(family[["dev.resids"]](y, mu, wt)) - dev.crit <- is.finite(dev) - val.crit <- family[["valideta"]](eta) && family[["validmu"]](mu) - imp.crit <- (dev - dev.old) / (0.1 + abs(dev)) <= -dev.tol - if (dev.crit && val.crit && imp.crit) break - rho <- rho / 2.0 - } - - # Check if step-halving failed - if (!dev.crit || !val.crit) { - stop("Inner loop failed; cannot correct step size.", call. = FALSE) - } - - # Check termination condition - if (abs(dev - dev.old) / (0.1 + abs(dev)) < dev.tol) break - - # Update starting guesses for acceleration - Myadj <- Myadj - yadj - } - - # Return \eta - eta -} - -# Generate auxiliary list of indexes for different sub panels ---- - -get_index_list_ <- function(k.vars, data) { - indexes <- seq.int(0L, nrow(data) - 1L) - lapply(k.vars, function(x, indexes, data) { - split(indexes, data[[x]]) - }, indexes = indexes, data = data) -} - -# Compute score matrix ---- - -get_score_matrix_ <- function(object) { - # Extract required quantities from result list - control <- object[["control"]] - data <- object[["data"]] - eta <- object[["eta"]] - wt <- object[["weights"]] - family <- object[["family"]] - - # Update weights and dependent variable - y <- data[[1L]] - mu <- family[["linkinv"]](eta) - mu.eta <- family[["mu.eta"]](eta) - w <- (wt * mu.eta^2) / family[["variance"]](mu) - # nu <- (y - mu) / mu.eta - nu <- update_nu_(y, mu, mu.eta) - - # Center regressor matrix (if required) - if (control[["keep.mx"]]) { - MX <- object[["MX"]] - } else { - # Extract additional required quantities from result list - formula <- object[["formula"]] - k.vars <- names(object[["lvls.k"]]) - - # Generate auxiliary list of indexes to project out the fixed effects - k.list <- get_index_list_(k.vars, data) - - # Extract regressor matrix - X <- model.matrix(formula, data, rhs = 1L)[, -1L, drop = FALSE] - nms.sp <- attr(X, "dimnames")[[2L]] - attr(X, "dimnames") <- NULL - - # Center variables - MX <- center_variables_(X, NA_real_, w, k.list, control[["center.tol"]], 10000L, FALSE) - colnames(MX) <- nms.sp - } - - # Return score matrix - MX * (nu * w) -} - -# Returns suitable name for a temporary variable -temp_var_ <- function(data) { - repeat { - tmp.var <- paste0(sample(letters, 5L, replace = TRUE), collapse = "") - if (!(tmp.var %in% colnames(data))) { - break - } - } - tmp.var -} diff --git a/README.Rmd b/README.Rmd index 7b03922..15a7f69 100644 --- a/README.Rmd +++ b/README.Rmd @@ -121,27 +121,27 @@ to test with testthat. Median time for the different models in the book [An Advanced Guide to Trade Policy Analysis](https://www.wto.org/english/res_e/publications_e/advancedguide2016_e.htm). -|package | PPML| Trade Diversion| Endogeneity| Reverse Causality| Non-linear/Phasing Effects| Globalization| -|:------------|-------:|---------------:|-----------:|-----------------:|--------------------------:|-------------:| -|Alpaca | 261ms| 2s | 2s | 2s | 3s | 6s | -|Base R | 2m | 2m | 23m | 24m | 23m | 25m | -|**Capybara** | 364ms| 3s | 1s | 2s | 2s | 4s | -|Fixest | 69ms| 488ms| 125ms| 148ms| 251ms| 497ms| +| package | PPML | Trade Diversion | Endogeneity | Reverse Causality | Non-linear/Phasing Effects | Globalization | +|:------------|-------:|-----------------:|------------:|-----------------:|----------------------------:|--------------:| +| Alpaca | 0.4s | 2.6s | 1.6s | 2.0s | 3.1s | 5.3s | +| Base R | 120.0s | 2.0m | 1380.0s | 1440.0s | 1380.0s | 1500.0s | +| **Capybara**| 0.3s | 2.0s | 1.2s | 1.4s | 1.7s | 3.4s | +| Fixest | 0.1s | 0.5s | 0.1s | 0.2s | 0.3s | 0.5s | Memory allocation for the same models |package | PPML| Trade Diversion| Endogeneity| Reverse Causality| Non-linear/Phasing Effects| Globalization| |:------------|-------:|---------------:|-----------:|-----------------:|--------------------------:|-------------:| -|Alpaca | 306MB| 341MB| 306MB| 336MB| 395MB| 541MB| -|Base R | 3GB| 3GB| 12GB| 12GB| 12GB| 12GB| -|**Capybara** | 211MB| 235MB| 243MB| 250MB| 265MB| 302MB| -|Fixest | 44MB| 36MB| 27MB| 32MB| 41MB| 63MB| +|Alpaca | 307MB | 341MB | 306MB | 336MB | 395MB | 541MB | +|Base R | 3000MB | 3000MB | 12000MB | 12000GB | 12000GB | 12000MB | +|**Capybara** | 27MB | 32MB | 20MB | 23MB | 29MB | 43MB | +|Fixest | 44MB | 36MB | 27MB | 32MB | 41MB | 63MB | # Debugging *This debugging is about code quality, not about statistical quality.* *There is a full set of numerical tests for testthat to check the math.* -*In this section of the test, I can write pi = 3 and if there are no memory +*In this section of the test, I could write "pi = 3" and if there are no memory leaks, it will pass the test.* I run `r_valgrind "dev/test_get_alpha.r"` or the corresponding test from the diff --git a/README.md b/README.md index 7155b6a..64db857 100644 --- a/README.md +++ b/README.md @@ -117,27 +117,27 @@ Median time for the different models in the book [An Advanced Guide to Trade Policy Analysis](https://www.wto.org/english/res_e/publications_e/advancedguide2016_e.htm). -| package | PPML | Trade Diversion | Endogeneity | Reverse Causality | Non-linear/Phasing Effects | Globalization | -| :----------- | ------: | --------------: | ----------: | ----------------: | -------------------------: | ------------: | -| Alpaca | 346.4ms | 2.52s | 1.51s | 1.9s | 2.96s | 5.57s | -| Base R | 1.5m | 1.53m | 23.43m | 23.52m | 23.16m | 24.85m | -| **Capybara** | 440ms | 2.86s | 1.92s | 2.29s | 2.96s | 4.46s | -| Fixest | 64.9ms | 503ms | 106.14ms | 145.04ms | 243.61ms | 524.7ms | +| package | PPML | Trade Diversion | Endogeneity | Reverse Causality | Non-linear/Phasing Effects | Globalization | +| :----------- | -----: | --------------: | ----------: | ----------------: | -------------------------: | ------------: | +| Alpaca | 0.4s | 2.6s | 1.6s | 2.0s | 3.1s | 5.3s | +| Base R | 120.0s | 2.0m | 1380.0s | 1440.0s | 1380.0s | 1500.0s | +| **Capybara** | 0.3s | 2.0s | 1.2s | 1.4s | 1.7s | 3.4s | +| Fixest | 0.1s | 0.5s | 0.1s | 0.2s | 0.3s | 0.5s | Memory allocation for the same models | package | PPML | Trade Diversion | Endogeneity | Reverse Causality | Non-linear/Phasing Effects | Globalization | | :----------- | -----: | --------------: | ----------: | ----------------: | -------------------------: | ------------: | -| Alpaca | 306MB | 340.8MB | 306.4MB | 335.9MB | 394.6MB | 541.3MB | -| Base R | 2.7GB | 2.6GB | 11.9GB | 11.92GB | 11.95GB | 11.97GB | -| **Capybara** | 210MB | 235MB | 241MB | 249MB | 263MB | 299MB | -| Fixest | 44.4MB | 36.4MB | 27.9MB | 32.2MB | 40.9MB | 62.7MB | +| Alpaca | 307MB | 341MB | 306MB | 336MB | 395MB | 541MB | +| Base R | 3000MB | 3000MB | 12000MB | 12000GB | 12000GB | 12000MB | +| **Capybara** | 27MB | 32MB | 20MB | 23MB | 29MB | 43MB | +| Fixest | 44MB | 36MB | 27MB | 32MB | 41MB | 63MB | # Debugging *This debugging is about code quality, not about statistical quality.* *There is a full set of numerical tests for testthat to check the math.* -*In this section of the test, I can write pi = 3 and if there are no +*In this section of the test, I could write “pi = 3” and if there are no memory leaks, it will pass the test.* I run `r_valgrind "dev/test_get_alpha.r"` or the corresponding test from @@ -219,3 +219,10 @@ leaks. When you are ready testing, you need to remove `-UDEGUG` from `src/Makevars`. + +## Code of Conduct + +Please note that the capybara project is released with a [Contributor +Code of +Conduct](https://contributor-covenant.org/version/2/1/CODE_OF_CONDUCT.html). +By contributing to this project, you agree to abide by its terms. diff --git a/dev/07_helpers.cpp b/dev/07_helpers.cpp new file mode 100644 index 0000000..fa49b96 --- /dev/null +++ b/dev/07_helpers.cpp @@ -0,0 +1,19 @@ +#include "00_main.h" + +// Generate auxiliary list of indexes for different sub panels + +[[cpp11::register]] list get_index_list_(const strings &k_vars, + const data_frame &data) { + writable::integers indexes(data.nrow()); + std::iota(indexes.begin(), indexes.end(), 0); + + writable::list out; + + auto split = cpp11::package("base")["split"]; + + for (const auto &k_var : k_vars) { + out.push_back(split(indexes, data[k_var])); + } + + return out; +} diff --git a/dev/benchmarks_tests_agtpa.R b/dev/benchmarks_tests_agtpa.R index c1a1ce0..abf1a49 100644 --- a/dev/benchmarks_tests_agtpa.R +++ b/dev/benchmarks_tests_agtpa.R @@ -42,9 +42,6 @@ form2 <- trade ~ log_dist + cntg + lang + clny + d <- filter(ch1_application3, importer != exporter) bench_ppml <- mark( - # round(glm(form, family = stats::quasipoisson(link = "log"), data = d)$coefficients["rta"], 3), - round(capybara::fepoisson(form2, data = d)$coefficients["rta"], 3), - round(fixest::fepois(form2, data = d)$coefficients["rta"], 3), round(alpaca::feglm(form2, data = d, family = poisson())$coefficients["rta"], 3) ) @@ -63,9 +60,6 @@ form2 <- trade ~ log_dist + cntg + lang + clny + d <- ch1_application3 bench_trade_diversion <- mark( - # round(glm(form, family = stats::quasipoisson(link = "log"), data = d)$coefficients["rta"], 3), - round(capybara::fepoisson(form2, data = d)$coefficients["rta"], 3), - round(fixest::fepois(form2, data = d)$coefficients["rta"], 3), round(alpaca::feglm(form2, data = d, family = poisson())$coefficients["rta"], 3) ) @@ -81,9 +75,6 @@ form2 <- trade ~ rta | exp_year + imp_year + pair_id_2 d <- filter(ch1_application3, sum_trade > 0) bench_endogeneity <- mark( - # round(glm(form, family = stats::quasipoisson(link = "log"), data = d)$coefficients["rta"], 3), - round(capybara::fepoisson(form2, data = d)$coefficients["rta"], 3), - round(fixest::fepois(form2, data = d)$coefficients["rta"], 3), round(alpaca::feglm(form2, data = d, family = poisson())$coefficients["rta"], 3) ) @@ -99,9 +90,6 @@ form2 <- trade ~ rta + rta_lead4 | exp_year + imp_year + pair_id_2 d <- filter(ch1_application3, sum_trade > 0) bench_reverse_causality <- mark( - # round(glm(form, family = stats::quasipoisson(link = "log"), data = d)$coefficients["rta"], 3), - round(capybara::fepoisson(form2, data = d)$coefficients["rta"], 3), - round(fixest::fepois(form2, data = d)$coefficients["rta"], 3), round(alpaca::feglm(form2, data = d, family = poisson())$coefficients["rta"], 3) ) @@ -120,9 +108,6 @@ form2 <- trade ~ rta + rta_lag4 + rta_lag8 + rta_lag12 | d <- filter(ch1_application3, sum_trade > 0) bench_phasing <- mark( - # round(glm(form, family = stats::quasipoisson(link = "log"), data = d)$coefficients["rta"], 3), - round(capybara::fepoisson(form2, data = d)$coefficients["rta"], 3), - round(fixest::fepois(form2, data = d)$coefficients["rta"], 3), round(alpaca::feglm(form2, data = d, family = poisson())$coefficients["rta"], 3) ) @@ -145,9 +130,6 @@ form2 <- trade ~ rta + rta_lag4 + rta_lag8 + rta_lag12 + d <- filter(ch1_application3, sum_trade > 0) bench_globalization <- mark( - # round(glm(form, family = stats::quasipoisson(link = "log"), data = d)$coefficients["rta"], 3), - round(capybara::fepoisson(form2, data = d)$coefficients["rta"], 3), - round(fixest::fepois(form2, data = d)$coefficients["rta"], 3), round(alpaca::feglm(form2, data = d, family = poisson())$coefficients["rta"], 3) ) @@ -176,14 +158,15 @@ bench_globalization <- readRDS("dev/bench_globalization.rds") bench_ppml %>% # mutate(package = c("Base R", "**Capybara**", "Fixest", "Alpaca")) %>% - mutate(package = c("**Capybara**", "Fixest", "Alpaca")) %>% + mutate(package = c("Alpaca")) %>% + # mutate(package = c("**Capybara**", "Fixest", "Alpaca")) %>% mutate(model = "PPML") %>% select(model, package, median) %>% pivot_wider(names_from = model, values_from = median) %>% left_join( bench_trade_diversion %>% # mutate(package = c("Base R", "**Capybara**", "Fixest", "Alpaca")) %>% - mutate(package = c("**Capybara**", "Fixest", "Alpaca")) %>% + mutate(package = c("Alpaca")) %>% mutate(model = "Trade Diversion") %>% select(model, package, median) %>% pivot_wider(names_from = model, values_from = median) @@ -191,7 +174,7 @@ bench_ppml %>% left_join( bench_endogeneity %>% # mutate(package = c("Base R", "**Capybara**", "Fixest", "Alpaca")) %>% - mutate(package = c("**Capybara**", "Fixest", "Alpaca")) %>% + mutate(package = c("Alpaca")) %>% mutate(model = "Endogeneity") %>% select(model, package, median) %>% pivot_wider(names_from = model, values_from = median) @@ -199,7 +182,7 @@ bench_ppml %>% left_join( bench_reverse_causality %>% # mutate(package = c("Base R", "**Capybara**", "Fixest", "Alpaca")) %>% - mutate(package = c("**Capybara**", "Fixest", "Alpaca")) %>% + mutate(package = c("Alpaca")) %>% mutate(model = "Reverse Causality") %>% select(model, package, median) %>% pivot_wider(names_from = model, values_from = median) @@ -207,7 +190,7 @@ bench_ppml %>% left_join( bench_phasing %>% # mutate(package = c("Base R", "**Capybara**", "Fixest", "Alpaca")) %>% - mutate(package = c("**Capybara**", "Fixest", "Alpaca")) %>% + mutate(package = c("Alpaca")) %>% mutate(model = "Non-linear/Phasing Effects") %>% select(model, package, median) %>% pivot_wider(names_from = model, values_from = median) @@ -215,7 +198,7 @@ bench_ppml %>% left_join( bench_globalization %>% # mutate(package = c("Base R", "**Capybara**", "Fixest", "Alpaca")) %>% - mutate(package = c("**Capybara**", "Fixest", "Alpaca")) %>% + mutate(package = c("Alpaca")) %>% mutate(model = "Globalization") %>% select(model, package, median) %>% pivot_wider(names_from = model, values_from = median) @@ -225,14 +208,14 @@ bench_ppml %>% bench_ppml %>% # mutate(package = c("Base R", "**Capybara**", "Fixest", "Alpaca")) %>% - mutate(package = c("**Capybara**", "Fixest", "Alpaca")) %>% + mutate(package = c("Alpaca")) %>% mutate(model = "PPML") %>% select(model, package, mem_alloc) %>% pivot_wider(names_from = model, values_from = mem_alloc) %>% left_join( bench_trade_diversion %>% # mutate(package = c("Base R", "**Capybara**", "Fixest", "Alpaca")) %>% - mutate(package = c("**Capybara**", "Fixest", "Alpaca")) %>% + mutate(package = c("Alpaca")) %>% mutate(model = "Trade Diversion") %>% select(model, package, mem_alloc) %>% pivot_wider(names_from = model, values_from = mem_alloc) @@ -240,7 +223,7 @@ bench_ppml %>% left_join( bench_endogeneity %>% # mutate(package = c("Base R", "**Capybara**", "Fixest", "Alpaca")) %>% - mutate(package = c("**Capybara**", "Fixest", "Alpaca")) %>% + mutate(package = c("Alpaca")) %>% mutate(model = "Endogeneity") %>% select(model, package, mem_alloc) %>% pivot_wider(names_from = model, values_from = mem_alloc) @@ -248,7 +231,7 @@ bench_ppml %>% left_join( bench_reverse_causality %>% # mutate(package = c("Base R", "**Capybara**", "Fixest", "Alpaca")) %>% - mutate(package = c("**Capybara**", "Fixest", "Alpaca")) %>% + mutate(package = c("Alpaca")) %>% mutate(model = "Reverse Causality") %>% select(model, package, mem_alloc) %>% pivot_wider(names_from = model, values_from = mem_alloc) @@ -256,7 +239,7 @@ bench_ppml %>% left_join( bench_phasing %>% # mutate(package = c("Base R", "**Capybara**", "Fixest", "Alpaca")) %>% - mutate(package = c("**Capybara**", "Fixest", "Alpaca")) %>% + mutate(package = c("Alpaca")) %>% mutate(model = "Non-linear/Phasing Effects") %>% select(model, package, mem_alloc) %>% pivot_wider(names_from = model, values_from = mem_alloc) @@ -264,7 +247,7 @@ bench_ppml %>% left_join( bench_globalization %>% # mutate(package = c("Base R", "**Capybara**", "Fixest", "Alpaca")) %>% - mutate(package = c("**Capybara**", "Fixest", "Alpaca")) %>% + mutate(package = c("Alpaca")) %>% mutate(model = "Globalization") %>% select(model, package, mem_alloc) %>% pivot_wider(names_from = model, values_from = mem_alloc) diff --git a/dev/benchmarks_tests_agtpa_capybara_only copy.R b/dev/benchmarks_tests_agtpa_capybara_only copy.R deleted file mode 100644 index 1a546f3..0000000 --- a/dev/benchmarks_tests_agtpa_capybara_only copy.R +++ /dev/null @@ -1,180 +0,0 @@ -# this is not just about speed/memory, but also about obtaining the same -# slopes as in base R - -load_all() -library(dplyr) -library(tidyr) -library(janitor) -library(bench) - -rm(list = ls()) -gc() - -# data ---- - -ch1_application3 <- tradepolicy::agtpa_applications %>% - clean_names() %>% - filter(year %in% seq(1986, 2006, 4)) %>% - mutate( - exp_year = paste0(exporter, year), - imp_year = paste0(importer, year), - year = paste0("intl_border_", year), - log_trade = log(trade), - log_dist = log(dist), - intl_brdr = ifelse(exporter == importer, pair_id, "inter"), - intl_brdr_2 = ifelse(exporter == importer, 0, 1), - pair_id_2 = ifelse(exporter == importer, "0-intra", pair_id) - ) %>% - spread(year, intl_brdr_2, fill = 0) - -ch1_application3 <- ch1_application3 %>% - group_by(pair_id) %>% - mutate(sum_trade = sum(trade)) %>% - ungroup() - -# ppml ---- - -form <- trade ~ 0 + log_dist + cntg + lang + clny + - rta + exp_year + imp_year - -form2 <- trade ~ log_dist + cntg + lang + clny + - rta | exp_year + imp_year - -d <- filter(ch1_application3, importer != exporter) - -bench_ppml <- mark( - fepoisson(form2, data = d)$coefficients["rta"] -) - -formula = form2 -data = d -weights = NULL -beta.start = NULL -eta.start = NULL -control = NULL -family <- poisson() - -check_formula_(formula) -check_data_(data) -check_family_(family) -control <- check_control_(control) -formula <- update_formula_(formula) -lhs <- NA # just to avoid global variable warning -nobs.na <- NA -nobs.full <- NA -model_frame_(data, formula, weights) -check_response_(data, lhs, family) -k.vars <- attr(terms(formula, rhs = 2L), "term.labels") -k <- length(k.vars) -tmp.var <- temp_var_(data) -data <- drop_by_link_type_(data, lhs, family, tmp.var, k.vars, control) -data <- transform_fe_(data, formula, k.vars) -nt <- nrow(data) -nobs <- nobs_(nobs.full, nobs.na, nt) -nms.sp <- NA -p <- NA -model_response_(data, formula) - -p - -qr_(X, FALSE)$rank -out <- qr(X) -dim(out$qr) - -bench_ppml$median -bench_ppml$mem_alloc - -# rm(d) - -# trade diversion ---- - -form <- trade ~ 0 + log_dist + cntg + lang + clny + - rta + exp_year + imp_year + intl_brdr - -form2 <- trade ~ log_dist + cntg + lang + clny + - rta | exp_year + imp_year + intl_brdr - -d <- ch1_application3 - -bench_trade_diversion <- mark( - round(fepoisson(form2, data = d)$coefficients["rta"], 3) -) - -bench_trade_diversion$median -bench_trade_diversion$mem_alloc - -# rm(d) - -# endogeneity ---- - -# form <- trade ~ 0 + rta + exp_year + imp_year + pair_id_2 -# form2 <- trade ~ rta | exp_year + imp_year + pair_id_2 - -# d <- filter(ch1_application3, sum_trade > 0) - -# bench_endogeneity <- mark( -# round(fepoisson(form2, data = d)$coefficients["rta"], 3), -# iterations = 1000L -# ) - -bench_endogeneity - -# rm(d) - -# reverse causality ---- - -# form <- trade ~ 0 + rta + rta_lead4 + exp_year + imp_year + pair_id_2 -# form2 <- trade ~ rta + rta_lead4 | exp_year + imp_year + pair_id_2 - -# d <- filter(ch1_application3, sum_trade > 0) - -# bench_reverse_causality <- mark( -# round(fepoisson(form2, data = d)$coefficients["rta"], 3) -# ) - -# bench_reverse_causality - -# rm(d) - -# non-linear/phasing effects ---- - -# form <- trade ~ 0 + rta + rta_lag4 + rta_lag8 + rta_lag12 + -# exp_year + imp_year + pair_id_2 - -# form2 <- trade ~ rta + rta_lag4 + rta_lag8 + rta_lag12 | -# exp_year + imp_year + pair_id_2 - -# d <- filter(ch1_application3, sum_trade > 0) - -# bench_phasing <- mark( -# round(fepoisson(form2, data = d)$coefficients["rta"], 3) -# ) - -# bench_phasing - -# rm(d) - -# globalization ---- - -form <- trade ~ 0 + rta + rta_lag4 + rta_lag8 + rta_lag12 + - intl_border_1986 + intl_border_1990 + intl_border_1994 + - intl_border_1998 + intl_border_2002 + - exp_year + imp_year + pair_id_2 - -form2 <- trade ~ rta + rta_lag4 + rta_lag8 + rta_lag12 + - intl_border_1986 + intl_border_1990 + intl_border_1994 + - intl_border_1998 + intl_border_2002 | - exp_year + imp_year + pair_id_2 - -d <- filter(ch1_application3, sum_trade > 0) - -bench_globalization <- mark( - round(fepoisson(form2, data = d)$coefficients["rta"], 3) -) - -bench_globalization - -rm(d, form, ch1_application3) - -rm(list = ls()) -gc() diff --git a/dev/glmfit.r b/dev/glmfit.r new file mode 100644 index 0000000..5967848 --- /dev/null +++ b/dev/glmfit.r @@ -0,0 +1,129 @@ +# Fitting algorithm (similar to glm.fit) ---- + +feglm_fit_ <- function(beta, eta, y, X, wt, k.list, family, control) { + # Extract control arguments + center.tol <- control[["center.tol"]] + dev.tol <- control[["dev.tol"]] + epsilon <- max(min(1.0e-07, dev.tol / 1000.0), .Machine[["double.eps"]]) + iter.max <- control[["iter.max"]] + trace <- control[["trace"]] + keep.mx <- control[["keep.mx"]] + + # Compute initial quantities for the maximization routine + nt <- length(y) + mu <- family[["linkinv"]](eta) + dev <- sum(family[["dev.resids"]](y, mu, wt)) + null.dev <- sum(family[["dev.resids"]](y, mean(y), wt)) + + # Generate temporary variables + Mnu <- as.matrix(numeric(nt)) + MX <- X + + # Start maximization of the log-likelihood + conv <- FALSE + for (iter in seq.int(iter.max)) { + # Store \eta, \beta, and deviance of the previous iteration + eta.old <- eta + beta.old <- beta + dev.old <- dev + + # Compute weights and dependent variable + mu.eta <- family[["mu.eta"]](eta) + w <- (wt * mu.eta^2) / family[["variance"]](mu) + nu <- (y - mu) / mu.eta + + # Centering variables + Mnu <- center_variables_(Mnu, nu, w, k.list, center.tol, 10000L, TRUE) + MX <- center_variables_(MX, NA_real_, w, k.list, center.tol, 10000L, FALSE) + + # Compute update step and update eta + # beta.upd <- as.vector(qr.solve(MX * w.tilde, Mnu * w.tilde, epsilon)) + # eta.upd <- nu - as.vector(Mnu - MX %*% beta.upd) + beta.upd <- solve_beta_(MX, Mnu, w, TRUE) + eta.upd <- solve_eta_(MX, Mnu, nu, beta.upd) + + # Step-halving with three checks + # 1. finite deviance + # 2. valid \eta and \mu + # 3. improvement as in glm2 + rho <- 1.0 + + for (inner.iter in seq.int(50L)) { + # eta <- eta.old + rho * eta.upd + # beta <- beta.old + rho * beta.upd + eta <- update_beta_eta_(eta.old, eta.upd, rho) + beta <- update_beta_eta_(beta.old, beta.upd, rho) + mu <- family[["linkinv"]](eta) + dev <- sum(family[["dev.resids"]](y, mu, wt)) + dev.crit <- is.finite(dev) + val.crit <- family[["valideta"]](eta) && family[["validmu"]](mu) + imp.crit <- (dev - dev.old) / (0.1 + abs(dev)) <= -dev.tol + if (dev.crit && val.crit && imp.crit) break + rho <- rho * 0.5 + } + + # Check if step-halving failed (deviance and invalid \eta or \mu) + if (!dev.crit || !val.crit) { + stop("Inner loop failed; cannot correct step size.", call. = FALSE) + } + + # Stop if we do not improve + if (!imp.crit) { + eta <- eta.old + beta <- beta.old + dev <- dev.old + mu <- family[["linkinv"]](eta) + } + + # Progress information + if (trace) { + cat( + "Deviance=", format(dev, digits = 5L, nsmall = 2L), "Iterations -", + iter, "\n" + ) + cat("Estimates=", format(beta, digits = 3L, nsmall = 2L), "\n") + } + + # Check convergence + dev.crit <- abs(dev - dev.old) / (0.1 + abs(dev)) + if (trace) cat("Stopping criterion=", dev.crit, "\n") + if (dev.crit < dev.tol) { + if (trace) cat("Convergence\n") + conv <- TRUE + break + } + + # Update starting guesses for acceleration + Mnu <- Mnu - nu + } + + # Information if convergence failed + if (!conv && trace) cat("Algorithm did not converge.\n") + + # Update weights and dependent variable + mu.eta <- family[["mu.eta"]](eta) + w <- (wt * mu.eta^2) / family[["variance"]](mu) + + # Center variables + MX <- center_variables_(X, NA_real_, w, k.list, center.tol, 10000L, FALSE) + # Recompute Hessian + H <- crossprod_(MX, w, TRUE, TRUE) + + # Generate result list + reslist <- list( + coefficients = beta, + eta = eta, + weights = wt, + Hessian = H, + deviance = dev, + null.deviance = null.dev, + conv = conv, + iter = iter + ) + + # Update result list + if (keep.mx) reslist[["MX"]] <- MX + + # Return result list + reslist +} diff --git a/dev/test-helpers.R b/dev/test-helpers.R new file mode 100644 index 0000000..04729f8 --- /dev/null +++ b/dev/test-helpers.R @@ -0,0 +1,18 @@ +test_that("multiplication works", { + # get_index_list_r_ <- function(k.vars, data) { + # indexes <- seq.int(0L, nrow(data) - 1L) + # lapply(k.vars, function(x, indexes, data) { + # split(indexes, data[[x]]) + # }, indexes = indexes, data = data) + # } + + # expect_equal( + # get_index_list_(names(mtcars), mtcars), + # get_index_list_r_(names(mtcars), mtcars) + # ) + + # expect_equal( + # get_index_list_(names(iris), iris), + # get_index_list_r_(names(iris), iris) + # ) +}) diff --git a/dev/test-offset.R b/dev/test-offset.R new file mode 100644 index 0000000..e833619 --- /dev/null +++ b/dev/test-offset.R @@ -0,0 +1,18 @@ +test_that("offset works", { + m1 <- feglm(mpg ~ wt | cyl, data = mtcars, family = poisson()) + y <- predict(m1, type = "response") + o1 <- feglm_offset_(m1, y) + + # m2 <- alpaca::feglm(mpg ~ wt | cyl, data = mtcars, family = poisson()) + # o2 <- drop(alpaca:::feglmOffset(m2, y) + # datapasta::vector_paste(round(o2, 4)) + o2 <- c( + 3.018703, 3.011154, 3.056387, 3.001613, 2.979713, 2.995091, 2.976723, + 3.026537, 3.027809, 2.995612, 2.995612, 2.999650, 3.006936, 3.005836, + 2.977558, 2.974679, 2.975975, 3.094682, 3.062526, 3.053450, 3.029361, + 2.956144, 2.958109, 2.949010, 2.948902, 3.049442, 3.041447, 3.066858, + 2.964431, 2.992499, 2.955002, 3.018302 + ) + + expect_equal(round(o1, 4), round(o2, 4)) +}) diff --git a/docs/404.html b/docs/404.html index 6b35323..a945fa2 100644 --- a/docs/404.html +++ b/docs/404.html @@ -39,7 +39,7 @@ capybara - 0.5.2 + 0.6.0 diff --git a/docs/CODE_OF_CONDUCT.html b/docs/CODE_OF_CONDUCT.html index dab6e73..4590326 100644 --- a/docs/CODE_OF_CONDUCT.html +++ b/docs/CODE_OF_CONDUCT.html @@ -17,7 +17,7 @@ capybara - 0.5.2 + 0.6.0 diff --git a/docs/CONTRIBUTING.html b/docs/CONTRIBUTING.html index a8e0609..6184ced 100644 --- a/docs/CONTRIBUTING.html +++ b/docs/CONTRIBUTING.html @@ -17,7 +17,7 @@ capybara - 0.5.2 + 0.6.0 diff --git a/docs/LICENSE.html b/docs/LICENSE.html index e2305ab..4f37ea2 100644 --- a/docs/LICENSE.html +++ b/docs/LICENSE.html @@ -17,7 +17,7 @@ capybara - 0.5.2 + 0.6.0 diff --git a/docs/articles/index.html b/docs/articles/index.html index 9eabf7e..bc96bc8 100644 --- a/docs/articles/index.html +++ b/docs/articles/index.html @@ -17,7 +17,7 @@ capybara - 0.5.2 + 0.6.0 diff --git a/docs/articles/intro.html b/docs/articles/intro.html index 9ef0b0b..0496226 100644 --- a/docs/articles/intro.html +++ b/docs/articles/intro.html @@ -40,7 +40,7 @@ capybara - 0.5.2 + 0.6.0 diff --git a/docs/authors.html b/docs/authors.html index d61c09d..a177fc8 100644 --- a/docs/authors.html +++ b/docs/authors.html @@ -17,7 +17,7 @@ capybara - 0.5.2 + 0.6.0 @@ -74,14 +74,14 @@

Citation

Vargas Sepulveda M (2024). capybara: Fast and Memory Efficient Fitting of Linear Models With High-Dimensional Fixed Effects. -R package version 0.5.2, https://github.com/pachadotdev/capybara, https://pacha.dev/capybara/. +R package version 0.6.0, https://github.com/pachadotdev/capybara, https://pacha.dev/capybara/.

@Manual{,
   title = {capybara: Fast and Memory Efficient Fitting of Linear Models With High-Dimensional
 Fixed Effects},
   author = {Mauricio {Vargas Sepulveda}},
   year = {2024},
-  note = {R package version 0.5.2, https://github.com/pachadotdev/capybara},
+  note = {R package version 0.6.0, https://github.com/pachadotdev/capybara},
   url = {https://pacha.dev/capybara/},
 }
diff --git a/docs/index.html b/docs/index.html index dbb9e93..4b687da 100644 --- a/docs/index.html +++ b/docs/index.html @@ -46,7 +46,7 @@ capybara - 0.5.2 + 0.6.0 @@ -140,13 +140,13 @@

BenchmarksAn Advanced Guide to Trade Policy Analysis.

------++++++ @@ -160,39 +160,39 @@

Benchmarks

- - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + +
packageAlpaca346.4ms2.52s1.51s1.9s2.96s5.57s0.4s2.6s1.6s2.0s3.1s5.3s
Base R1.5m1.53m23.43m23.52m23.16m24.85m120.0s2.0m1380.0s1440.0s1380.0s1500.0s
Capybara440ms2.86s1.92s2.29s2.96s4.46s0.3s2.0s1.2s1.4s1.7s3.4s
Fixest64.9ms503ms106.14ms145.04ms243.61ms524.7ms0.1s0.5s0.1s0.2s0.3s0.5s
@@ -219,39 +219,39 @@

Benchmarks Alpaca +307MB +341MB 306MB -340.8MB -306.4MB -335.9MB -394.6MB -541.3MB +336MB +395MB +541MB Base R -2.7GB -2.6GB -11.9GB -11.92GB -11.95GB -11.97GB +3000MB +3000MB +12000MB +12000GB +12000GB +12000MB Capybara -210MB -235MB -241MB -249MB -263MB -299MB +27MB +32MB +20MB +23MB +29MB +43MB Fixest -44.4MB -36.4MB -27.9MB -32.2MB -40.9MB -62.7MB +44MB +36MB +27MB +32MB +41MB +63MB @@ -260,7 +260,7 @@

Benchmarks

Debugging

-

This debugging is about code quality, not about statistical quality. There is a full set of numerical tests for testthat to check the math. In this section of the test, I can write pi = 3 and if there are no memory leaks, it will pass the test.

+

This debugging is about code quality, not about statistical quality. There is a full set of numerical tests for testthat to check the math. In this section of the test, I could write “pi = 3” and if there are no memory leaks, it will pass the test.

I run r_valgrind "dev/test_get_alpha.r" or the corresponding test from the project’s root in a new terminal (bash).

This works because I previously defined this in .bashrc, to make it work you need to run source ~/.bashrc or reboot:

function r_debug_symbols () {
@@ -328,6 +328,11 @@ 

Debugging +

Code of Conduct +

+

Please note that the capybara project is released with a Contributor Code of Conduct. By contributing to this project, you agree to abide by its terms.

+

diff --git a/docs/news/index.html b/docs/news/index.html index b0accd0..1330cb0 100644 --- a/docs/news/index.html +++ b/docs/news/index.html @@ -17,7 +17,7 @@ capybara - 0.5.2 + 0.6.0 @@ -57,6 +57,11 @@

Changelog

Source: NEWS.md +
+ +
  • Moves all the heavy computation to C++ using Armadillo and it exports the results to R. Previously, there were multiple data copies between R and C++ that added overhead to the computations.
  • +
  • The previous versions returned MX by default, now it has to be specified.
  • +
  • Uses an O(n log(n)) algorithm to compute the Kendall correlation for the pseudo-R2 in the Poisson model.
diff --git a/docs/pkgdown.yml b/docs/pkgdown.yml index 82a9933..1065a4d 100644 --- a/docs/pkgdown.yml +++ b/docs/pkgdown.yml @@ -3,5 +3,5 @@ pkgdown: 2.0.7 pkgdown_sha: ~ articles: intro: intro.html -last_built: 2024-07-16T00:21Z +last_built: 2024-07-23T00:52Z diff --git a/docs/reference/apes.html b/docs/reference/apes.html index bec9e49..194abc6 100644 --- a/docs/reference/apes.html +++ b/docs/reference/apes.html @@ -26,7 +26,7 @@ capybara - 0.5.2 + 0.6.0 @@ -82,10 +82,10 @@

Compute average partial effects after fitting binary choice models
apes(
   object = NULL,
-  n.pop = NULL,
-  panel.structure = c("classic", "network"),
-  sampling.fe = c("independence", "unrestricted"),
-  weak.exo = FALSE
+  n_pop = NULL,
+  panel_structure = c("classic", "network"),
+  sampling_fe = c("independence", "unrestricted"),
+  weak_exo = FALSE
 )
@@ -96,7 +96,7 @@

Arguments

currently restricted to binomial.

-
n.pop
+
n_pop

unsigned integer indicating a finite population correction for the estimation of the covariance matrix of the average partial effects proposed by Cruz-Gonzalez, Fernández-Val, and Weidner (2017). The correction @@ -107,7 +107,7 @@

Arguments

a factor of zero and a covariance obtained by the delta method.

-
panel.structure
+
panel_structure

a string equal to "classic" or "network" which determines the structure of the panel used. "classic" denotes panel structures where for example the same cross-sectional units are @@ -116,7 +116,7 @@

Arguments

observed for several time periods. Default is "classic".

-
sampling.fe
+
sampling_fe

a string equal to "independence" or "unrestricted" which imposes sampling assumptions about the unobserved effects. "independence" imposes that all unobserved @@ -125,7 +125,7 @@

Arguments

population correction. Default is "independence".

-
weak.exo
+
weak_exo

logical indicating if some of the regressors are assumed to be weakly exogenous (e.g. predetermined). If object is of class "bias_corr", the option will be automatically set to TRUE if @@ -178,7 +178,7 @@

Examples

summary(mod_ape) #> Estimates: #> Estimate Std. Error z value Pr(>|z|) -#> lang 0.05594 0.01512 3.699 0.000217 *** +#> lang 0.05594 0.01513 3.698 0.000218 *** #> --- #> Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 @@ -186,7 +186,7 @@

Examples

mod_bc <- bias_corr(mod) summary(mod_bc) #> Formula: trade ~ lang | year -#> <environment: 0x6194fd0f13a0> +#> <environment: 0x579102d4cff0> #> #> Family: Binomial #> @@ -194,7 +194,7 @@

Examples

#> #> | | Estimate | Std. Error | z value | Pr(>|z|) | #> |------|----------|------------|---------|------------| -#> | lang | 0.2393 | 0.0634 | 3.7724 | 0.0002 *** | +#> | lang | 0.2394 | 0.0634 | 3.7740 | 0.0002 *** | #> #> Significance codes: *** 99.9%; ** 99%; * 95%; . 90% #> @@ -207,7 +207,7 @@

Examples

summary(mod_ape_bc) #> Estimates: #> Estimate Std. Error z value Pr(>|z|) -#> lang 0.05593 0.01512 3.698 0.000217 *** +#> lang 0.05596 0.01513 3.699 0.000216 *** #> --- #> Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
diff --git a/docs/reference/bias_corr.html b/docs/reference/bias_corr.html index fba8378..4dc5d37 100644 --- a/docs/reference/bias_corr.html +++ b/docs/reference/bias_corr.html @@ -24,7 +24,7 @@ capybara - 0.5.2 + 0.6.0 @@ -76,7 +76,7 @@

Asymptotic bias correction after fitting binary choice models with a
-
bias_corr(object = NULL, L = 0L, panel.structure = c("classic", "network"))
+
bias_corr(object = NULL, L = 0L, panel_structure = c("classic", "network"))
@@ -95,7 +95,7 @@

Arguments

be partialed out is important for bandwidths larger than zero.

-
panel.structure
+
panel_structure

a string equal to "classic" or "network" which determines the structure of the panel used. "classic" denotes panel structures where for example the same cross-sectional units are @@ -146,7 +146,7 @@

Examples

mod_bc <- bias_corr(mod) summary(mod_bc) #> Formula: trade ~ lang | year -#> <environment: 0x6194fd0d1c00> +#> <environment: 0x579100916318> #> #> Family: Binomial #> @@ -154,7 +154,7 @@

Examples

#> #> | | Estimate | Std. Error | z value | Pr(>|z|) | #> |------|----------|------------|---------|------------| -#> | lang | 0.2393 | 0.0634 | 3.7724 | 0.0002 *** | +#> | lang | 0.2394 | 0.0634 | 3.7740 | 0.0002 *** | #> #> Significance codes: *** 99.9%; ** 99%; * 95%; . 90% #> diff --git a/docs/reference/capybara-package.html b/docs/reference/capybara-package.html index 471542f..280f8cc 100644 --- a/docs/reference/capybara-package.html +++ b/docs/reference/capybara-package.html @@ -26,7 +26,7 @@ capybara - 0.5.2 + 0.6.0 diff --git a/docs/reference/feglm.html b/docs/reference/feglm.html index c395427..09beb95 100644 --- a/docs/reference/feglm.html +++ b/docs/reference/feglm.html @@ -22,7 +22,7 @@ capybara - 0.5.2 + 0.6.0 @@ -78,8 +78,8 @@

GLM fitting with high-dimensional k-way fixed effects

data = NULL, family = gaussian(), weights = NULL, - beta.start = NULL, - eta.start = NULL, + beta_start = NULL, + eta_start = NULL, control = NULL ) @@ -111,13 +111,13 @@

Arguments

variable in data.

-
beta.start
+
beta_start

an optional vector of starting values for the structural parameters in the linear predictor. Default is \(\boldsymbol{\beta} = \mathbf{0}\).

-
eta.start
+
eta_start

an optional vector of starting values for the linear predictor.

@@ -162,7 +162,7 @@

Examples

summary(mod) #> Formula: trade ~ log_dist + lang + cntg + clny | exp_year + imp_year -#> <environment: 0x6194fe4f6ed0> +#> <environment: 0x5791016e2240> #> #> Family: Poisson #> @@ -192,7 +192,7 @@

Examples

summary(mod, type = "clustered") #> Formula: trade ~ log_dist + lang + cntg + clny | exp_year + imp_year | #> pair -#> <environment: 0x6194fe4f6ed0> +#> <environment: 0x5791016e2240> #> #> Family: Poisson #> diff --git a/docs/reference/feglm_control.html b/docs/reference/feglm_control.html index 7ee1cec..8f375a0 100644 --- a/docs/reference/feglm_control.html +++ b/docs/reference/feglm_control.html @@ -18,7 +18,7 @@ capybara - 0.5.2 + 0.6.0 @@ -66,33 +66,33 @@

Set feglm Control Parameters

feglm_control(
-  dev.tol = 1e-08,
-  center.tol = 1e-08,
-  iter.max = 25L,
+  dev_tol = 1e-08,
+  center_tol = 1e-08,
+  iter_max = 25L,
   limit = 10L,
   trace = FALSE,
-  drop.pc = TRUE,
-  keep.mx = TRUE
+  drop_pc = TRUE,
+  keep_mx = FALSE
 )

Arguments

-
dev.tol
+
dev_tol

tolerance level for the first stopping condition of the maximization routine. The stopping condition is based on the relative change of the deviance in iteration \(r\) and can be expressed as follows: \(|dev_{r} - dev_{r - 1}| / (0.1 + |dev_{r}|) < tol\). The default is 1.0e-08.

-
center.tol
+
center_tol

tolerance level for the stopping condition of the centering algorithm. The stopping condition is based on the relative change of the centered variable similar to the 'lfe' package. The default is 1.0e-08.

-
iter.max
+
iter_max

unsigned integer indicating the maximum number of iterations in the maximization routine. The default is 25L.

@@ -107,7 +107,7 @@

Arguments

iteration. Default is FALSE.

-
drop.pc
+
drop_pc

logical indicating to drop observations that are perfectly classified/separated and hence do not contribute to the log-likelihood. This option is useful to reduce the computational costs of the maximization @@ -116,7 +116,7 @@

Arguments

The default is TRUE.

-
keep.mx
+
keep_mx

logical indicating if the centered regressor matrix should be stored. The centered regressor matrix is required for some covariance estimators, bias corrections, and average partial effects. This option saves diff --git a/docs/reference/felm.html b/docs/reference/felm.html index 53de5a3..82d4ea1 100644 --- a/docs/reference/felm.html +++ b/docs/reference/felm.html @@ -18,7 +18,7 @@ capybara - 0.5.2 + 0.6.0

@@ -116,7 +116,7 @@

Examples

summary(mod) #> Formula: log(trade) ~ log_dist + lang + cntg + clny | exp_year + imp_year -#> <environment: 0x6194fe51d928> +#> <environment: 0x5791041ea358> #> #> Estimates: #> @@ -130,7 +130,7 @@

Examples

#> Significance codes: *** 99.9%; ** 99%; * 95%; . 90% #> #> R-squared : 0.2761 -#> Adj. R-squared: 0.2541 +#> Adj. R-squared: 0.276 #> #> Number of observations: Full 28152; Missing 0; Perfect classification 0 diff --git a/docs/reference/fenegbin.html b/docs/reference/fenegbin.html index 5170cfd..be195d6 100644 --- a/docs/reference/fenegbin.html +++ b/docs/reference/fenegbin.html @@ -19,7 +19,7 @@ capybara - 0.5.2 + 0.6.0 @@ -70,9 +70,9 @@

Negative Binomial model fitting with high-dimensional k-way fixed formula = NULL, data = NULL, weights = NULL, - beta.start = NULL, - eta.start = NULL, - init.theta = NULL, + beta_start = NULL, + eta_start = NULL, + init_theta = NULL, link = c("log", "identity", "sqrt"), control = NULL ) @@ -98,18 +98,18 @@

Arguments

variable in data.

-
beta.start
+
beta_start

an optional vector of starting values for the structural parameters in the linear predictor. Default is \(\boldsymbol{\beta} = \mathbf{0}\).

-
eta.start
+
eta_start

an optional vector of starting values for the linear predictor.

-
init.theta
+
init_theta

an optional initial value for the theta parameter (see glm.nb).

@@ -141,7 +141,7 @@

Examples

summary(mod) #> Formula: trade ~ log_dist + lang + cntg + clny | exp_year + imp_year -#> <environment: 0x6194fec80d60> +#> <environment: 0x57910411c3a0> #> #> Family: Negative Binomial(1.1839) #> diff --git a/docs/reference/fepoisson.html b/docs/reference/fepoisson.html index 13dc8c5..89fea9f 100644 --- a/docs/reference/fepoisson.html +++ b/docs/reference/fepoisson.html @@ -18,7 +18,7 @@ capybara - 0.5.2 + 0.6.0 @@ -69,8 +69,8 @@

Poisson model fitting high-dimensional with k-way fixed effects

formula = NULL, data = NULL, weights = NULL, - beta.start = NULL, - eta.start = NULL, + beta_start = NULL, + eta_start = NULL, control = NULL ) @@ -95,13 +95,13 @@

Arguments

variable in data.

-
beta.start
+
beta_start

an optional vector of starting values for the structural parameters in the linear predictor. Default is \(\boldsymbol{\beta} = \mathbf{0}\).

-
eta.start
+
eta_start

an optional vector of starting values for the linear predictor.

@@ -128,7 +128,7 @@

Examples

summary(mod) #> Formula: trade ~ log_dist + lang + cntg + clny | exp_year + imp_year -#> <environment: 0x6194fe9e6d30> +#> <environment: 0x579101725c50> #> #> Family: Poisson #> diff --git a/docs/reference/fixed_effects.html b/docs/reference/fixed_effects.html index 0d3223f..91f2078 100644 --- a/docs/reference/fixed_effects.html +++ b/docs/reference/fixed_effects.html @@ -20,7 +20,7 @@ capybara - 0.5.2 + 0.6.0 @@ -69,7 +69,7 @@

Recover the estimates of the fixed effects after fitting (G)LMs

-
fixed_effects(object = NULL, alpha.tol = 1e-08)
+
fixed_effects(object = NULL, alpha_tol = 1e-08)
@@ -78,7 +78,7 @@

Arguments

an object of class "feglm".

-
alpha.tol
+
alpha_tol

tolerance level for the stopping condition. The algorithm is stopped at iteration \(i\) if \(||\boldsymbol{\alpha}_{i} - \boldsymbol{\alpha}_{i - 1}||_{2} < tol ||\boldsymbol{\alpha}_{i - 1}|| diff --git a/docs/reference/index.html b/docs/reference/index.html index 2c38e11..80d9012 100644 --- a/docs/reference/index.html +++ b/docs/reference/index.html @@ -17,7 +17,7 @@ capybara - 0.5.2 + 0.6.0

diff --git a/docs/reference/kendall_cor.html b/docs/reference/kendall_cor.html index 2b5cabd..4220891 100644 --- a/docs/reference/kendall_cor.html +++ b/docs/reference/kendall_cor.html @@ -28,7 +28,7 @@ capybara - 0.5.2 + 0.6.0 diff --git a/docs/reference/kendall_cor_test.html b/docs/reference/kendall_cor_test.html index f66fc93..930dc2c 100644 --- a/docs/reference/kendall_cor_test.html +++ b/docs/reference/kendall_cor_test.html @@ -19,7 +19,7 @@ capybara - 0.5.2 + 0.6.0 diff --git a/docs/reference/pipe.html b/docs/reference/pipe.html index 5a26aaa..3dba9cf 100644 --- a/docs/reference/pipe.html +++ b/docs/reference/pipe.html @@ -17,7 +17,7 @@ capybara - 0.5.2 + 0.6.0 diff --git a/docs/reference/reexports.html b/docs/reference/reexports.html index 03ef988..a4b7b03 100644 --- a/docs/reference/reexports.html +++ b/docs/reference/reexports.html @@ -24,7 +24,7 @@ capybara - 0.5.2 + 0.6.0 diff --git a/docs/reference/trade_panel.html b/docs/reference/trade_panel.html index bbde894..2415b96 100644 --- a/docs/reference/trade_panel.html +++ b/docs/reference/trade_panel.html @@ -17,7 +17,7 @@ capybara - 0.5.2 + 0.6.0 diff --git a/docs/reference/vcov.feglm.html b/docs/reference/vcov.feglm.html index 3907120..3782ca5 100644 --- a/docs/reference/vcov.feglm.html +++ b/docs/reference/vcov.feglm.html @@ -1,7 +1,7 @@ Covariance matrix for GLMs — vcov.feglm • capybara @@ -19,7 +19,7 @@ capybara - 0.5.2 + 0.6.0 @@ -63,7 +63,7 @@

Covariance matrix for GLMs

Covariance matrix for the estimator of the structural parameters from objects returned by feglm. The covariance is computed -from the Hessian, the scores, or a combination of both after convergence.

+from the hessian, the scores, or a combination of both after convergence.

@@ -83,7 +83,7 @@

Arguments

type

the type of covariance estimate required. "hessian" refers -to the inverse of the negative expected Hessian after convergence and is the +to the inverse of the negative expected hessian after convergence and is the default option. "outer.product" is the outer-product-of-the-gradient estimator. "sandwich" is the sandwich estimator (sometimes also referred as robust estimator), and "clustered" computes a clustered @@ -121,11 +121,11 @@

Examples

) round(vcov(mod, type = "clustered"), 5) -#> [,1] [,2] [,3] [,4] -#> [1,] 0.00068 0.00005 0.00106 -0.00063 -#> [2,] 0.00005 0.00388 -0.00118 -0.00193 -#> [3,] 0.00106 -0.00118 0.00452 -0.00013 -#> [4,] -0.00063 -0.00193 -0.00013 0.00854 +#> log_dist lang cntg clny +#> log_dist 0.00068 0.00005 0.00106 -0.00063 +#> lang 0.00005 0.00388 -0.00118 -0.00193 +#> cntg 0.00106 -0.00118 0.00452 -0.00013 +#> clny -0.00063 -0.00193 -0.00013 0.00854
diff --git a/docs/reference/vcov.felm.html b/docs/reference/vcov.felm.html index fb77315..ba886b4 100644 --- a/docs/reference/vcov.felm.html +++ b/docs/reference/vcov.felm.html @@ -1,7 +1,7 @@ Covariance matrix for LMs — vcov.felm • capybara @@ -19,7 +19,7 @@ capybara - 0.5.2 + 0.6.0 @@ -62,8 +62,8 @@

Covariance matrix for LMs

Covariance matrix for the estimator of the structural parameters -from objects returned by feglm. The covariance is computed -from the Hessian, the scores, or a combination of both after convergence.

+from objects returned by felm. The covariance is computed +from the hessian, the scores, or a combination of both after convergence.

@@ -78,12 +78,12 @@

Covariance matrix for LMs

Arguments

object
-

an object of class "feglm".

+

an object of class "felm".

type

the type of covariance estimate required. "hessian" refers -to the inverse of the negative expected Hessian after convergence and is the +to the inverse of the negative expected hessian after convergence and is the default option. "outer.product" is the outer-product-of-the-gradient estimator. "sandwich" is the sandwich estimator (sometimes also referred as robust estimator), and "clustered" computes a clustered @@ -121,11 +121,11 @@

Examples

) round(vcov(mod, type = "clustered"), 5) -#> [,1] [,2] [,3] [,4] -#> [1,] 0.00068 0.00005 0.00106 -0.00063 -#> [2,] 0.00005 0.00388 -0.00118 -0.00193 -#> [3,] 0.00106 -0.00118 0.00452 -0.00013 -#> [4,] -0.00063 -0.00193 -0.00013 0.00854 +#> log_dist lang cntg clny +#> log_dist 0.00068 0.00005 0.00106 -0.00063 +#> lang 0.00005 0.00388 -0.00118 -0.00193 +#> cntg 0.00106 -0.00118 0.00452 -0.00013 +#> clny -0.00063 -0.00193 -0.00013 0.00854
diff --git a/man/apes.Rd b/man/apes.Rd index 2e2cd7d..ad61f9a 100644 --- a/man/apes.Rd +++ b/man/apes.Rd @@ -7,40 +7,40 @@ with a 1,2,3-way error component} \usage{ apes( object = NULL, - n.pop = NULL, - panel.structure = c("classic", "network"), - sampling.fe = c("independence", "unrestricted"), - weak.exo = FALSE + n_pop = NULL, + panel_structure = c("classic", "network"), + sampling_fe = c("independence", "unrestricted"), + weak_exo = FALSE ) } \arguments{ \item{object}{an object of class \code{"bias_corr"} or \code{"feglm"}; currently restricted to \code{\link[stats]{binomial}}.} -\item{n.pop}{unsigned integer indicating a finite population correction for +\item{n_pop}{unsigned integer indicating a finite population correction for the estimation of the covariance matrix of the average partial effects proposed by Cruz-Gonzalez, Fernández-Val, and Weidner (2017). The correction factor is computed as follows: -\eqn{(n^{\ast} - n) / (n^{\ast} - 1)}{(n.pop - n) / (n.pop - 1)}, -where \eqn{n^{\ast}}{n.pop} and \eqn{n}{n} are the sizes of the entire +\eqn{(n^{\ast} - n) / (n^{\ast} - 1)}{(n_pop - n) / (n_pop - 1)}, +where \eqn{n^{\ast}}{n_pop} and \eqn{n}{n} are the sizes of the entire population and the full sample size. Default is \code{NULL}, which refers to a factor of zero and a covariance obtained by the delta method.} -\item{panel.structure}{a string equal to \code{"classic"} or \code{"network"} +\item{panel_structure}{a string equal to \code{"classic"} or \code{"network"} which determines the structure of the panel used. \code{"classic"} denotes panel structures where for example the same cross-sectional units are observed several times (this includes pseudo panels). \code{"network"} denotes panel structures where for example bilateral trade flows are observed for several time periods. Default is \code{"classic"}.} -\item{sampling.fe}{a string equal to \code{"independence"} or +\item{sampling_fe}{a string equal to \code{"independence"} or \code{"unrestricted"} which imposes sampling assumptions about the unobserved effects. \code{"independence"} imposes that all unobserved effects are independent sequences. \code{"unrestricted"} does not impose any sampling assumptions. Note that this option only affects the optional finite population correction. Default is \code{"independence"}.} -\item{weak.exo}{logical indicating if some of the regressors are assumed to +\item{weak_exo}{logical indicating if some of the regressors are assumed to be weakly exogenous (e.g. predetermined). If object is of class \code{"bias_corr"}, the option will be automatically set to \code{TRUE} if the chosen bandwidth parameter is larger than zero. Note that this option diff --git a/man/bias_corr.Rd b/man/bias_corr.Rd index 9026431..7825153 100644 --- a/man/bias_corr.Rd +++ b/man/bias_corr.Rd @@ -5,7 +5,7 @@ \title{Asymptotic bias correction after fitting binary choice models with a 1,2,3-way error component} \usage{ -bias_corr(object = NULL, L = 0L, panel.structure = c("classic", "network")) +bias_corr(object = NULL, L = 0L, panel_structure = c("classic", "network")) } \arguments{ \item{object}{an object of class \code{"feglm"}.} @@ -18,7 +18,7 @@ weakly exogenous regressors, e.g. lagged outcome variables, we suggest to choose a bandwidth between one and four. Note that the order of factors to be partialed out is important for bandwidths larger than zero.} -\item{panel.structure}{a string equal to \code{"classic"} or \code{"network"} +\item{panel_structure}{a string equal to \code{"classic"} or \code{"network"} which determines the structure of the panel used. \code{"classic"} denotes panel structures where for example the same cross-sectional units are observed several times (this includes pseudo panels). \code{"network"} diff --git a/man/feglm.Rd b/man/feglm.Rd index aed0168..f8c376d 100644 --- a/man/feglm.Rd +++ b/man/feglm.Rd @@ -9,8 +9,8 @@ feglm( data = NULL, family = gaussian(), weights = NULL, - beta.start = NULL, - eta.start = NULL, + beta_start = NULL, + eta_start = NULL, control = NULL ) } @@ -32,11 +32,11 @@ details of family functions.} \item{weights}{an optional string with the name of the 'prior weights' variable in \code{data}.} -\item{beta.start}{an optional vector of starting values for the structural +\item{beta_start}{an optional vector of starting values for the structural parameters in the linear predictor. Default is \eqn{\boldsymbol{\beta} = \mathbf{0}}{\beta = 0}.} -\item{eta.start}{an optional vector of starting values for the linear +\item{eta_start}{an optional vector of starting values for the linear predictor.} \item{control}{a named list of parameters for controlling the fitting diff --git a/man/feglm_control.Rd b/man/feglm_control.Rd index 1afece9..9838e19 100644 --- a/man/feglm_control.Rd +++ b/man/feglm_control.Rd @@ -5,28 +5,28 @@ \title{Set \code{feglm} Control Parameters} \usage{ feglm_control( - dev.tol = 1e-08, - center.tol = 1e-08, - iter.max = 25L, + dev_tol = 1e-08, + center_tol = 1e-08, + iter_max = 25L, limit = 10L, trace = FALSE, - drop.pc = TRUE, - keep.mx = TRUE + drop_pc = TRUE, + keep_mx = FALSE ) } \arguments{ -\item{dev.tol}{tolerance level for the first stopping condition of the +\item{dev_tol}{tolerance level for the first stopping condition of the maximization routine. The stopping condition is based on the relative change of the deviance in iteration \eqn{r} and can be expressed as follows: \eqn{|dev_{r} - dev_{r - 1}| / (0.1 + |dev_{r}|) < tol}{|dev - devold| / (0.1 + |dev|) < tol}. The default is \code{1.0e-08}.} -\item{center.tol}{tolerance level for the stopping condition of the centering +\item{center_tol}{tolerance level for the stopping condition of the centering algorithm. The stopping condition is based on the relative change of the centered variable similar to the \code{'lfe'} package. The default is \code{1.0e-08}.} -\item{iter.max}{unsigned integer indicating the maximum number of iterations +\item{iter_max}{unsigned integer indicating the maximum number of iterations in the maximization routine. The default is \code{25L}.} \item{limit}{unsigned integer indicating the maximum number of iterations of @@ -35,14 +35,14 @@ in the maximization routine. The default is \code{25L}.} \item{trace}{logical indicating if output should be produced in each iteration. Default is \code{FALSE}.} -\item{drop.pc}{logical indicating to drop observations that are perfectly +\item{drop_pc}{logical indicating to drop observations that are perfectly classified/separated and hence do not contribute to the log-likelihood. This option is useful to reduce the computational costs of the maximization problem and improves the numerical stability of the algorithm. Note that dropping perfectly separated observations does not affect the estimates. The default is \code{TRUE}.} -\item{keep.mx}{logical indicating if the centered regressor matrix should be +\item{keep_mx}{logical indicating if the centered regressor matrix should be stored. The centered regressor matrix is required for some covariance estimators, bias corrections, and average partial effects. This option saves some computation time at the cost of memory. The default is \code{TRUE}.} diff --git a/man/fenegbin.Rd b/man/fenegbin.Rd index 273d85c..8ff9039 100644 --- a/man/fenegbin.Rd +++ b/man/fenegbin.Rd @@ -9,9 +9,9 @@ fenegbin( formula = NULL, data = NULL, weights = NULL, - beta.start = NULL, - eta.start = NULL, - init.theta = NULL, + beta_start = NULL, + eta_start = NULL, + init_theta = NULL, link = c("log", "identity", "sqrt"), control = NULL ) @@ -29,14 +29,14 @@ in the model.} \item{weights}{an optional string with the name of the 'prior weights' variable in \code{data}.} -\item{beta.start}{an optional vector of starting values for the structural +\item{beta_start}{an optional vector of starting values for the structural parameters in the linear predictor. Default is \eqn{\boldsymbol{\beta} = \mathbf{0}}{\beta = 0}.} -\item{eta.start}{an optional vector of starting values for the linear +\item{eta_start}{an optional vector of starting values for the linear predictor.} -\item{init.theta}{an optional initial value for the theta parameter (see +\item{init_theta}{an optional initial value for the theta parameter (see \code{\link[MASS]{glm.nb}}).} \item{link}{the link function. Must be one of \code{"log"}, \code{"sqrt"}, or diff --git a/man/fepoisson.Rd b/man/fepoisson.Rd index 37924a0..392ca8f 100644 --- a/man/fepoisson.Rd +++ b/man/fepoisson.Rd @@ -8,8 +8,8 @@ fepoisson( formula = NULL, data = NULL, weights = NULL, - beta.start = NULL, - eta.start = NULL, + beta_start = NULL, + eta_start = NULL, control = NULL ) } @@ -26,11 +26,11 @@ in the model.} \item{weights}{an optional string with the name of the 'prior weights' variable in \code{data}.} -\item{beta.start}{an optional vector of starting values for the structural +\item{beta_start}{an optional vector of starting values for the structural parameters in the linear predictor. Default is \eqn{\boldsymbol{\beta} = \mathbf{0}}{\beta = 0}.} -\item{eta.start}{an optional vector of starting values for the linear +\item{eta_start}{an optional vector of starting values for the linear predictor.} \item{control}{a named list of parameters for controlling the fitting diff --git a/man/fixed_effects.Rd b/man/fixed_effects.Rd index 1e625b5..92dca22 100644 --- a/man/fixed_effects.Rd +++ b/man/fixed_effects.Rd @@ -4,12 +4,12 @@ \alias{fixed_effects} \title{Recover the estimates of the fixed effects after fitting (G)LMs} \usage{ -fixed_effects(object = NULL, alpha.tol = 1e-08) +fixed_effects(object = NULL, alpha_tol = 1e-08) } \arguments{ \item{object}{an object of class \code{"feglm"}.} -\item{alpha.tol}{tolerance level for the stopping condition. The algorithm is +\item{alpha_tol}{tolerance level for the stopping condition. The algorithm is stopped at iteration \eqn{i} if \eqn{||\boldsymbol{\alpha}_{i} - \boldsymbol{\alpha}_{i - 1}||_{2} < tol ||\boldsymbol{\alpha}_{i - 1}|| {2}}{||\Delta \alpha|| < tol ||\alpha_old||}. Default is \code{1.0e-08}.} diff --git a/man/vcov.feglm.Rd b/man/vcov.feglm.Rd index d81d66f..9e56b20 100644 --- a/man/vcov.feglm.Rd +++ b/man/vcov.feglm.Rd @@ -14,7 +14,7 @@ \item{object}{an object of class \code{"feglm"}.} \item{type}{the type of covariance estimate required. \code{"hessian"} refers -to the inverse of the negative expected Hessian after convergence and is the +to the inverse of the negative expected hessian after convergence and is the default option. \code{"outer.product"} is the outer-product-of-the-gradient estimator. \code{"sandwich"} is the sandwich estimator (sometimes also referred as robust estimator), and \code{"clustered"} computes a clustered @@ -30,7 +30,7 @@ A named matrix of covariance estimates. \description{ Covariance matrix for the estimator of the structural parameters from objects returned by \code{\link{feglm}}. The covariance is computed -from the Hessian, the scores, or a combination of both after convergence. +from the hessian, the scores, or a combination of both after convergence. } \examples{ mod <- fepoisson( diff --git a/man/vcov.felm.Rd b/man/vcov.felm.Rd index e97c1de..9b3ac9a 100644 --- a/man/vcov.felm.Rd +++ b/man/vcov.felm.Rd @@ -11,10 +11,10 @@ ) } \arguments{ -\item{object}{an object of class \code{"feglm"}.} +\item{object}{an object of class \code{"felm"}.} \item{type}{the type of covariance estimate required. \code{"hessian"} refers -to the inverse of the negative expected Hessian after convergence and is the +to the inverse of the negative expected hessian after convergence and is the default option. \code{"outer.product"} is the outer-product-of-the-gradient estimator. \code{"sandwich"} is the sandwich estimator (sometimes also referred as robust estimator), and \code{"clustered"} computes a clustered @@ -29,8 +29,8 @@ A named matrix of covariance estimates. } \description{ Covariance matrix for the estimator of the structural parameters -from objects returned by \code{\link{feglm}}. The covariance is computed -from the Hessian, the scores, or a combination of both after convergence. +from objects returned by \code{\link{felm}}. The covariance is computed +from the hessian, the scores, or a combination of both after convergence. } \examples{ mod <- fepoisson( diff --git a/src/00_main.h b/src/00_main.h index cf902ac..d4153b2 100644 --- a/src/00_main.h +++ b/src/00_main.h @@ -11,3 +11,36 @@ using namespace arma; using namespace cpp11; + +// used across the scripts + +Mat center_variables_(const Mat &V, const Col &w, + const list &klist, const double &tol, + const int &maxiter); + +Col solve_beta_(const Mat &MX, const Mat &MNU, + const Col &w); + +Col solve_eta_(const Mat &MX, const Mat &MNU, + const Col &nu, const Col &beta); + +Mat crossprod_(const Mat &X, const Col &w, const int &n, + const int &p, const bool &weighted, + const bool &root_weights); + +std::string tidy_family_(const std::string &family); + +Col link_inv_(const Col &eta, const std::string &fam); + +double dev_resids_(const Col &y, const Col &mu, + const double &theta, const Col &wt, + const std::string &fam); + +Col mu_eta_(Col &eta, const std::string &fam); + +Col variance_(const Col &mu, const double &theta, + const std::string &fam); + +bool valid_eta_(const Col &eta, const std::string &fam); + +bool valid_mu_(const Col &mu, const std::string &fam); diff --git a/src/01_center_variables.cpp b/src/01_center_variables.cpp index 66250d1..492c169 100644 --- a/src/01_center_variables.cpp +++ b/src/01_center_variables.cpp @@ -1,20 +1,9 @@ #include "00_main.h" // Method of alternating projections (Halperin) -[[cpp11::register]] doubles_matrix<> -center_variables_(const doubles_matrix<> &V_r, const doubles &v_sum_r, - const doubles &w_r, const list &klist, const double &tol, - const int &maxiter, const bool &sum_v) { - // Type conversion - Mat V = as_Mat(V_r); - Mat w = as_Mat(w_r); - - if (sum_v) { - Mat v_sum = as_Mat(v_sum_r); - V.each_col() += v_sum; - v_sum.reset(); - } - +Mat center_variables_(const Mat &V, const Col &w, + const list &klist, const double &tol, + const int &maxiter) { // Auxiliary variables (fixed) const int N = V.n_rows; const int P = V.n_cols; @@ -24,6 +13,7 @@ center_variables_(const doubles_matrix<> &V_r, const doubles &v_sum_r, // Auxiliary variables (storage) int iter, j, k, p, J; double delta, meanj; + Mat C(N, P); Mat x(N, 1); Mat x0(N, 1); @@ -31,9 +21,6 @@ center_variables_(const doubles_matrix<> &V_r, const doubles &v_sum_r, field> group_indices(K); field group_weights(K); - // #ifdef _OPENMP - // #pragma omp parallel for private(indices, j, J) schedule(static) - // #endif for (k = 0; k < K; ++k) { list jlist = klist[k]; J = jlist.size(); @@ -46,9 +33,6 @@ center_variables_(const doubles_matrix<> &V_r, const doubles &v_sum_r, } // Halperin projections - // #ifdef _OPENMP - // #pragma omp parallel for private(x, x0, iter, j, k, J, meanj, delta) schedule(static) - // #endif for (p = 0; p < P; ++p) { // Center each variable x = V.col(p); @@ -56,6 +40,7 @@ center_variables_(const doubles_matrix<> &V_r, const doubles &v_sum_r, if ((iter % 1000) == 0) { check_user_interrupt(); } + // Store centered vector from the last iteration x0 = x; @@ -72,15 +57,22 @@ center_variables_(const doubles_matrix<> &V_r, const doubles &v_sum_r, } // Break loop if convergence is reached - delta = - accu(abs(x - x0) / (1.0 + abs(x0)) % w) * inv_sw; + delta = accu(abs(x - x0) / (1.0 + abs(x0)) % w) * inv_sw; if (delta < tol) { break; } } - V.col(p) = x; + C.col(p) = x; } // Return matrix with centered variables - return as_doubles_matrix(V); + return C; +} + +[[cpp11::register]] doubles_matrix<> center_variables_r_( + const doubles_matrix<> &V_r, const doubles &w_r, + const list &klist, const double &tol, const int &maxiter) { + return as_doubles_matrix( + center_variables_(as_Mat(V_r), as_Mat(w_r), klist, tol, maxiter) + ); } diff --git a/src/04_linear_algebra.cpp b/src/04_linear_algebra.cpp index 07d1619..a6480b3 100644 --- a/src/04_linear_algebra.cpp +++ b/src/04_linear_algebra.cpp @@ -1,195 +1,68 @@ #include "00_main.h" -// Y <- crossprod(X) -// Y <- t(X) %*% X - -[[cpp11::register]] doubles_matrix<> crossprod_(const doubles_matrix<> &x, - const doubles &w, - const bool &weighted, - const bool &root_weights) { - Mat X = as_Mat(x); - int P = X.n_cols; - - Mat res(P, P); - - if (!weighted) { - res = X.t() * X; - } else { - Mat W = as_Mat(w); - - if (root_weights) { - W = sqrt(W); - } - - X = X.each_col() % W; - - res = X.t() * X; - } - - return as_doubles_matrix(res); -} - // WinvJ < -solve(object[["Hessian"]] / nt.full, J) // Gamma < -(MX %*% WinvJ - PPsi) * v / nt.full -// V < -crossprod(Gamma) - -[[cpp11::register]] doubles_matrix<> -gamma_(const doubles_matrix<> &mx, const doubles_matrix<> &hessian, - const doubles_matrix<> &j, const doubles_matrix<> &ppsi, - const doubles &v, const SEXP &nt_full) { - Mat MX = as_Mat(mx); - Mat H = as_Mat(hessian); - Mat J = as_Mat(j); - Mat PPsi = as_Mat(ppsi); - Mat V = as_Mat(v); - - double inv_N = 1.0 / as_cpp(nt_full); - - Mat res = (MX * solve(H * inv_N, J)) - PPsi; - res = (res.each_col() % V) * inv_N; - - return as_doubles_matrix(res); -} - -// solve(H) - -[[cpp11::register]] doubles_matrix<> inv_(const doubles_matrix<> &h) { - Mat H = inv(as_Mat(h)); - return as_doubles_matrix(H); -} - -// qr(X)$rank - -[[cpp11::register]] int rank_(const doubles_matrix<> &x) { - Mat X = as_Mat(x); - return arma::rank(X); // SVD -} - -// Beta_uncorr - solve(H / nt, B) - -[[cpp11::register]] doubles solve_bias_(const doubles &beta_uncorr, - const doubles_matrix<> &hessian, - const double &nt, const doubles &b) { - Mat Beta_uncorr = as_Mat(beta_uncorr); - Mat H = as_Mat(hessian); - Mat B = as_Mat(b); - - double inv_nt = 1.0 / nt; - - return as_doubles(Beta_uncorr - solve(H * inv_nt, B)); -} - -// A %*% x - -[[cpp11::register]] doubles solve_y_(const doubles_matrix<> &a, - const doubles &x) { - Mat A = as_Mat(a); - Mat X = as_Mat(x); - - return as_doubles(A * X); -} - -// A %*% B %*% A -[[cpp11::register]] doubles_matrix<> sandwich_(const doubles_matrix<> &a, - const doubles_matrix<> &b) { - Mat A = as_Mat(a); - Mat B = as_Mat(b); - - Mat res = A * (B * A); - return as_doubles_matrix(res); -} +// [[cpp11::register]] doubles_matrix<> +// gamma_(const doubles_matrix<> &mx, const doubles_matrix<> &hessian, +// const doubles_matrix<> &j, const doubles_matrix<> &ppsi, +// const doubles &v, const SEXP &nt_full) { +// double inv_N = 1.0 / as_cpp(nt_full); -// eta <- eta.old + rho * eta.upd +// Mat res = +// (as_Mat(mx) * solve(as_Mat(hessian) * inv_N, as_Mat(j))) - as_Mat(ppsi); +// res = (res.each_col() % as_Mat(v)) * inv_N; -[[cpp11::register]] doubles -update_beta_eta_(const doubles &old, const doubles &upd, const double ¶m) { - int N = old.size(); - writable::doubles res(N); +// return as_doubles_matrix(res); +// } - double *old_data = REAL(old); - double *upd_data = REAL(upd); +// solve(H) -#ifdef _OPENMP -#pragma omp parallel for schedule(static) -#endif - for (int n = 0; n < N; ++n) { - res[n] = old_data[n] + (upd_data[n] * param); - } +// [[cpp11::register]] doubles_matrix<> inv_(const doubles_matrix<> &h) { +// Mat H = inv(as_Mat(h)); +// return as_doubles_matrix(H); +// } - return res; -} +// qr(X)$rank -// nu <- (y - mu) / mu.eta +// [[cpp11::register]] int rank_(const doubles_matrix<> &x) { +// return arma::rank(as_Mat(x)); // SVD +// } -[[cpp11::register]] doubles update_nu_(const SEXP &y, const SEXP &mu, - const SEXP &mu_eta) { - int n = Rf_length(y); - writable::doubles res(n); +// Y <- crossprod(X) +// Y <- t(X) %*% X - double *y_data = REAL(y); - double *mu_data = REAL(mu); - double *mu_eta_data = REAL(mu_eta); +Mat crossprod_(const Mat &X, const Col &w, const int &n, + const int &p, const bool &weighted, + const bool &root_weights) { + Mat res(p, p); - for (int i = 0; i < n; ++i) { - res[i] = (y_data[i] - mu_data[i]) / mu_eta_data[i]; + if (weighted == false) { + res = X.t() * X; + } else { + Mat Y(n, p); + if (root_weights == false) { + Y = X.each_col() % w; + } else { + Y = X.each_col() % sqrt(w); + } + res = Y.t() * Y; } return res; } -[[cpp11::register]] doubles solve_beta_(const doubles_matrix<> &mx, - const doubles_matrix<> &mnu, - const doubles &wtilde, - const bool &weighted) { - Mat X = as_Mat(mx); - Mat Y = as_Mat(mnu); - - // Weight the X and Y matrices - if (weighted) { - Mat w = as_Mat(wtilde); - w = sqrt(w); - X = X.each_col() % w; // element-wise multiplication - Y = Y.each_col() % w; - } - - // Solve the system X * beta = Y - - // QR decomposition +Col solve_beta_(const Mat &MX, const Mat &MNU, + const Col &w) { + Col wtilde = sqrt(w); Mat Q, R; - bool computable = qr_econ(Q, R, X); + bool computable = qr_econ(Q, R, MX.each_col() % wtilde); if (!computable) { stop("QR decomposition failed"); - } else { - // backsolve - return as_doubles(solve(R, Q.t() * Y)); } -} - -// eta.upd <- nu - as.vector(Mnu - MX %*% beta.upd) - -[[cpp11::register]] doubles solve_eta_(const doubles_matrix<> &mx, - const doubles_matrix<> &mnu, - const doubles &nu, const doubles &beta) { - Mat MX = as_Mat(mx); - Mat MNU = as_Mat(mnu); - Mat Nu = as_Mat(nu); - Mat Beta = as_Mat(beta); - - return as_doubles(Nu - (MNU - MX * Beta)); -} - -// eta.upd <- yadj - as.vector(Myadj) + offset - eta - -[[cpp11::register]] doubles solve_eta2_(const doubles &yadj, const doubles_matrix<> &myadj, - const doubles &offset, const doubles &eta) { - Mat Yadj = as_Mat(yadj); - Mat Myadj = as_Mat(myadj); - Mat Offset = as_Mat(offset); - Mat Eta = as_Mat(eta); - return as_doubles(Yadj - Myadj + Offset - Eta); + return solve(R, Q.t() * (MNU.each_col() % wtilde)); } diff --git a/src/05_glm_fit.cpp b/src/05_glm_fit.cpp new file mode 100644 index 0000000..9973818 --- /dev/null +++ b/src/05_glm_fit.cpp @@ -0,0 +1,453 @@ +#include "00_main.h" + +std::string tidy_family_(const std::string &family) { + // tidy family param + std::string fam = family; + + // 1. put all in lowercase + std::transform(fam.begin(), fam.end(), fam.begin(), + [](unsigned char c) { return std::tolower(c); }); + + // 2. remove numbers + fam.erase(std::remove_if(fam.begin(), fam.end(), ::isdigit), fam.end()); + + // 3. remove parentheses and everything inside + size_t pos = fam.find("("); + if (pos != std::string::npos) { + fam.erase(pos, fam.size()); + } + + // 4. replace spaces and dots + std::replace(fam.begin(), fam.end(), ' ', '_'); + std::replace(fam.begin(), fam.end(), '.', '_'); + + // 5. trim + fam.erase(std::remove_if(fam.begin(), fam.end(), ::isspace), fam.end()); + + return fam; +} + +Col link_inv_gaussian_(const Col &eta) { + return eta; +} + +Col link_inv_poisson_(const Col &eta) { + return exp(eta); +} + +Col link_inv_logit_(const Col &eta) { + Col expeta = exp(eta); + return expeta / (1 + expeta); +} + +Col link_inv_gamma_(const Col &eta) { + return 1 / eta; +} + +Col link_inv_invgaussian_(const Col &eta) { + return 1 / sqrt(eta); +} + +Col link_inv_negbin_(const Col &eta) { + return exp(eta); +} + +double dev_resids_gaussian_(const Col &y, const Col &mu, + const Col &wt) { + return accu(wt % square(y - mu)); +} + +double dev_resids_poisson_(const Col &y, const Col &mu, + const Col &wt) { + Col r = mu % wt; + + uvec p = find(y > 0); + r(p) = wt(p) % (y(p) % log(y(p) / mu(p)) - (y(p) - mu(p))); + + return 2 * accu(r); +} + +// Adapted from binomial_dev_resids() +// in R base it can be found in src/library/stats/src/family.c +// unfortunately the functions that work with a SEXP won't work with a Col<> +double dev_resids_logit_(const Col &y, const Col &mu, const Col &wt) { + Col r(y.n_elem, fill::zeros); + Col s(y.n_elem, fill::zeros); + + uvec p = find(y == 1); + uvec q = find(y == 0); + r(p) = y(p) % log(y(p) / mu(p)); + s(q) = (1 - y(q)) % log((1 - y(q)) / (1 - mu(q))); + + return 2 * accu(wt % (r + s)); +} + +double dev_resids_gamma_(const Col &y, const Col &mu, + const Col &wt) { + Col r = y / mu; + + uvec p = find(y == 0); + r.elem(p).fill(1.0); + r = wt % (log(r) - (y - mu) / mu); + + return -2 * accu(r); +} + +double dev_resids_invgaussian_(const Col &y, const Col &mu, + const Col &wt) { + return accu(wt % square(y - mu) / (y % square(mu))); +} + +double dev_resids_negbin_(const Col &y, const Col &mu, + const double &theta, const Col &wt) { + Col r = y; + + uvec p = find(y < 1); + r.elem(p).fill(1.0); + r = wt % (y % log(r / mu) - (y + theta) % log((y + theta) / (mu + theta))); + + return 2 * accu(r); +} + +Col mu_eta_gaussian_(const Col &eta) { + return ones>(eta.n_elem); +} + +Col mu_eta_poisson_(const Col &eta) { + return exp(eta); +} + +Col mu_eta_logit_(const Col &eta) { + Col expeta = exp(eta); + return expeta / square(1 + expeta); +} + +Col mu_eta_gamma_(const Col &eta) { + return -1 / square(eta); +} + +Col mu_eta_invgaussian_(const Col &eta) { + return -1 / (2 * pow(eta, 1.5)); +} + +Col mu_eta_negbin_(const Col &eta) { + return exp(eta); +} + +Col variance_gaussian_(const Col &mu) { + return ones>(mu.n_elem); +} + +Col variance_poisson_(const Col &mu) { + return mu; +} + +Col variance_binomial_(const Col &mu) { + return mu % (1 - mu); +} + +Col variance_gamma_(const Col &mu) { + return square(mu); +} + +Col variance_invgaussian_(const Col &mu) { + return pow(mu, 3.0); +} + +Col variance_negbin_(const Col &mu, const double &theta) { + return mu + square(mu) / theta; +} + +Col link_inv_(const Col &eta, const std::string &fam) { + Col res(eta.n_elem); + + if (fam == "gaussian") { + res = link_inv_gaussian_(eta); + } else if (fam == "poisson") { + res = link_inv_poisson_(eta); + } else if (fam == "binomial") { + res = link_inv_logit_(eta); + } else if (fam == "gamma") { + res = link_inv_gamma_(eta); + } else if (fam == "inverse_gaussian") { + res = link_inv_invgaussian_(eta); + } else if (fam == "negative_binomial") { + res = link_inv_negbin_(eta); + } else { + stop("Unknown family"); + } + + return res; +} + +double dev_resids_(const Col &y, const Col &mu, + const double &theta, const Col &wt, + const std::string &fam) { + double res; + + if (fam == "gaussian") { + res = dev_resids_gaussian_(y, mu, wt); + } else if (fam == "poisson") { + res = dev_resids_poisson_(y, mu, wt); + } else if (fam == "binomial") { + res = dev_resids_logit_(y, mu, wt); + } else if (fam == "gamma") { + res = dev_resids_gamma_(y, mu, wt); + } else if (fam == "inverse_gaussian") { + res = dev_resids_invgaussian_(y, mu, wt); + } else if (fam == "negative_binomial") { + res = dev_resids_negbin_(y, mu, theta, wt); + } else { + stop("Unknown family"); + } + + return res; +} + +bool valid_eta_(const Col &eta, const std::string &fam) { + bool res; + + if (fam == "gaussian") { + res = true; + } else if (fam == "poisson") { + res = true; + } else if (fam == "binomial") { + res = true; + } else if (fam == "gamma") { + res = is_finite(eta) && all(eta != 0.0); + } else if (fam == "inverse_gaussian") { + res = is_finite(eta) && all(eta > 0.0); + } else if (fam == "negative_binomial") { + res = true; + } else { + stop("Unknown family"); + } + + return res; +} + +bool valid_mu_(const Col &mu, const std::string &fam) { + bool res; + + if (fam == "gaussian") { + res = true; + } else if (fam == "poisson") { + res = is_finite(mu) && all(mu > 0); + } else if (fam == "binomial") { + res = is_finite(mu) && all(mu > 0 && mu < 1); + } else if (fam == "gamma") { + res = is_finite(mu) && all(mu > 0.0); + } else if (fam == "inverse_gaussian") { + res = true; + } else if (fam == "negative_binomial") { + return all(mu > 0.0); + } else { + stop("Unknown family"); + } + + return res; +} + +// mu_eta = d link_inv / d eta = d mu / d eta + +Col mu_eta_(Col &eta, const std::string &fam) { + Col res(eta.n_elem); + + if (fam == "gaussian") { + res = mu_eta_gaussian_(eta); + } else if (fam == "poisson") { + res = mu_eta_poisson_(eta); + } else if (fam == "binomial") { + res = mu_eta_logit_(eta); + } else if (fam == "gamma") { + res = mu_eta_gamma_(eta); + } else if (fam == "inverse_gaussian") { + res = mu_eta_invgaussian_(eta); + } else if (fam == "negative_binomial") { + res = mu_eta_negbin_(eta); + } else { + stop("Unknown family"); + } + + return res; +} + +Col variance_(const Col &mu, const double &theta, + const std::string &fam) { + Col res(mu.n_elem); + + if (fam == "gaussian") { + res = variance_gaussian_(mu); + } else if (fam == "poisson") { + res = variance_poisson_(mu); + } else if (fam == "binomial") { + res = variance_binomial_(mu); + } else if (fam == "gamma") { + res = variance_gamma_(mu); + } else if (fam == "inverse_gaussian") { + res = variance_invgaussian_(mu); + } else if (fam == "negative_binomial") { + res = variance_negbin_(mu, theta); + } else { + stop("Unknown family"); + } + + return res; +} + +[[cpp11::register]] list feglm_fit_(const doubles &beta_r, const doubles &eta_r, + const doubles &y_r, + const doubles_matrix<> &x_r, + const doubles &wt_r, + const double &theta, + const std::string &family, + const list &control, const list &k_list) { + // Type conversion + + Col beta = as_Col(beta_r); + Col eta = as_Col(eta_r); + Col y = as_Col(y_r); + Mat MX = as_Mat(x_r); + Mat MNU = Mat(y.n_elem, 1, fill::zeros); + Col wt = as_Col(wt_r); + + // Auxiliary variables (fixed) + + std::string fam = tidy_family_(family); + double center_tol = as_cpp(control["center_tol"]); + double dev_tol = as_cpp(control["dev_tol"]); + int iter, iter_max = as_cpp(control["iter_max"]); + int iter_center_max = 10000; + bool keep_mx = as_cpp(control["keep_mx"]); + int iter_inner, iter_inner_max = 50; + const int k = beta.n_elem; + + // Auxiliary variables (storage) + + Col mu = link_inv_(eta, fam); + Col ymean = mean(y) * Col(y.n_elem, fill::ones); + double dev = dev_resids_(y, mu, theta, wt, fam); + double null_dev = dev_resids_(y, ymean, theta, wt, fam); + + const int n = y.n_elem; + const int p = MX.n_cols; + Col mu_eta(n), nu(n); + Mat H(p, p), w(n, 1); + bool conv = false; + + bool dev_crit, val_crit, imp_crit; + double dev_old, dev_ratio, dev_ratio_inner, rho; + Col eta_upd(n), beta_upd(k), eta_old(n), beta_old(k); + + // Maximize the log-likelihood + + for (iter = 0; iter < iter_max; ++iter) { + rho = 1.0; + eta_old = eta, beta_old = beta, dev_old = dev; + + // Compute weights and dependent variable + + mu_eta = mu_eta_(eta, fam); + w = (wt % square(mu_eta)) / variance_(mu, theta, fam); + nu = (y - mu) / mu_eta; + + // Center variables + + MNU = center_variables_(MNU + nu, w, k_list, center_tol, iter_center_max); + MX = center_variables_(MX, w, k_list, center_tol, iter_center_max); + + // Compute update step and update eta + + // Step-halving with three checks: + // 1. finite deviance + // 2. valid eta and mu + // 3. improvement as in glm2 + + beta_upd = solve_beta_(MX, MNU, w); + eta_upd = nu - MNU + MX * beta_upd; + + for (iter_inner = 0; iter_inner < iter_inner_max; ++iter_inner) { + eta = eta_old + (rho * eta_upd); + beta = beta_old + (rho * beta_upd); + mu = link_inv_(eta, fam); + dev = dev_resids_(y, mu, theta, wt, fam); + dev_ratio_inner = (dev - dev_old) / (0.1 + fabs(dev)); + + dev_crit = is_finite(dev); + val_crit = (valid_eta_(eta, fam) && valid_mu_(mu, fam)); + imp_crit = (dev_ratio_inner <= -dev_tol); + + if (dev_crit == true && val_crit == true && imp_crit == true) { + break; + } + + rho *= 0.5; + } + + // Check if step-halving failed (deviance and invalid eta or mu) + + if (dev_crit == false || val_crit == false) { + stop("Inner loop failed; cannot correct step size."); + } + + // If step halving does not improve the deviance + + if (imp_crit == false) { + eta = eta_old; + beta = beta_old; + dev = dev_old; + mu = link_inv_(eta, fam); + } + + // Check convergence + + dev_ratio = fabs(dev - dev_old) / (0.1 + fabs(dev)); + + if (dev_ratio < dev_tol) { + conv = true; + break; + } + + // Update starting guesses for acceleration + + MNU = MNU - nu; + } + + // Information if convergence failed + + if (conv == false) { + stop("Algorithm did not converge."); + } + + // Update weights and dependent variable + + mu_eta = mu_eta_(eta, fam); + w = (wt % square(mu_eta)) / variance_(mu, theta, fam); + + // Recompute Hessian + + H = crossprod_(MX, w, n, p, true, true); + + // Generate result list + + writable::list out; + + out.push_back({"coefficients"_nm = as_doubles(beta)}); + out.push_back({"eta"_nm = as_doubles(eta)}); + out.push_back({"weights"_nm = as_doubles(wt)}); + out.push_back({"hessian"_nm = as_doubles_matrix(H)}); + out.push_back({"deviance"_nm = dev}); + out.push_back({"null_deviance"_nm = null_dev}); + out.push_back({"conv"_nm = conv}); + out.push_back({"iter"_nm = iter + 1}); + + if (keep_mx == true) { + out.push_back({ + "MX"_nm = as_doubles_matrix( + center_variables_(as_Mat(x_r), w, k_list, center_tol, iter_center_max) + ) + }); + } + + return out; +} diff --git a/src/06_glm_offset_fit.cpp b/src/06_glm_offset_fit.cpp new file mode 100644 index 0000000..51e7077 --- /dev/null +++ b/src/06_glm_offset_fit.cpp @@ -0,0 +1,101 @@ +#include "00_main.h" + +[[cpp11::register]] doubles feglm_offset_fit_( + const doubles &eta_r, const doubles &y_r, const doubles &offset_r, + const doubles &wt_r, const std::string &family, const list &control, + const list &k_list) { + // Type conversion + + Col eta = as_Col(eta_r); + Col y = as_Col(y_r); + Col offset = as_Col(offset_r); + Mat Myadj = Mat(y.n_elem, 1, fill::zeros); + Col wt = as_Col(wt_r); + + // Auxiliary variables (fixed) + + std::string fam = tidy_family_(family); + double center_tol = as_cpp(control["center_tol"]); + double dev_tol = as_cpp(control["dev_tol"]); + int iter, iter_max = as_cpp(control["iter_max"]); + int iter_center_max = 10000; + int iter_inner, iter_inner_max = 50; + + // Auxiliary variables (storage) + + Col mu = link_inv_(eta, fam); + double dev = dev_resids_(y, mu, 0.0, wt, fam); + + const int n = y.n_elem; + Col mu_eta(n), yadj(n); + Mat w(n, 1); + bool conv = false; + + bool dev_crit, val_crit, imp_crit; + double dev_old, dev_ratio, dev_ratio_inner, rho; + Col eta_upd(n), eta_old(n); + + // Maximize the log-likelihood + + for (iter = 0; iter < iter_max; ++iter) { + rho = 1.0; + eta_old = eta, dev_old = dev; + + // Compute weights and dependent variable + + mu_eta = mu_eta_(eta, fam); + w = (wt % square(mu_eta)) / variance_(mu, 0.0, fam); + yadj = (y - mu) / mu_eta + eta - offset; + + // Center variables + + Myadj = center_variables_(Myadj + yadj, w, k_list, center_tol, iter_center_max); + + // Compute update step and update eta + + // Step-halving with three checks: + // 1. finite deviance + // 2. valid eta and mu + // 3. improvement as in glm2 + + eta_upd = yadj - Myadj + offset - eta; + + for (iter_inner = 0; iter_inner < iter_inner_max; ++iter_inner) { + eta = eta_old + (rho * eta_upd); + mu = link_inv_(eta, fam); + dev = dev_resids_(y, mu, 0.0, wt, fam); + dev_ratio_inner = (dev - dev_old) / (0.1 + fabs(dev_old)); + + dev_crit = is_finite(dev); + val_crit = (valid_eta_(eta, fam) && valid_mu_(mu, fam)); + imp_crit = (dev_ratio_inner <= -dev_tol); + + if (dev_crit == true && val_crit == true && imp_crit == true) { + break; + } + + rho *= 0.5; + } + + // Check if step-halving failed (deviance and invalid eta or mu) + + if (dev_crit == false || val_crit == false) { + stop("Inner loop failed; cannot correct step size."); + } + + // Check convergence + + dev_ratio = fabs(dev - dev_old) / (0.1 + fabs(dev)); + + if (dev_ratio < dev_tol) { + conv = true; + break; + } + + // Update starting guesses for acceleration + + Myadj = Myadj - yadj; + } + + return as_doubles(eta); +} diff --git a/src/05_kendall_correlation.cpp b/src/07_kendall_correlation.cpp similarity index 100% rename from src/05_kendall_correlation.cpp rename to src/07_kendall_correlation.cpp diff --git a/src/cpp11.cpp b/src/cpp11.cpp index 2c74811..5d10912 100644 --- a/src/cpp11.cpp +++ b/src/cpp11.cpp @@ -6,10 +6,10 @@ #include // 01_center_variables.cpp -doubles_matrix<> center_variables_(const doubles_matrix<> & V_r, const doubles & v_sum_r, const doubles & w_r, const list & klist, const double & tol, const int & maxiter, const bool & sum_v); -extern "C" SEXP _capybara_center_variables_(SEXP V_r, SEXP v_sum_r, SEXP w_r, SEXP klist, SEXP tol, SEXP maxiter, SEXP sum_v) { +doubles_matrix<> center_variables_r_(const doubles_matrix<> & V_r, const doubles & w_r, const list & klist, const double & tol, const int & maxiter); +extern "C" SEXP _capybara_center_variables_r_(SEXP V_r, SEXP w_r, SEXP klist, SEXP tol, SEXP maxiter) { BEGIN_CPP11 - return cpp11::as_sexp(center_variables_(cpp11::as_cpp &>>(V_r), cpp11::as_cpp>(v_sum_r), cpp11::as_cpp>(w_r), cpp11::as_cpp>(klist), cpp11::as_cpp>(tol), cpp11::as_cpp>(maxiter), cpp11::as_cpp>(sum_v))); + return cpp11::as_sexp(center_variables_r_(cpp11::as_cpp &>>(V_r), cpp11::as_cpp>(w_r), cpp11::as_cpp>(klist), cpp11::as_cpp>(tol), cpp11::as_cpp>(maxiter))); END_CPP11 } // 02_get_alpha.cpp @@ -47,98 +47,28 @@ extern "C" SEXP _capybara_group_sums_cov_(SEXP M_r, SEXP N_r, SEXP jlist) { return cpp11::as_sexp(group_sums_cov_(cpp11::as_cpp &>>(M_r), cpp11::as_cpp &>>(N_r), cpp11::as_cpp>(jlist))); END_CPP11 } -// 04_linear_algebra.cpp -doubles_matrix<> crossprod_(const doubles_matrix<> & x, const doubles & w, const bool & weighted, const bool & root_weights); -extern "C" SEXP _capybara_crossprod_(SEXP x, SEXP w, SEXP weighted, SEXP root_weights) { +// 05_glm_fit.cpp +list feglm_fit_(const doubles & beta_r, const doubles & eta_r, const doubles & y_r, const doubles_matrix<> & x_r, const doubles & wt_r, const double & theta, const std::string & family, const list & control, const list & k_list); +extern "C" SEXP _capybara_feglm_fit_(SEXP beta_r, SEXP eta_r, SEXP y_r, SEXP x_r, SEXP wt_r, SEXP theta, SEXP family, SEXP control, SEXP k_list) { BEGIN_CPP11 - return cpp11::as_sexp(crossprod_(cpp11::as_cpp &>>(x), cpp11::as_cpp>(w), cpp11::as_cpp>(weighted), cpp11::as_cpp>(root_weights))); + return cpp11::as_sexp(feglm_fit_(cpp11::as_cpp>(beta_r), cpp11::as_cpp>(eta_r), cpp11::as_cpp>(y_r), cpp11::as_cpp &>>(x_r), cpp11::as_cpp>(wt_r), cpp11::as_cpp>(theta), cpp11::as_cpp>(family), cpp11::as_cpp>(control), cpp11::as_cpp>(k_list))); END_CPP11 } -// 04_linear_algebra.cpp -doubles_matrix<> gamma_(const doubles_matrix<> & mx, const doubles_matrix<> & hessian, const doubles_matrix<> & j, const doubles_matrix<> & ppsi, const doubles & v, const SEXP & nt_full); -extern "C" SEXP _capybara_gamma_(SEXP mx, SEXP hessian, SEXP j, SEXP ppsi, SEXP v, SEXP nt_full) { +// 06_glm_offset_fit.cpp +doubles feglm_offset_fit_(const doubles & eta_r, const doubles & y_r, const doubles & offset_r, const doubles & wt_r, const std::string & family, const list & control, const list & k_list); +extern "C" SEXP _capybara_feglm_offset_fit_(SEXP eta_r, SEXP y_r, SEXP offset_r, SEXP wt_r, SEXP family, SEXP control, SEXP k_list) { BEGIN_CPP11 - return cpp11::as_sexp(gamma_(cpp11::as_cpp &>>(mx), cpp11::as_cpp &>>(hessian), cpp11::as_cpp &>>(j), cpp11::as_cpp &>>(ppsi), cpp11::as_cpp>(v), cpp11::as_cpp>(nt_full))); + return cpp11::as_sexp(feglm_offset_fit_(cpp11::as_cpp>(eta_r), cpp11::as_cpp>(y_r), cpp11::as_cpp>(offset_r), cpp11::as_cpp>(wt_r), cpp11::as_cpp>(family), cpp11::as_cpp>(control), cpp11::as_cpp>(k_list))); END_CPP11 } -// 04_linear_algebra.cpp -doubles_matrix<> inv_(const doubles_matrix<> & h); -extern "C" SEXP _capybara_inv_(SEXP h) { - BEGIN_CPP11 - return cpp11::as_sexp(inv_(cpp11::as_cpp &>>(h))); - END_CPP11 -} -// 04_linear_algebra.cpp -int rank_(const doubles_matrix<> & x); -extern "C" SEXP _capybara_rank_(SEXP x) { - BEGIN_CPP11 - return cpp11::as_sexp(rank_(cpp11::as_cpp &>>(x))); - END_CPP11 -} -// 04_linear_algebra.cpp -doubles solve_bias_(const doubles & beta_uncorr, const doubles_matrix<> & hessian, const double & nt, const doubles & b); -extern "C" SEXP _capybara_solve_bias_(SEXP beta_uncorr, SEXP hessian, SEXP nt, SEXP b) { - BEGIN_CPP11 - return cpp11::as_sexp(solve_bias_(cpp11::as_cpp>(beta_uncorr), cpp11::as_cpp &>>(hessian), cpp11::as_cpp>(nt), cpp11::as_cpp>(b))); - END_CPP11 -} -// 04_linear_algebra.cpp -doubles solve_y_(const doubles_matrix<> & a, const doubles & x); -extern "C" SEXP _capybara_solve_y_(SEXP a, SEXP x) { - BEGIN_CPP11 - return cpp11::as_sexp(solve_y_(cpp11::as_cpp &>>(a), cpp11::as_cpp>(x))); - END_CPP11 -} -// 04_linear_algebra.cpp -doubles_matrix<> sandwich_(const doubles_matrix<> & a, const doubles_matrix<> & b); -extern "C" SEXP _capybara_sandwich_(SEXP a, SEXP b) { - BEGIN_CPP11 - return cpp11::as_sexp(sandwich_(cpp11::as_cpp &>>(a), cpp11::as_cpp &>>(b))); - END_CPP11 -} -// 04_linear_algebra.cpp -doubles update_beta_eta_(const doubles & old, const doubles & upd, const double & param); -extern "C" SEXP _capybara_update_beta_eta_(SEXP old, SEXP upd, SEXP param) { - BEGIN_CPP11 - return cpp11::as_sexp(update_beta_eta_(cpp11::as_cpp>(old), cpp11::as_cpp>(upd), cpp11::as_cpp>(param))); - END_CPP11 -} -// 04_linear_algebra.cpp -doubles update_nu_(const SEXP & y, const SEXP & mu, const SEXP & mu_eta); -extern "C" SEXP _capybara_update_nu_(SEXP y, SEXP mu, SEXP mu_eta) { - BEGIN_CPP11 - return cpp11::as_sexp(update_nu_(cpp11::as_cpp>(y), cpp11::as_cpp>(mu), cpp11::as_cpp>(mu_eta))); - END_CPP11 -} -// 04_linear_algebra.cpp -doubles solve_beta_(const doubles_matrix<> & mx, const doubles_matrix<> & mnu, const doubles & wtilde, const bool & weighted); -extern "C" SEXP _capybara_solve_beta_(SEXP mx, SEXP mnu, SEXP wtilde, SEXP weighted) { - BEGIN_CPP11 - return cpp11::as_sexp(solve_beta_(cpp11::as_cpp &>>(mx), cpp11::as_cpp &>>(mnu), cpp11::as_cpp>(wtilde), cpp11::as_cpp>(weighted))); - END_CPP11 -} -// 04_linear_algebra.cpp -doubles solve_eta_(const doubles_matrix<> & mx, const doubles_matrix<> & mnu, const doubles & nu, const doubles & beta); -extern "C" SEXP _capybara_solve_eta_(SEXP mx, SEXP mnu, SEXP nu, SEXP beta) { - BEGIN_CPP11 - return cpp11::as_sexp(solve_eta_(cpp11::as_cpp &>>(mx), cpp11::as_cpp &>>(mnu), cpp11::as_cpp>(nu), cpp11::as_cpp>(beta))); - END_CPP11 -} -// 04_linear_algebra.cpp -doubles solve_eta2_(const doubles & yadj, const doubles_matrix<> & myadj, const doubles & offset, const doubles & eta); -extern "C" SEXP _capybara_solve_eta2_(SEXP yadj, SEXP myadj, SEXP offset, SEXP eta) { - BEGIN_CPP11 - return cpp11::as_sexp(solve_eta2_(cpp11::as_cpp>(yadj), cpp11::as_cpp &>>(myadj), cpp11::as_cpp>(offset), cpp11::as_cpp>(eta))); - END_CPP11 -} -// 05_kendall_correlation.cpp +// 06_kendall_correlation.cpp double kendall_cor_(const doubles_matrix<> & m); extern "C" SEXP _capybara_kendall_cor_(SEXP m) { BEGIN_CPP11 return cpp11::as_sexp(kendall_cor_(cpp11::as_cpp &>>(m))); END_CPP11 } -// 05_kendall_correlation.cpp +// 06_kendall_correlation.cpp doubles pkendall_(doubles Q, int n); extern "C" SEXP _capybara_pkendall_(SEXP Q, SEXP n) { BEGIN_CPP11 @@ -148,26 +78,16 @@ extern "C" SEXP _capybara_pkendall_(SEXP Q, SEXP n) { extern "C" { static const R_CallMethodDef CallEntries[] = { - {"_capybara_center_variables_", (DL_FUNC) &_capybara_center_variables_, 7}, - {"_capybara_crossprod_", (DL_FUNC) &_capybara_crossprod_, 4}, - {"_capybara_gamma_", (DL_FUNC) &_capybara_gamma_, 6}, + {"_capybara_center_variables_r_", (DL_FUNC) &_capybara_center_variables_r_, 5}, + {"_capybara_feglm_fit_", (DL_FUNC) &_capybara_feglm_fit_, 9}, + {"_capybara_feglm_offset_fit_", (DL_FUNC) &_capybara_feglm_offset_fit_, 7}, {"_capybara_get_alpha_", (DL_FUNC) &_capybara_get_alpha_, 3}, {"_capybara_group_sums_", (DL_FUNC) &_capybara_group_sums_, 3}, {"_capybara_group_sums_cov_", (DL_FUNC) &_capybara_group_sums_cov_, 3}, {"_capybara_group_sums_spectral_", (DL_FUNC) &_capybara_group_sums_spectral_, 5}, {"_capybara_group_sums_var_", (DL_FUNC) &_capybara_group_sums_var_, 2}, - {"_capybara_inv_", (DL_FUNC) &_capybara_inv_, 1}, {"_capybara_kendall_cor_", (DL_FUNC) &_capybara_kendall_cor_, 1}, {"_capybara_pkendall_", (DL_FUNC) &_capybara_pkendall_, 2}, - {"_capybara_rank_", (DL_FUNC) &_capybara_rank_, 1}, - {"_capybara_sandwich_", (DL_FUNC) &_capybara_sandwich_, 2}, - {"_capybara_solve_beta_", (DL_FUNC) &_capybara_solve_beta_, 4}, - {"_capybara_solve_bias_", (DL_FUNC) &_capybara_solve_bias_, 4}, - {"_capybara_solve_eta2_", (DL_FUNC) &_capybara_solve_eta2_, 4}, - {"_capybara_solve_eta_", (DL_FUNC) &_capybara_solve_eta_, 4}, - {"_capybara_solve_y_", (DL_FUNC) &_capybara_solve_y_, 2}, - {"_capybara_update_beta_eta_", (DL_FUNC) &_capybara_update_beta_eta_, 3}, - {"_capybara_update_nu_", (DL_FUNC) &_capybara_update_nu_, 3}, {NULL, NULL, 0} }; } diff --git a/tests/testthat/test-apes-bias.R b/tests/testthat/test-apes-bias.R index d410c9e..4ab15f7 100644 --- a/tests/testthat/test-apes-bias.R +++ b/tests/testthat/test-apes-bias.R @@ -1,13 +1,24 @@ -test_that("apes works", { +test_that("apes/bias works", { trade_short <- trade_panel[trade_panel$year %in% 2002L:2006L, ] trade_short$trade <- ifelse(trade_short$trade > 100, 1L, 0L) - mod <- feglm(trade ~ lang | year, trade_short, family = binomial()) + mod1 <- feglm(trade ~ lang | year, trade_short, family = binomial()) + apes1 <- apes(mod1) + bias1 <- bias_corr(mod1) - expect_output(print(mod)) + # mod2 <- alpaca::feglm(trade ~ lang | year, trade_short, family = binomial()) + # apes2 <- alpaca::getAPEs(mod2) + # bias2 <- alpaca::biasCorr(mod2) + apes2 <- c("lang" = 0.05594) + bias2 <- c("lang" = 0.23390) - expect_gt(length(coef(apes(mod))), 0) - expect_gt(length(coef(summary(apes(mod)))), 0) - expect_gt(length(coef(bias_corr(mod))), 0) - expect_gt(length(coef(summary(bias_corr(mod)))), 0) + expect_output(print(mod1)) + + expect_equal(length(coef(apes1)), 1) + expect_equal(round(coef(apes1), 5), apes2) + expect_equal(length(coef(summary(apes(mod1)))), 4) + + expect_equal(length(coef(bias1)), 1) + expect_equal(round(coef(bias1), 1), round(bias2, 1)) + expect_equal(length(coef(summary(bias1))), 4) }) diff --git a/tests/testthat/test-errors.R b/tests/testthat/test-errors.R index f404e9e..72d0611 100644 --- a/tests/testthat/test-errors.R +++ b/tests/testthat/test-errors.R @@ -1,4 +1,4 @@ -test_that("multiplication works", { +test_that("error conditions", { trade_panel_2002 <- trade_panel[trade_panel$year == 2002, ] trade_panel_2002$trade_100 <- ifelse(trade_panel_2002$trade >= 100, 1, 0) trade_panel_2002$trade_200_100 <- as.factor(ifelse(trade_panel_2002$trade >= 200, 1, @@ -30,7 +30,7 @@ test_that("multiplication works", { data = trade_panel_2002, family = binomial() ), - panel.structure = "classic" + panel_structure = "classic" ), "two-way" ) @@ -41,7 +41,7 @@ test_that("multiplication works", { data = trade_panel_2002, family = binomial() ), - panel.structure = "network" + panel_structure = "network" ), "three-way" ) @@ -51,7 +51,7 @@ test_that("multiplication works", { fepoisson( trade ~ log_dist | rta, data = trade_panel_2002, - control = list(dev.tol = -1.0) + control = list(dev_tol = -1.0) ), "greater than zero" ) @@ -60,7 +60,7 @@ test_that("multiplication works", { fepoisson( trade ~ log_dist | rta, data = trade_panel_2002, - control = list(iter.max = 0) + control = list(iter_max = 0) ), "at least one" ) diff --git a/tests/testthat/test-feglm.R b/tests/testthat/test-feglm.R new file mode 100644 index 0000000..7de742f --- /dev/null +++ b/tests/testthat/test-feglm.R @@ -0,0 +1,75 @@ +test_that("feglm is similar to glm", { + # Gaussian ---- + + # see felm + + # Poisson + + # see fepoisson + + # Binomial ---- + + mod <- feglm( + am ~ wt + mpg | cyl, + mtcars, + family = binomial() + ) + + mod_base <- glm( + am ~ wt + mpg + as.factor(cyl), + mtcars, + family = binomial() + ) + + expect_equal(unname(round(coef(mod) - coef(mod_base)[2:3], 3)), rep(0, 2)) + + fe <- unname(drop(fixed_effects(mod)$cyl)) + fe_base <- coef(mod_base)[c(1, 4, 5)] + fe_base <- unname(fe_base + c(0, rep(fe_base[1], 2))) + + expect_equal(round(fe - fe_base, 2), rep(0, 3)) + + # Gamma ---- + + mod <- feglm( + mpg ~ wt + am | cyl, + mtcars, + family = Gamma() + ) + + mod_base <- glm( + mpg ~ wt + am + as.factor(cyl), + mtcars, + family = Gamma() + ) + + expect_equal(unname(round(coef(mod) - coef(mod_base)[2:3], 3)), rep(0, 2)) + + fe <- unname(drop(fixed_effects(mod)$cyl)) + fe_base <- coef(mod_base)[c(1, 4, 5)] + fe_base <- unname(fe_base + c(0, rep(fe_base[1], 2))) + + expect_equal(round(fe - fe_base, 2), rep(0, 3)) + + # Inverse Gaussian ---- + + mod <- feglm( + mpg ~ wt + am | cyl, + mtcars, + family = inverse.gaussian() + ) + + mod_base <- glm( + mpg ~ wt + am + as.factor(cyl), + mtcars, + family = inverse.gaussian() + ) + + expect_equal(unname(round(coef(mod) - coef(mod_base)[2:3], 3)), rep(0, 2)) + + fe <- unname(drop(fixed_effects(mod)$cyl)) + fe_base <- coef(mod_base)[c(1, 4, 5)] + fe_base <- unname(fe_base + c(0, rep(fe_base[1], 2))) + + expect_equal(round(fe - fe_base, 2), rep(0, 3)) +}) diff --git a/tests/testthat/test-felm.R b/tests/testthat/test-felm.R index 25b1a77..8aa0991 100644 --- a/tests/testthat/test-felm.R +++ b/tests/testthat/test-felm.R @@ -2,9 +2,25 @@ test_that("felm works", { m1 <- felm(mpg ~ wt | cyl, mtcars) m2 <- lm(mpg ~ wt + as.factor(cyl), mtcars) - expect_equal(coef(m1), coef(m2)[2]) - expect_gt(length(fitted(m1)), 0) - expect_gt(length(predict(m1)), 0) - expect_gt(length(coef(m1)), 0) - expect_gt(length(coef(summary(m1))), 0) + expect_equal(round(coef(m1), 5), round(coef(m2)[2], 5)) + + n <- nrow(mtcars) + expect_equal(length(fitted(m1)), n) + expect_equal(length(predict(m1)), n) + expect_equal(length(coef(m1)), 1) + expect_equal(length(coef(summary(m1))), 4) + + m1 <- felm(mpg ~ wt + qsec | cyl, mtcars) + m2 <- lm(mpg ~ wt + qsec + as.factor(cyl), mtcars) + + expect_equal(round(coef(m1), 5), round(coef(m2)[c(2,3)], 5)) + + m1 <- felm(mpg ~ wt + qsec | cyl + am, mtcars) + m2 <- lm(mpg ~ wt + qsec + as.factor(cyl) + as.factor(am), mtcars) + + expect_equal(round(coef(m1), 5), round(coef(m2)[c(2, 3)], 5)) + + m1 <- felm(mpg ~ wt + qsec | cyl + am | carb, mtcars) + + expect_equal(round(coef(m1), 5), round(coef(m2)[c(2, 3)], 5)) }) diff --git a/tests/testthat/test-fepoisson.R b/tests/testthat/test-fepoisson.R index c4500e7..582dd66 100644 --- a/tests/testthat/test-fepoisson.R +++ b/tests/testthat/test-fepoisson.R @@ -9,6 +9,10 @@ test_that("fepoisson is similar to fixest", { # trade_panel, # cluster = ~pair # ) + + coef_mod_fixest <- c(-0.8409273, 0.2474765, 0.4374432, -0.2224899) + + expect_equal(unname(round(coef(mod) - coef_mod_fixest, 5)), rep(0, 4)) summary_mod <- summary(mod, type = "clustered") @@ -24,26 +28,22 @@ test_that("fepoisson is similar to fixest", { expect_visible(summary(mod, type = "cluster")) fes <- fixed_effects(mod) - + n <- unname(mod[["nobs"]]["nobs"]) + expect_equal(length(fes), 2) + expect_equal(length(fitted(mod)), n) + expect_equal(length(predict(mod)), n) + expect_equal(length(coef(mod)), 4) expect_equal(length(fes), 2) + expect_equal(round(fes[["exp_year"]][1:3], 3), c(10.195, 11.081, 11.260)) + expect_equal(round(fes[["imp_year"]][1:3], 3), c(0.226, -0.254, 1.115)) smod <- summary(mod) - expect_gt(length(fitted(mod)), 0) - expect_gt(length(predict(mod)), 0) - expect_gt(length(coef(mod)), 0) - expect_gt(length(coef(smod)), 0) - + expect_equal(length(coef(smod)[, 1]), 4) expect_output(summary_formula_(smod)) expect_output(summary_family_(smod)) expect_output(summary_estimates_(smod, 3)) expect_output(summary_r2_(smod, 3)) expect_output(summary_nobs_(smod)) expect_output(summary_fisher_(smod)) - - fe <- fixed_effects(mod) - - expect_equal(length(fe), 2) - expect_equal(round(fe$exp_year[1:3], 3), c(10.195, 11.081, 11.260)) - expect_equal(round(fe$imp_year[1:3], 3), c(0.226, -0.254, 1.115)) }) diff --git a/tests/testthat/test-linear_algebra.R b/tests/testthat/test-linear_algebra.R deleted file mode 100644 index 8a2b453..0000000 --- a/tests/testthat/test-linear_algebra.R +++ /dev/null @@ -1,62 +0,0 @@ -test_that("solve_bias_ works", { - A <- matrix(c(1, 0, 0, 1), nrow = 2, ncol = 2) - x <- c(2, 2) - expect_equal(as.vector(A %*% x), solve_y_(A, x)) - expect_equal(x - solve(A, x), solve_bias_(x, A, 1, x)) -}) - -test_that("inv_ works", { - A <- matrix(c(1, 0, 0, 1, 1, 0, 0, 1, 1), nrow = 3, ncol = 3, byrow = TRUE) - expect_equal(solve(A), inv_(A)) - - # non-invertible matrix - A <- matrix(c(1, 0, 0, 1, 0, 0, 0, 1, 1), nrow = 3, ncol = 3, byrow = TRUE) - expect_error(inv_(A)) - - # non-square matrix - A <- matrix(c(1, 0, 0, 1, 1, 0), nrow = 2, ncol = 3, byrow = TRUE) - expect_error(inv_(A)) -}) - -test_that("crossprod_ works", { - set.seed(123) - A <- matrix(rnorm(4), nrow = 2) - w <- c(1,1) - - expect_equal(crossprod(A), crossprod_(A, w, T, T)) - expect_equal(crossprod(A), crossprod_(A, NA_real_, F, F)) - - w <- c(1,2) - - # Multiply A by w column-wise - B <- matrix(NA_real_, nrow = nrow(A), ncol = ncol(A)) - - for (j in 1:ncol(A)) { - B[, j] <- A[, j] * w - } - - expect_equal(crossprod(B), crossprod_(A, w, T, F)) - - for (j in 1:ncol(A)) { - B[, j] <- A[, j] * sqrt(w) - } - - expect_equal(crossprod(B), crossprod_(A, w, T, T)) -}) - -test_that("backsolve_ works", { - A <- matrix(c(1, 0, 0, 1, 1, 0, 0, 1, 1), nrow = 3, ncol = 3, byrow = TRUE) - b <- c(6.50, 7.50, 8.50) - - expect_equal(solve(A,b), solve_beta_(A, b, NA_real_, FALSE)) - - # With weights - # Multiply each column of A by w pair-wise - # Multiply each b by w pair-wise - - w <- c(1, 2, 3) - AW <- A * w - bw <- b * w - - expect_equal(solve(AW, bw), solve_beta_(A, b, w, TRUE)) -})