--- title: "Randomized LASSO for a Gaussian linear model" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Randomized LASSO for a Gaussian linear model} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>", eval = requireNamespace("glmnet", quietly = TRUE)) ``` This vignette shows the basic workflow of `lassoinf`. We fit a LASSO to a randomized response, then use `LassoInference` for confidence intervals and p-values for the selected coefficients that are valid after selection. ## Randomization and the proportion used for selection In data splitting, a proportion $\pi$ of the data is used to select the model and the rest for inference. For the score $Z = X'y$, with variance $\Sigma = \sigma^2 X'X$, the score of a $\pi$ fraction of the data has variance $\Sigma / \pi$. Randomization instead selects with $Z_{noisy} = Z + \omega$, $\omega \sim N(0, \Omega)$. Matching $\text{Var}(Z_{noisy}) = \Sigma + \Omega = \Sigma / \pi$ gives $$ \Omega = \frac{1 - \pi}{\pi} \Sigma. $$ For least squares, this is the same as fitting the LASSO to $y_{noisy} = y + \epsilon$ with $\epsilon \sim N(0, \frac{1-\pi}{\pi}\sigma^2 I)$. As $\pi \to 1$ almost all the information is used for selection and little is left for inference. Smaller $\pi$ leaves more. ## Data We use $n = 100$ and $p = 20$, with one strong effect, two weaker ones and $\sigma^2 = 2$. Selection uses $\pi = 0.9$. ```{r} library(lassoinf) set.seed(0) n <- 100; p <- 20 signal_strength <- 4 X <- matrix(rnorm(n * p), n, p) beta <- numeric(p) beta[1:3] <- c(2, 1, -1) * signal_strength / sqrt(n) sigma2 <- 2 y <- drop(X %*% beta) + rnorm(n, sd = sqrt(sigma2)) ``` With $n > p$ we estimate $\sigma^2$ from the full least squares fit, and build $\Sigma$ and $\Omega$: ```{r} pi_selection <- 0.9 Q <- crossprod(X) Z_full <- drop(crossprod(X, y)) sigma2_hat <- sum(lm.fit(X, y)$residuals^2) / (n - p) Sigma <- sigma2_hat * Q sigma2_rand <- (1 - pi_selection) / pi_selection * sigma2_hat Sigma_noise <- sigma2_rand * Q y_noisy <- y + rnorm(n, sd = sqrt(sigma2_rand)) c(sigma2_hat = sigma2_hat, sigma2_rand = sigma2_rand) ``` ## Fit the LASSO on the randomized response We fit the LASSO $\frac{1}{2}\|y_{noisy} - Xb\|^2 + \lambda \|b\|_1$. glmnet scales the loss by $1/n$, so we pass it $\lambda / n$. `LassoInference` needs the solution, the gradient of the smooth part at it, $G = X'(X\hat{\beta} - y_{noisy})$, and its Hessian, $Q = X'X$. ```{r} lam <- 2 * sqrt(n) fit <- glmnet::glmnet(X, y_noisy, lambda = lam / n, intercept = FALSE, standardize = FALSE, control = list(thresh = 1e-14)) beta_hat <- as.numeric(coef(fit))[-1] G_hat <- drop(crossprod(X, X %*% beta_hat - y_noisy)) which(beta_hat != 0) ``` ## Inference The arguments are the solution, gradient and Hessian, the penalty weights $D$, the bounds $L$, $U$ (none here), the full-data score and its covariance, and the randomization covariance. ```{r} li <- LassoInference$new(beta_hat, G_hat, Q, rep(lam, p), NULL, NULL, Z_full, Sigma, Sigma_noise, level = 0.9) summ <- li$summary() summ ``` `index` is 0-based. `beta_hat` is the LASSO solution, which is shrunk toward zero and can lie outside its interval. The intervals are for the coefficients of the selected model, $\beta_E = (X_E'X_E)^{-1} X_E' \mu$ with $\mu = E[y]$. They come from the contrasts in `li$contrasts`, so we can compute the targets and check coverage: ```{r} Z_mu <- drop(Q %*% beta) summ$truth <- sapply(as.character(summ$index), function(j) sum(li$contrasts[[j]] * Z_mu)) summ$cover <- summ$lower_conf <= summ$truth & summ$truth <= summ$upper_conf summ ``` `summary()` also stores naive intervals, which ignore selection, and data-splitting intervals in `li$"_naive"` and `li$"_splitting"`: ```{r} li$`_naive` li$`_splitting` ``` ## Coverage over repeated experiments The randomized intervals should cover their targets 90% of the time. The naive intervals should not. ```{r} one_replication <- function() { X <- matrix(rnorm(n * p), n, p) y <- drop(X %*% beta) + rnorm(n, sd = sqrt(sigma2)) Q <- crossprod(X) sigma2_hat <- sum(lm.fit(X, y)$residuals^2) / (n - p) sigma2_rand <- (1 - pi_selection) / pi_selection * sigma2_hat y_noisy <- y + rnorm(n, sd = sqrt(sigma2_rand)) fit <- glmnet::glmnet(X, y_noisy, lambda = lam / n, intercept = FALSE, standardize = FALSE, control = list(thresh = 1e-14)) beta_hat <- as.numeric(coef(fit))[-1] if (all(beta_hat == 0)) return(NULL) G_hat <- drop(crossprod(X, X %*% beta_hat - y_noisy)) li <- LassoInference$new(beta_hat, G_hat, Q, rep(lam, p), NULL, NULL, drop(crossprod(X, y)), sigma2_hat * Q, sigma2_rand * Q, level = 0.9) summ <- li$summary() truth <- sapply(as.character(summ$index), function(j) sum(li$contrasts[[j]] * drop(Q %*% beta))) covers <- function(df) df$lower_conf <= truth & truth <= df$upper_conf data.frame(method = rep(c("randomized", "naive", "splitting"), each = nrow(summ)), cover = c(covers(summ), covers(li$`_naive`), covers(li$`_splitting`)), length = c(summ$upper_conf - summ$lower_conf, li$`_naive`$upper_conf - li$`_naive`$lower_conf, li$`_splitting`$upper_conf - li$`_splitting`$lower_conf)) } set.seed(1) results <- do.call(rbind, replicate(200, one_replication(), simplify = FALSE)) data.frame(coverage = tapply(results$cover, results$method, mean), median_length = tapply(results$length, results$method, median)) ``` The randomized intervals use the information that selection left over, so they are typically shorter than data splitting while keeping the nominal coverage.