Regularization

App to demonstrate how regularized regression works
Regression
Author

Haley Grant

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 600
#| 
library(shiny)
library(plotly)
library(glmnet)

set.seed(123)
n <- 100
X <- matrix(rnorm(n * 2), ncol = 2)
colnames(X) <- c("x1", "x2")
beta_true <- c(3, -2)
y <- as.vector(X %*% beta_true + rnorm(n))

ui <- fluidPage(
  titlePanel("Penalized Regression"),
  sidebarLayout(
    sidebarPanel(
      sliderInput("lambda", "Lambda (penalty strength)", min = 0.01, max = 3, value = 1, step = 0.01),
      sliderInput("alpha", "Alpha (0 = Ridge, 1 = Lasso)", min = 0, max = 1, value = 1, step = 0.05)
    ),
    mainPanel(
      plotlyOutput("msePlot", height = "700px", width = "100%")
    )
  )
)

server <- function(input, output) {
  output$msePlot <- renderPlotly({
    lambda <- input$lambda
    alpha <- input$alpha
    
    beta1_seq <- seq(beta_true[1] - 5, beta_true[1] + 5, length.out = 50)
    beta2_seq <- seq(beta_true[2] - 5, beta_true[2] + 5, length.out = 50)
    grid <- expand.grid(beta1 = beta1_seq, beta2 = beta2_seq)
    
    # Calculate MSE with NO intercept for each beta pair
    grid$MSE <- apply(grid, 1, function(b) {
      preds <- X %*% as.numeric(b)
      mean((y - preds)^2)
    })
    
    z_matrix <- matrix(grid$MSE, nrow = length(beta1_seq), byrow = FALSE)
    
    # Fit OLS with no intercept
    ols_fit <- lm(y ~ X - 1)
    beta_ols <- coef(ols_fit)
    mse_min <- min(grid$MSE)
    mse_max <- max(grid$MSE)
    z_vals <- seq(mse_min, mse_max, length.out = 50)
    
    # Fit glmnet penalized model with no intercept
    fit <- glmnet(X, y, alpha = alpha, lambda = lambda, intercept = FALSE, standardize = TRUE)
    beta_pen <- as.numeric(coef(fit))[-1]
    mse_pen <- mean((y - X %*% beta_pen)^2)
    
    # Compute penalty level 't' from the penalized solution (not scaled by n)
    L1_norm <- sum(abs(beta_pen))
    L2_norm2 <- sum(beta_pen^2)
    t <- alpha * L1_norm + (1 - alpha) * L2_norm2
    if (t == 0) t <- 1e-6
    
    plt <- plot_ly() %>%
      add_surface(
        x = ~beta1_seq,
        y = ~beta2_seq,
        z = ~z_matrix,
        colorscale = list(c(0, 1), c("lightgrey", "black")),
        showscale = FALSE,
        opacity = 0.9,
        name = "MSE Surface"
      ) %>%
      add_trace(
        x = rep(beta_ols[1], 2),
        y = rep(beta_ols[2], 2),
        z = c(mse_min, mse_max),
        type = "scatter3d",
        mode = "lines",
        line = list(color = "red", width = 6),
        name = "Unpenalized"
      ) %>%
      add_trace(
        x = rep(beta_pen[1], 2),
        y = rep(beta_pen[2], 2),
        z = c(mse_min, mse_max),
        type = "scatter3d",
        mode = "lines",
        line = list(color = "turquoise", width = 6),
        name = "Penalized"
      )
    
    ## === Constraint Wall === ##
    if (alpha == 1) {
      # === Lasso: Diamond (L1 norm constraint) ===
      angles <- seq(0, 2 * pi, length.out = 200)
      diamond <- data.frame(
        beta1 = t * cos(angles) / (abs(cos(angles)) + abs(sin(angles))),
        beta2 = t * sin(angles) / (abs(cos(angles)) + abs(sin(angles)))
      )
      
      n <- nrow(diamond)
      beta1_mat <- matrix(rep(diamond$beta1, length(z_vals)), nrow = n)
      beta2_mat <- matrix(rep(diamond$beta2, length(z_vals)), nrow = n)
      z_mat <- matrix(rep(z_vals, each = n), nrow = n)
      
      plt <- plt %>%
        add_surface(
          x = ~beta1_mat,
          y = ~beta2_mat,
          z = ~z_mat,
          opacity = 0.3,
          surfacecolor = matrix(1, nrow = n, ncol = length(z_vals)),
          colorscale = list(c(0, 1), c("turquoise", "turquoise")),
          showscale = FALSE,
          name = "Lasso Constraint"
        )
      
    } else if (alpha == 0) {
      # === Ridge: Circle (L2 norm constraint) ===
      angles <- seq(0, 2 * pi, length.out = 200)
      radius <- sqrt(t)
      circle <- data.frame(
        beta1 = radius * cos(angles),
        beta2 = radius * sin(angles)
      )
      
      n <- nrow(circle)
      beta1_mat <- matrix(rep(circle$beta1, length(z_vals)), nrow = n)
      beta2_mat <- matrix(rep(circle$beta2, length(z_vals)), nrow = n)
      z_mat <- matrix(rep(z_vals, each = n), nrow = n)
      
      plt <- plt %>%
        add_surface(
          x = ~beta1_mat,
          y = ~beta2_mat,
          z = ~z_mat,
          opacity = 0.3,
          surfacecolor = matrix(1, nrow = n, ncol = length(z_vals)),
          colorscale = list(c(0, 1), c("turquoise", "turquoise")),
          showscale = FALSE,
          name = "Ridge Constraint"
        )
      
    } else {
      # === Elastic Net: Numerical Solution for Boundary ===
      theta <- seq(0, 2 * pi, length.out = 200)
      
      get_radius <- function(theta, t, alpha) {
        fn <- function(r) {
          b1 <- r * cos(theta)
          b2 <- r * sin(theta)
          alpha * (abs(b1) + abs(b2)) + (1 - alpha) * (b1^2 + b2^2) - t
        }
        uniroot(fn, lower = 1e-6, upper = 1e3)$root
      }
      
      radii <- sapply(theta, get_radius, t = t, alpha = alpha)
      
      beta1_enet <- radii * cos(theta)
      beta2_enet <- radii * sin(theta)
      
      n <- length(beta1_enet)
      beta1_mat <- matrix(rep(beta1_enet, length(z_vals)), nrow = n)
      beta2_mat <- matrix(rep(beta2_enet, length(z_vals)), nrow = n)
      z_mat <- matrix(rep(z_vals, each = n), nrow = n)
      
      plt <- plt %>%
        add_surface(
          x = ~beta1_mat,
          y = ~beta2_mat,
          z = ~z_mat,
          opacity = 0.3,
          surfacecolor = matrix(1, nrow = n, ncol = length(z_vals)),
          colorscale = list(c(0, 1), c("turquoise", "turquoise")),
          showscale = FALSE,
          name = "Elastic Net Constraint"
        )
    }
    
    plt %>%
      layout(
        scene = list(
          xaxis = list(title = "Beta 1"),
          yaxis = list(title = "Beta 2"),
          zaxis = list(title = "Loss Function")
        )
      )
  })
}



shinyApp(ui, server)