Lasso Regression
The engine for Automated Parsimony. Lasso (L1 Regularization) audits vast fields of predictors, utilizing absolute-magnitude penalties to set irrelevant coefficients EXACTLY to zero—yielding the definitive 'Shortlist' of discovery.
What is it?
Lasso Regression (L1) adds a penalty equal to the absolute sum of coefficients. This shrinks some coefficients to exactly zero, performing automatic variable selection.
When to use it
- High Dimensionality: Predictor counts (P) are large, close to or exceeding N.
- Collinearity Check: Multi-variable dependencies skew standard OLS errors.
- Sparsity Preference: Prefer a model containing only key non-zero features.
Regularization Path
Observe how the 4 coefficients shrink from their raw OLS values (far left) as the regularization penalty increases:
Lasso Coefficient Path Laboratory
Increase the penalty lambda slider. Notice how coefficients shrink (Lasso hits exactly 0; Ridge decays asymptotically).
| Coefficient | OLS Raw Value | Shrunk Value |
|---|---|---|
| Beta 1 (Strong) | 70.0 | 52.00 |
| Beta 2 (Medium) | -45.0 | -27.00 |
| Beta 3 (Weak) | 25.0 | 7.00 |
| Beta 4 (Near Zero) | -5.0 | 0.00 |
Hypotheses
Pragmatic null and alternative hypotheses defined in mathematical notation.
H₀: β₁ = 0 (predictor has no effect and should be excluded from model)
Hₐ: β₁ ≠ 0 (predictor affects outcome and is selected by model)
Lasso regression is primarily used for prediction with automatic variable selection rather than hypothesis testing. Focus is on minimizing prediction error (MSE) via cross-validation while simultaneously selecting a sparse subset of relevant predictors. The L1 penalty λ||β||₁ = λ·Σ|βⱼ| has unique property: sets coefficients EXACTLY to zero (unlike ridge which only shrinks). This enables automatic feature selection: non-zero coefficients = selected variables. Key distinction from ridge: lasso performs SELECTION (some β exactly 0), ridge performs SHRINKAGE (all β ≠ 0 but small). Key distinction from elastic net: lasso arbitrarily picks one from correlated group, elastic net selects entire group.
Assumptions
The core mathematical criteria needed to ensure that statistical testing remains unbiased and valid.
Diagnostics
Checking residual plots and indices to examine model deviations and ensure standard error integrity.
- Cross-validation curve (CV error vs λ): verify minimum and λ selection
- Test set MSE or MAE (prediction error on held-out data)
- Number of selected variables vs λ: shows sparsity pattern
- Coefficient path plot (β vs λ): visualize when variables enter/exit model
- Selected variables list with non-zero coefficients at optimal λ
- Comparison to OLS and ridge regression (test MSE)
- Residual plots (residuals vs fitted, Q-Q plot) on test set
- R² on train vs test: check for overfitting (large gap indicates issue)
- Selection stability across CV folds: check agreement of selected variables
- Variable importance ranking by absolute coefficient magnitude
- Coefficient comparison: λ.min (best prediction) vs λ.1se (simplest within 1 SE)
- Prediction plots: predicted vs observed on test set
- λ.min vs λ.1se model comparison: number of variables, prediction performance
- Stability selection analysis (if high-dimensional): bootstrap selected variables
- True positive and false positive rates (if truth known from simulation)
- Correlation matrix of selected predictors (check if lasso picked arbitrarily from correlated groups)
Applied Minds
Review concrete study examples, data layout guidelines, and copy executable syntax scripts.
Predicting Customer Churn from High-Dimensional Features
Research question: Predict customer churn (continuous propensity score) from large feature set with many irrelevant predictors. Design: N=500 customers, p=200 features (demographics, usage patterns, service interactions, engagement metrics). Most features are noise (sparse truth: only 15-20 truly matter). Outcome: Churn propensity score (0-100, continuous). Goal: Identify key churn drivers AND predict accurately. Demonstrate lasso's advantage: automatic variable selection (interpretable model with few features) vs OLS (unstable, p approaching n), ridge (keeps all 200 features, uninterpretable), and elastic net (for comparison).
# Lasso Regression Example 1: Customer Churn Prediction with High-Dimensional Features
# Demonstrate automatic variable selection and compare to OLS, Ridge, Elastic Net
library(glmnet) # Lasso, ridge, elastic net
library(caret) # Cross-validation
library(ggplot2) # Visualization
library(dplyr) # Data manipulation
library(reshape2) # Data reshaping
library(pheatmap) # Heatmaps
set.seed(2025)
# === STEP 1: Simulate Customer Churn Data (High-Dimensional, Sparse) ===
n <- 500 # Customers
p <- 200 # Features
cat("=== Simulating Customer Churn Data ===")
cat("\nGenerating", n, "customers with", p, "features...\n")
# Create predictor matrix with moderate correlations
# (Not block-structured like gene data; more diffuse correlations like customer features)
cor_matrix <- matrix(0.15, p, p) # Weak background correlation
diag(cor_matrix) <- 1
# Add stronger correlation for some feature groups (e.g., usage metrics correlated)
for (i in 1:10) {
group_start <- (i-1) * 20 + 1
group_end <- min(i * 20, p)
if (group_end <= p) {
cor_matrix[group_start:group_end, group_start:group_end] <-
cor_matrix[group_start:group_end, group_start:group_end] + 0.25
}
}
cor_matrix[cor_matrix > 1] <- 0.95
diag(cor_matrix) <- 1
# Generate correlated predictors
library(MASS)
X <- mvrnorm(n, mu=rep(0, p), Sigma=cor_matrix)
colnames(X) <- paste0("Feature_", 1:p)
# True coefficients: SPARSE (only 18 out of 200 non-zero)
true_beta <- rep(0, p)
# Select 18 relevant features randomly
relevant_features <- sort(sample(1:p, 18))
# Assign non-zero coefficients with varying magnitudes
true_beta[relevant_features[1:6]] <- rnorm(6, mean=8, sd=2) # Strong positive
true_beta[relevant_features[7:12]] <- rnorm(6, mean=-6, sd=2) # Strong negative
true_beta[relevant_features[13:18]] <- rnorm(6, mean=3, sd=1) # Moderate positive
cat("\n=== True Model Sparsity ===")
cat("\nTotal features:", p)
cat("\nTrue non-zero coefficients:", sum(true_beta != 0), "(",
round(sum(true_beta != 0)/p*100, 1), "% - SPARSE)")
cat("\nRelevant features:", paste(relevant_features, collapse=", "), "\n")
# Generate outcome with noise
y <- X %*% true_beta + rnorm(n, mean=50, sd=15)
y <- as.vector(y)
# Scale outcome to 0-100 range (churn propensity score)
y <- (y - min(y)) / (max(y) - min(y)) * 100
data <- data.frame(y = y, X)
cat("\n=== Data Simulation Complete ===")
cat("\nOutcome range: [", round(min(y), 1), ",", round(max(y), 1), "]")
cat("\nOutcome mean:", round(mean(y), 2))
cat("\nOutcome SD:", round(sd(y), 2), "\n")
# === STEP 2: Train-Test Split ===
set.seed(123)
train_idx <- sample(1:n, size=0.7*n)
train_data <- data[train_idx, ]
test_data <- data[-train_idx, ]
cat("\n=== Train-Test Split ===")
cat("\nTraining set:", nrow(train_data), "observations")
cat("\nTest set:", nrow(test_data), "observations")
cat("\nRatio p/n(training):", round(p / nrow(train_data), 2), "\n")
# Prepare matrices for glmnet
X_train <- as.matrix(train_data[, -1])
y_train <- train_data$y
X_test <- as.matrix(test_data[, -1])
y_test <- test_data$y
# === STEP 3: OLS Regression (Baseline - will struggle with high p/n) ===
cat("\n=== OLS Regression(Baseline) ===")
if (p < nrow(train_data)) {
model_ols <- lm(y ~ ., data=train_data)
pred_ols_test <- predict(model_ols, newdata=test_data)
mse_ols_test <- mean((y_test - pred_ols_test)^2)
rmse_ols_test <- sqrt(mse_ols_test)
r2_ols_test <- 1 - mse_ols_test / var(y_test)
cat("\nOLS Test MSE:", round(mse_ols_test, 2))
cat("\nOLS Test RMSE:", round(rmse_ols_test, 2))
cat("\nOLS Test R²:", round(r2_ols_test, 3))
cat("\nOLS uses all", p, "predictors(no selection)\n")
} else {
cat("\nOLS cannot be fit(p >= n). This demonstrates need for regularization.\n")
mse_ols_test <- NA
rmse_ols_test <- NA
r2_ols_test <- NA
}
# === STEP 4: Ridge Regression (Baseline for comparison) ===
cat("\n=== Ridge Regression(α = 0, no variable selection) ===")
cv_ridge <- cv.glmnet(X_train, y_train,
alpha=0, # Ridge (L2 penalty)
nfolds=10, # 10-fold CV
standardize=TRUE) # Auto-standardize
pred_ridge <- predict(cv_ridge, newx=X_test, s="lambda.min")
mse_ridge <- mean((y_test - pred_ridge)^2)
rmse_ridge <- sqrt(mse_ridge)
r2_ridge <- 1 - mse_ridge / var(y_test)
ridge_coef <- coef(cv_ridge, s="lambda.min")[-1] # Exclude intercept
n_selected_ridge <- sum(abs(ridge_coef) > 1e-10)
cat("\nOptimal λ:", round(cv_ridge$lambda.min, 4))
cat("\nTest MSE:", round(mse_ridge, 2))
cat("\nTest RMSE:", round(rmse_ridge, 2))
cat("\nTest R²:", round(r2_ridge, 3))
cat("\n'Selected' variables:", n_selected_ridge, "(Ridge keeps all, just shrunk)\n")
# === STEP 5: Lasso Regression (α = 1, automatic variable selection) ===
cat("\n=== Lasso Regression(α = 1, L1 PENALTY) ===")
cat("\nPerforming 10-fold cross-validation for λ selection...\n")
cv_lasso <- cv.glmnet(X_train, y_train,
alpha=1, # Lasso (L1 penalty)
nfolds=10,
standardize=TRUE)
cat("\n=== Cross-Validation Results ===")
cat("\nλ that minimizes CV error(lambda.min):", round(cv_lasso$lambda.min, 4))
cat("\nλ within 1 SE(lambda.1se, simpler model):", round(cv_lasso$lambda.1se, 4))
cat("\nMin CV MSE:", round(min(cv_lasso$cvm), 2))
cat("\nCV MSE at lambda.1se:", round(cv_lasso$cvm[cv_lasso$lambda == cv_lasso$lambda.1se], 2), "\n")
# Plot CV curve
plot(cv_lasso, main="Lasso: Cross-Validation Curve")
abline(v=log(cv_lasso$lambda.min), col="red", lty=2, lwd=2)
abline(v=log(cv_lasso$lambda.1se), col="blue", lty=2, lwd=2)
legend("topright",
legend=c("λ.min(best prediction)", "λ.1se (simplest model)"),
col=c("red", "blue"), lty=2, lwd=2, cex=0.8)
# === STEP 6: Extract Lasso Coefficients and Selected Variables ===
# Coefficients at lambda.min
lasso_coef_min <- coef(cv_lasso, s="lambda.min")[-1] # Exclude intercept
selected_min <- which(lasso_coef_min != 0)
n_selected_min <- length(selected_min)
cat("\n=== Lasso Variable Selection(λ = lambda.min) ===")
cat("\nSelected variables:", n_selected_min, "out of", p, "(",
round(n_selected_min/p*100, 1), "%)")
cat("\nSelected feature indices:", paste(head(selected_min, 20), collapse=", "))
if (n_selected_min > 20) cat("... (showing first 20)")
cat("\n")
# Coefficients at lambda.1se (simpler model)
lasso_coef_1se <- coef(cv_lasso, s="lambda.1se")[-1]
selected_1se <- which(lasso_coef_1se != 0)
n_selected_1se <- length(selected_1se)
cat("\n=== Lasso Variable Selection(λ = lambda.1se, more sparse) ===")
cat("\nSelected variables:", n_selected_1se, "out of", p, "(",
round(n_selected_1se/p*100, 1), "%)")
cat("\nSelected feature indices:", paste(head(selected_1se, 20), collapse=", "))
if (n_selected_1se > 20) cat("... (showing first 20)")
cat("\n")
# === STEP 7: Lasso Predictions on Test Set ===
pred_lasso_min <- predict(cv_lasso, newx=X_test, s="lambda.min")
mse_lasso_min <- mean((y_test - pred_lasso_min)^2)
rmse_lasso_min <- sqrt(mse_lasso_min)
r2_lasso_min <- 1 - mse_lasso_min / var(y_test)
cat("\n=== Lasso Performance(Test Set, λ = lambda.min) ===")
cat("\nMSE:", round(mse_lasso_min, 2))
cat("\nRMSE:", round(rmse_lasso_min, 2))
cat("\nR²:", round(r2_lasso_min, 3))
pred_lasso_1se <- predict(cv_lasso, newx=X_test, s="lambda.1se")
mse_lasso_1se <- mean((y_test - pred_lasso_1se)^2)
rmse_lasso_1se <- sqrt(mse_lasso_1se)
r2_lasso_1se <- 1 - mse_lasso_1se / var(y_test)
cat("\n=== Lasso Performance(Test Set, λ = lambda.1se) ===")
cat("\nMSE:", round(mse_lasso_1se, 2))
cat("\nRMSE:", round(rmse_lasso_1se, 2))
cat("\nR²:", round(r2_lasso_1se, 3), "\n")
# === STEP 8: Elastic Net (for comparison) ===
cat("\n=== Elastic Net(α = 0.5, for comparison) ===")
cv_elastic <- cv.glmnet(X_train, y_train, alpha=0.5, nfolds=10, standardize=TRUE)
pred_elastic <- predict(cv_elastic, newx=X_test, s="lambda.min")
mse_elastic <- mean((y_test - pred_elastic)^2)
rmse_elastic <- sqrt(mse_elastic)
r2_elastic <- 1 - mse_elastic / var(y_test)
elastic_coef <- coef(cv_elastic, s="lambda.min")[-1]
n_selected_elastic <- sum(elastic_coef != 0)
cat("\nOptimal λ:", round(cv_elastic$lambda.min, 4))
cat("\nTest MSE:", round(mse_elastic, 2))
cat("\nTest RMSE:", round(rmse_elastic, 2))
cat("\nTest R²:", round(r2_elastic, 3))
cat("\nSelected variables:", n_selected_elastic, "\n")
# === STEP 9: Model Comparison ===
cat("\n", "="*70, "\n")
cat("=== MODEL COMPARISON(Test Set) ===")
cat("\n", "="*70, "\n")
if (!is.na(mse_ols_test)) {
comparison <- data.frame(
Model = c("OLS", "Ridge(α=0)", "Lasso(α=1, λ.min)",
"Lasso(α=1, λ.1se)", "Elastic Net(α=0.5)"),
MSE = c(mse_ols_test, mse_ridge, mse_lasso_min, mse_lasso_1se, mse_elastic),
RMSE = c(rmse_ols_test, rmse_ridge, rmse_lasso_min, rmse_lasso_1se, rmse_elastic),
R2 = c(r2_ols_test, r2_ridge, r2_lasso_min, r2_lasso_1se, r2_elastic),
N_Selected = c(p, p, n_selected_min, n_selected_1se, n_selected_elastic)
)
} else {
comparison <- data.frame(
Model = c("Ridge(α=0)", "Lasso(α=1, λ.min)",
"Lasso(α=1, λ.1se)", "Elastic Net(α=0.5)"),
MSE = c(mse_ridge, mse_lasso_min, mse_lasso_1se, mse_elastic),
RMSE = c(rmse_ridge, rmse_lasso_min, rmse_lasso_1se, rmse_elastic),
R2 = c(r2_ridge, r2_lasso_min, r2_lasso_1se, r2_elastic),
N_Selected = c(p, n_selected_min, n_selected_1se, n_selected_elastic)
)
}
print(comparison)
best_model <- comparison$Model[which.min(comparison$MSE)]
cat("\n*** Best Model(Lowest Test MSE):", best_model, "***")
cat("\nLowest Test MSE:", round(min(comparison$MSE), 2))
cat("\n\nKey Insight: Lasso achieves SPARSITY(few variables) with competitive/superior prediction\n")
# === STEP 10: Variable Selection Quality Analysis ===
cat("\n=== Variable Selection Quality ===")
cat("\nComparing selected variables to true relevant features...\n")
# True positives and false positives (lambda.min)
tp_min <- length(intersect(selected_min, relevant_features))
fp_min <- length(setdiff(selected_min, relevant_features))
fn_min <- length(setdiff(relevant_features, selected_min))
if (n_selected_min > 0) {
precision_min <- tp_min / n_selected_min
recall_min <- tp_min / length(relevant_features)
f1_min <- 2 * precision_min * recall_min / (precision_min + recall_min)
} else {
precision_min <- 0
recall_min <- 0
f1_min <- 0
}
cat("\n=== Lasso Selection Quality(λ = lambda.min) ===")
cat("\nTrue positives:", tp_min, "/", length(relevant_features), "truly relevant")
cat("\nFalse positives:", fp_min)
cat("\nFalse negatives:", fn_min)
cat("\nPrecision:", round(precision_min, 3), "(proportion of selected that are truly relevant)")
cat("\nRecall:", round(recall_min, 3), "(proportion of truly relevant that were selected)")
cat("\nF1 Score:", round(f1_min, 3), "\n")
# True positives and false positives (lambda.1se)
tp_1se <- length(intersect(selected_1se, relevant_features))
fp_1se <- length(setdiff(selected_1se, relevant_features))
fn_1se <- length(setdiff(relevant_features, selected_1se))
if (n_selected_1se > 0) {
precision_1se <- tp_1se / n_selected_1se
recall_1se <- tp_1se / length(relevant_features)
f1_1se <- 2 * precision_1se * recall_1se / (precision_1se + recall_1se)
} else {
precision_1se <- 0
recall_1se <- 0
f1_1se <- 0
}
cat("\n=== Lasso Selection Quality(λ = lambda.1se) ===")
cat("\nTrue positives:", tp_1se, "/", length(relevant_features))
cat("\nFalse positives:", fp_1se)
cat("\nFalse negatives:", fn_1se)
cat("\nPrecision:", round(precision_1se, 3))
cat("\nRecall:", round(recall_1se, 3))
cat("\nF1 Score:", round(f1_1se, 3), "\n")
# === STEP 11: Coefficient Path Plot (Lasso Trace) ===
cat("\n=== Generating Coefficient Path Plot ===")
# Fit lasso across sequence of λ values
lasso_path <- glmnet(X_train, y_train, alpha=1, standardize=TRUE)
plot(lasso_path, xvar="lambda", label=TRUE,
main="Lasso: Coefficient Paths(Variable Selection)",
xlab="Log(λ)", ylab="Standardized Coefficients")
abline(v=log(cv_lasso$lambda.min), col="red", lty=2, lwd=2)
abline(v=log(cv_lasso$lambda.1se), col="blue", lty=2, lwd=2)
abline(h=0, col="gray", lty=1)
legend("topright", legend=c("λ.min", "λ.1se"), col=c("red", "blue"), lty=2, lwd=2, cex=0.8)
cat("\nAs λ increases(moving right), coefficients are set to EXACTLY zero")
cat("\nVariables 'exit' the model one by one(unlike ridge where all stay non-zero)\n")
# Plot number of selected variables vs lambda
df_lambda <- data.frame(
lambda = lasso_path$lambda,
n_vars = lasso_path$df
)
ggplot(df_lambda, aes(x=log(lambda), y=n_vars)) +
geom_line(linewidth=1.2, color="darkblue") +
geom_vline(xintercept=log(cv_lasso$lambda.min), linetype="dashed", color="red", linewidth=1) +
geom_vline(xintercept=log(cv_lasso$lambda.1se), linetype="dashed", color="blue", linewidth=1) +
labs(title="Lasso: Number of Selected Variables vs Log(λ)",
subtitle="Red: λ.min, Blue: λ.1se",
x="Log(λ)", y="Number of Non-Zero Coefficients") +
theme_classic() +
theme(plot.title = element_text(size=14, face="bold"))
# === STEP 12: Visualizations ===
# Model Comparison Bar Plot
comparison_plot <- comparison
ggplot(comparison_plot, aes(x=reorder(Model, MSE), y=MSE, fill=Model)) +
geom_bar(stat="identity", alpha=0.7, color="black") +
geom_text(aes(label=round(MSE, 1)), vjust=-0.5, size=3.5) +
labs(title="Model Comparison: Test MSE",
subtitle="Lower is better",
x="Model", y="Mean Squared Error") +
theme_classic() +
theme(legend.position="none",
plot.title = element_text(size=14, face="bold"),
axis.text.x = element_text(angle=25, hjust=1))
# Number of selected variables comparison
ggplot(comparison_plot, aes(x=reorder(Model, -N_Selected), y=N_Selected, fill=Model)) +
geom_bar(stat="identity", alpha=0.7, color="black") +
geom_text(aes(label=N_Selected), vjust=-0.5, size=3.5) +
labs(title="Model Comparison: Number of Selected Variables",
subtitle="Lasso achieves sparsity",
x="Model", y="Number of Selected Features") +
scale_y_continuous(limits=c(0, p*1.1)) +
theme_classic() +
theme(legend.position="none",
plot.title = element_text(size=14, face="bold"),
axis.text.x = element_text(angle=25, hjust=1))
# Predicted vs Observed (Lasso lambda.min)
results_df <- data.frame(
Observed = y_test,
Predicted = as.vector(pred_lasso_min)
)
ggplot(results_df, aes(x=Observed, y=Predicted)) +
geom_point(alpha=0.6, size=2.5, color="steelblue") +
geom_abline(slope=1, intercept=0, linetype="dashed", color="red", linewidth=1) +
labs(title="Lasso: Predicted vs Observed Churn Propensity(Test Set)",
subtitle=paste0("λ = ", round(cv_lasso$lambda.min, 4), ", ",
n_selected_min, " variables selected"),
x="Observed Churn Propensity", y="Predicted Churn Propensity") +
theme_classic() +
theme(plot.title = element_text(size=13, face="bold"))
# Coefficient comparison: True vs Lasso
coef_compare <- data.frame(
Feature = paste0("F", 1:p),
True = true_beta,
Lasso_min = as.vector(lasso_coef_min),
Lasso_1se = as.vector(lasso_coef_1se)
)
# Plot for top features by true coefficient magnitude
top_features <- order(abs(coef_compare$True), decreasing=TRUE)[1:30]
coef_top <- coef_compare[top_features, ]
coef_top_long <- melt(coef_top, id.vars="Feature")
ggplot(coef_top_long, aes(x=reorder(Feature, -abs(value)), y=value, fill=variable)) +
geom_bar(stat="identity", position="dodge", alpha=0.7) +
scale_fill_manual(values=c("True"="gold", "Lasso_min"="steelblue",
"Lasso_1se"="darkgreen"),
name="Model",
labels=c("True", "Lasso(λ.min)", "Lasso(λ.1se)")) +
labs(title="Coefficient Comparison: Top 30 Features by True Magnitude",
x="Feature", y="Coefficient Value") +
theme_classic() +
theme(axis.text.x = element_text(angle=90, hjust=1, vjust=0.5, size=7),
plot.title = element_text(size=12, face="bold"))
# === STEP 13: Selection Stability Analysis ===
cat("\n=== Selection Stability Analysis(Bootstrap) ===")
cat("\nAssessing stability of variable selection across 50 bootstrap samples...\n")
n_boot <- 50
selected_boot <- matrix(0, nrow=n_boot, ncol=p)
for (b in 1:n_boot) {
boot_idx <- sample(1:nrow(X_train), replace=TRUE)
X_boot <- X_train[boot_idx, ]
y_boot <- y_train[boot_idx]
cv_boot <- cv.glmnet(X_boot, y_boot, alpha=1, nfolds=5, standardize=TRUE)
coef_boot <- coef(cv_boot, s="lambda.min")[-1]
selected_boot[b, which(coef_boot != 0)] <- 1
}
# Calculate selection frequency
selection_freq <- colMeans(selected_boot)
stable_vars <- which(selection_freq >= 0.8) # Selected in >=80% of boots
cat("\nStable variables(selected in ≥80% of bootstrap samples):", length(stable_vars))
cat("\nStable variable indices:", paste(head(stable_vars, 20), collapse=", "))
if (length(stable_vars) > 20) cat("... (showing first 20)")
# Check overlap with true relevant features
stable_tp <- length(intersect(stable_vars, relevant_features))
cat("\n\nStable variables that are truly relevant:", stable_tp, "/", length(stable_vars))
cat("\nStability-based precision:", round(stable_tp / max(length(stable_vars), 1), 3), "\n")
# Plot selection frequency
freq_df <- data.frame(
Feature = 1:p,
Frequency = selection_freq,
TrueRelevant = ifelse(1:p %in% relevant_features, "Yes", "No")
)
ggplot(freq_df, aes(x=Feature, y=Frequency, color=TrueRelevant)) +
geom_point(alpha=0.6, size=1.5) +
geom_hline(yintercept=0.8, linetype="dashed", color="red", linewidth=1) +
scale_color_manual(values=c("Yes"="red", "No"="gray50")) +
labs(title="Lasso: Variable Selection Frequency Across 50 Bootstrap Samples",
subtitle="Red line: 80% threshold for 'stable' selection",
x="Feature Index", y="Selection Frequency",
color="Truly Relevant?") +
theme_classic() +
theme(plot.title = element_text(size=12, face="bold"))
# === STEP 14: Residual Diagnostics ===
cat("\n=== Residual Diagnostics(Lasso, Test Set) ===")
residuals_lasso <- y_test - pred_lasso_min
par(mfrow=c(2,2))
# Residuals vs Fitted
plot(pred_lasso_min, residuals_lasso,
main="Lasso: Residuals vs Fitted(Test Set)",
xlab="Fitted Values", ylab="Residuals",
pch=19, col=rgb(0,0,1,0.5))
abline(h=0, col="red", lty=2, lwd=2)
lines(lowess(pred_lasso_min, residuals_lasso), col="blue", lwd=2)
# Q-Q Plot
qqnorm(residuals_lasso, main="Lasso: Q-Q Plot(Test Set)",
pch=19, col=rgb(0,0,1,0.5))
qqline(residuals_lasso, col="red", lwd=2)
# Residuals histogram
hist(residuals_lasso, breaks=20, main="Lasso: Residual Distribution",
xlab="Residuals", col="lightblue", border="black")
curve(dnorm(x, mean=mean(residuals_lasso), sd=sd(residuals_lasso)) *
length(residuals_lasso) * diff(hist(residuals_lasso, plot=FALSE)$breaks)[1],
add=TRUE, col="red", lwd=2)
# Scale-location
plot(pred_lasso_min, sqrt(abs(residuals_lasso)),
main="Lasso: Scale-Location(Test Set)",
xlab="Fitted Values", ylab="√|Residuals|",
pch=19, col=rgb(0,0,1,0.5))
abline(h=mean(sqrt(abs(residuals_lasso))), col="red", lty=2, lwd=2)
lines(lowess(pred_lasso_min, sqrt(abs(residuals_lasso))), col="blue", lwd=2)
par(mfrow=c(1,1))
cat("\nResidual diagnostics show approximately normal errors(Q-Q plot)")
cat("\nNo systematic patterns in residuals vs fitted(random scatter)\n")
# === APA-Style Reporting ===
cat("\n" , "="*70, "\n")
cat("=== APA-STYLE REPORT ===")
cat("\n", "="*70, "\n")
cat("\nLasso regression(L1 regularization) was used to predict customer churn\n")
cat("propensity from", p, "features(N =", n, "customers, 70/30 train/test split).\n")
cat("The high-dimensional feature set(p/n =", round(p/nrow(train_data), 2), ") exhibited\n")
cat("moderate correlations(mean |r| = 0.25) with an underlying sparse structure\n")
cat("(only", length(relevant_features), "truly relevant features out of", p, ").\n")
cat("\n")
cat("The lasso penalty parameter λ was selected via 10-fold cross-validation\n")
cat("on the training set(λ.min =", round(cv_lasso$lambda.min, 4), "for optimal\n")
cat("prediction; λ.1se =", round(cv_lasso$lambda.1se, 4), "for the simplest model\n")
cat("within 1 SE of minimum). Lasso achieved superior test set performance\n")
cat("(MSE =", round(mse_lasso_min, 2), ", RMSE =", round(rmse_lasso_min, 2), ",\n")
cat("R² =", round(r2_lasso_min, 3), ") compared to ridge regression(MSE =",
round(mse_ridge, 2), ")")
if (!is.na(mse_ols_test)) {
cat(" and OLS(MSE =", round(mse_ols_test, 2), ")")
}
cat(".\n")
cat("\n")
cat("Critically, lasso performed automatic variable selection, identifying\n")
cat(n_selected_min, "relevant features(", round(n_selected_min/p*100, 1), "% of total)\n")
cat("while setting", p - n_selected_min, "coefficients exactly to zero(L1 penalty\n")
cat("property). Variable selection quality analysis showed precision =",
round(precision_min, 3), "\n")
cat("and recall =", round(recall_min, 3), ", successfully identifying",
tp_min, "out of", length(relevant_features), "\n")
cat("truly relevant features. Selection stability analysis(50 bootstrap samples)\n")
cat("identified", length(stable_vars), "stable features(selected in ≥80% of samples),\n")
cat("with", stable_tp, "overlapping with the true relevant set.\n")
cat("\n")
cat("The λ.1se model(1 SE rule) provided an even sparser alternative with\n")
cat(n_selected_1se, "selected features(MSE =", round(mse_lasso_1se, 2), "), trading\n")
cat("minimal prediction accuracy(", round((mse_lasso_1se - mse_lasso_min)/mse_lasso_min*100, 1),
"% increase)\n")
cat("for substantially improved interpretability(",
round((n_selected_min - n_selected_1se)/n_selected_min*100, 1), "% fewer variables).\n")
cat("\n")
cat("Residual diagnostics on the test set showed approximately normal errors\n")
cat("(Shapiro-Wilk p = 0.XX) with no systematic patterns in residuals vs fitted\n")
cat("values, validating model assumptions. Lasso regression successfully combined\n")
cat("accurate prediction with automatic feature selection, producing an interpretable\n")
cat("model that identified key churn drivers while avoiding overfitting in the\n")
cat("high-dimensional setting. This demonstrates lasso's primary advantage: achieving\n")
cat("sparsity(few variables) through the L1 penalty's unique property of setting\n")
cat("coefficients EXACTLY to zero, unlike ridge which only shrinks coefficients.\n")
cat("\n", "="*70, "\n")Lasso successfully performs automatic variable selection in high-dimensional setting (p=200, n=500, p/n=0.4) by setting coefficients EXACTLY to zero via L1 penalty. Key finding: Selected only 25-40 variables (12-20% of total) while achieving test MSE competitive with or better than ridge (which uses all 200 variables). Selection quality: precision ~0.6-0.8 (most selected variables are truly relevant), recall ~0.7-0.9 (captures most truly relevant variables). Critical advantage over ridge: interpretable sparse model (25 features vs 200). Critical advantage over OLS: handles high p/n ratio without overfitting. Lambda.min (best prediction) vs lambda.1se (simplest within 1 SE): trade-off between prediction accuracy and sparsity. Stability analysis: ~60-80% of selected variables stable across bootstrap samples (high stability = reliable selection). Common pattern: lasso identifies 15-20 strong signals robustly, plus 5-10 weak signals variably. For customer churn: lasso pinpoints key drivers (e.g., support_tickets, tenure, usage_frequency) while excluding noise features, enabling actionable business insights.
Alternatives
Structured fallback pathways for choosing alternative tests when normality or slopes requirements fail.
- Elastic Net — The mandatory pivot when predictors are highly correlated (L1 + L2 hybrid).
- Ridge Regression — Switch to L2 only if you require all predictors to remain in the equation.
- Stability Selection — Resample the data to find features that survive in >80% of trials.
- Knockoff Filter — Utilize the knockoff framework to protect against false discovery in high-D grids.
- GAM-Lasso — Incorporate smoothing splines into the penalized GLM framework.
- Kernel Lasso — Map the features into high-dimensional space to capture curved associations.
Post-hoc
Group mean comparisons and correction controls (e.g. Tukey HSD, Bonferroni) to protect against Family-Wise Error Rates.
Post-hoc pairwise tests defined for this model.
No specific guidelines provided.
Effect Size
Understanding effect sizes (e.g., Cohen's d, Partial Eta-Squared) and clinical impact benchmarks.
Primary metric: compare lasso test MSE to OLS, ridge, and elastic net. Lower is better. Improvement of 10-40% over OLS typical when p/n > 0.5 and sparse truth. Lasso may be similar to or slightly worse than ridge if low sparsity (many true non-zeros).
Test R² more honest than train R². Lasso train R² ≤ OLS train R² (regularization bias), but lasso test R² often > OLS test R² (better generalization from selection). Compare to ridge: similar R² but lasso achieves with far fewer variables.
Number of selected variables (non-zero coefficients) is KEY metric for lasso. Sparsity ratio = n_selected / p. For p=200, lasso might select 20-50 (10-25% sparsity). Lower sparsity = more interpretable model. Compare lambda.min (more variables, better prediction) vs lambda.1se (fewer variables, simpler model).
Precision = TP / (TP + FP) (proportion of selected variables that are truly relevant). Recall = TP / (TP + FN) (proportion of truly relevant variables that were selected). Requires known truth (simulations or validation). Typical: precision 0.6-0.9, recall 0.5-0.8. Higher precision = fewer false discoveries. Higher recall = fewer missed signals. F1 score = harmonic mean of precision and recall.
0.2
0.5
0.8
Sample Size
Guidelines for minimum sample requirements and power analysis parameters.
The 'Signal-to-Noise' Minimum: A minimum of 5 participants per potential predictor is required. Lasso math thrives in high-dimensional grids but collapses if the N is so small that the algorithm 'Chokes' on every signal.
| Effect Size | Parameters | Required n |
|---|---|---|
| Small Effect | Signal-to-Noise = 0.5 (Small) | n ≈ 500 total |
| Medium Effect | Signal-to-Noise = 1.5 (Medium) | n ≈ 120 total |
| Large Effect | Signal-to-Noise = 3.0 (Large) | n ≈ 60 total |
The 'Stability Strike': Lasso power is meaningless if the selection changes with every new participant. Utilize 'Stability Selection' or bootstrapping to audit the consistency of your features before claiming discovery.
Reporting
How to compile statistical results into publication prose matching APA and journal style guides.
Lasso regression (L1 regularization) was used to predict outcome from p predictors (N = n, train/test split or k-fold CV). The data exhibited evidence of sparsity: domain knowledge or preliminary analysis suggesting many predictors irrelevant. The penalty parameter λ was selected via k-fold cross-validation on the training set (optimal λ = value; λ.1se = value for 1 SE rule). Lasso achieved test set MSE = value (RMSE = value, R² = value), outperforming OLS/ridge (MSE = value, percent% improvement). Critically, lasso performed automatic variable selection, identifying n_selected relevant predictors (percent% of total) while setting n_zero coefficients exactly to zero (L1 penalty property). If truth known: Variable selection quality showed precision = [value, recall = value, successfully identifying n_tp out of n_true truly relevant predictors.] If stability analysis: Selection stability analysis ([n_boot bootstrap samples) identified n_stable stable variables (selected in ≥80% of samples).] Lambda.min vs lambda.1se: The λ.1se model provided a sparser alternative with [n_selected_1se variables (MSE = value), trading percent% prediction accuracy for percent% fewer variables.] Residual diagnostics showed results. Lasso regression successfully combined accurate prediction with automatic feature selection, producing an interpretable sparse model ideal for application context.
- Sample size (n) and number of predictors (p)
- Train/test split or CV scheme
- Evidence of sparsity assumption
- Optimal λ (and λ.1se) via CV
- Test set MSE, RMSE, R²
- Comparison to OLS and/or ridge (percent improvement)
- Number of selected variables (and sparsity ratio %)
- List of selected variables or top N by importance
- Selection quality if known truth (precision, recall, F1)
- Selection stability if reported (bootstrap or CV folds)
- Lambda.min vs lambda.1se comparison
- Cross-validation performance curve
Manuscript Lab
Copy standard summary tables and forensic reporting grids to outline analysis details.
| Predictor | OLS Estimate | Lasso Estimate (λ_min) | Status |
|---|---|---|---|
| Biomarker 01 | 1.42 | 1.15 | Retained |
| Biomarker 02 | 0.85 | 0.00 | ELIMINATED |
| Biomarker 03 | -0.45 | -0.22 | Retained |
| Biomarker 04 | 0.12 | 0.00 | ELIMINATED |
The 'Selector'. Adds a penalty to the model that forces unimportant coefficients to exactly ZERO, effectively performing automatic variable selection.
The 'Pressure' Parameter. Controls the strength of the penalty. Higher λ results in more zeros (simpler model).
Command Center
Syntax libraries and function parameters for executing calculations in stats packages.
# 1. Fit Lasso with Cross-Validation
cv_model <- glmnet::cv.glmnet(X, y, alpha = 1)
# 2. Extract Coefficients at Optimal Lambda
coef(cv_model, s = 'lambda.min')
# 3. Visualize Path of Shrinkage
plot(cv_model)Lasso is the ultimate lie detector for 'P-Hacking'. By penalizing coefficients, it ensures only the most robust predictors survive, preventing you from chasing noise.
# Compare Lambda.min vs Lambda.1se
# 1se gives a more parsimonious (simpler) model that is within 1 standard error of the minimum.Common Mistakes
Analytical caveats and corrections to maintain modeling integrity.
References
Scholarly lineage and citation keys grounding the statistical framework.