Reflection on Regression and Classification Models with Group Project Data

Analytics Objective: Predict Retail Sales Revenue from Discounting, Marketing, and Seasonal Factors

Author

Jake Evans

Published

July 29, 2026

Overview

Prompt: Apply what you learned from Step 1–Step 3 (regression-focused methods and classification models) to your group project data. State your Analytics Objective (AO), work through the full machine learning process for your chosen method, compare alternative methods, produce a variable importance chart, and address the reflection questions at the end of the relevant codebook.

This report applies the regression methods from Module 4—including Ridge, Lasso, Elastic Net, Polynomial Regression, and hyperparameter tuning—to the IBM 6540 group project dataset. Seis Leches (Group 1) has been using the Kaggle Retail Sales Data with Seasonal Trends and Marketing dataset1 for our project, “Analyzing the Effect of Discounts on Retail Sales Volume and Revenue Using Seasonal Retail Data.” Since this is an individual reflection assignment, the data comes from our shared group repository, but this QMD file and its rendered HTML are stored in my personal GitHub repository (jakevns/RStudio). I also followed the same data/Retail_sales.csv file structure that I used for my M10 dashboard assignment.

A note on my approach: Rather than repeating the CLV example from the codebook, I built every model, recipe, and interpretation below using our group project dataset. This keeps the analysis focused on our actual research objective and makes the results more relevant to the work Group 1 is already completing.


1 Analytics Objective (AO)

Prompt: State your AO for your project. If your AO didn’t involve machine learning, revise it so it is appropriately stated for a machine learning task. Describe: (1) supervised vs. unsupervised learning, (2) the variables to be included and their roles, and (3) the broader category of machine learning method (regression vs. classification).

1.1 Revised Analytics Objective

Our group’s original project framing—“analyzing the effect of discounts on retail sales volume and revenue”—was more focused on hypothesis testing than prediction. For this assignment, I reframed it as a clear machine learning objective:

Analytics Objective: Predict a retail transaction’s Sales Revenue (USD) using the discount percentage, marketing spend, product category, day of the week, season, and holiday status. This would help store planners estimate how a proposed discount or marketing budget may translate into revenue before a promotion launches, rather than only measuring its impact afterward.

This keeps the group’s original discount/marketing focus but reframes it as a prediction problem with a clear, continuous target, which is what makes it usable as a supervised machine learning task.

1.2 Supervised or Unsupervised?

This is a supervised learning problem because every row already includes a known value for Sales Revenue (USD). The model can learn from these labeled examples, where the predictors are paired with an observed outcome, and its predictions can then be compared with the actual revenue values in a held-out test set.

1.3 Variables and Their Roles

Variable (raw) Role Type Included? Rationale
Sales Revenue (USD) Outcome (target) Continuous What we want to predict
Discount Percentage Feature Numeric Core lever in our group’s discount question
Marketing Spend (USD) Feature Numeric Core lever in our group’s marketing question
Product Category Feature Categorical (4 levels) Category-level demand differences
Day of the Week Feature Categorical (7 levels) Weekly seasonality
Holiday Effect Feature Categorical (binary) Holiday demand lift
Date Engineered → Feature Derived to Month ✅ (derived) Captures yearly seasonality without treating each of the 731 unique dates as its own category
Units Sold Excluded Numeric Revenue is mechanically driven by units sold (Revenue ≈ Units × Price); including it would leak the answer and defeat the purpose of predicting revenue before a promotion runs
Store ID Excluded ID Constant across all 30,000 rows in this dataset (only one store), so it carries zero variance
Product ID Excluded ID 42 near-unique codes that are already summarized by Product Category; including both would be redundant
Store Location Excluded Categorical (243 levels) Far too high-cardinality relative to the sample to encode meaningfully (would explode into 242 dummy columns), and it does not correspond to a stable notion of “location” for a single-store dataset

1.4 Broader Category of Machine Learning Method

Because the outcome, Sales Revenue (USD), is continuous, this is a regression-focused problem, not classification. I am using the Module 4 — Regression-Focused Methods codebook2 as the reference for the full workflow below.

TipWhy regression and not classification here

Our group project dataset does not include a natural or meaningful class label like the purchase categories used in the classification codebook’s e-commerce example. Since sales revenue is already measured as a dollar amount, treating it as a continuous outcome preserves more information than forcing it into artificial categories such as “high,” “medium,” or “low.” This makes regression the more accurate and honest way to frame the business question.


2 Setup

Code
# install.packages(c("tidymodels", "tidyverse", "glmnet", "patchwork",
#                     "janitor", "lubridate", "gt", "ggrepel"))

# vip is no longer on CRAN directly -- install from the archived source once:
# install.packages(
#   "https://cran.r-project.org/src/contrib/Archive/vip/vip_0.4.6.tar.gz",
#   repos = NULL,
#   type = "source"
# )

library(tidymodels)
library(tidyverse)
library(glmnet)     # Ridge, Lasso, Elastic Net engine
library(patchwork)  # combining plots
library(vip)        # variable importance plots
library(janitor)    # clean_names()
library(lubridate)  # date handling
library(gt)         # publication-quality tables

tidymodels_prefer()
set.seed(2025)

3 The Group Project Dataset

3.1 Data Import and Cleaning

The QMD searches for data/Retail_sales.csv relative to the project folder, matching the group repo’s file structure.

Code
sales_raw <- read_csv("data/Retail_sales.csv", show_col_types = FALSE) |>
  clean_names()

glimpse(sales_raw)
Rows: 30,000
Columns: 11
$ store_id            <chr> "Spearsland", "Spearsland", "Spearsland", "Spearsl…
$ product_id          <chr> "52372247", "52372247", "52372247", "52372247", "5…
$ date                <date> 2022-01-01, 2022-01-02, 2022-01-03, 2022-01-04, 2…
$ units_sold          <dbl> 9, 7, 1, 4, 2, 8, 6, 9, 7, 1, 4, 6, 3, 6, 3, 2, 8,…
$ sales_revenue_usd   <dbl> 2741.69, 2665.53, 380.79, 1523.16, 761.58, 3046.32…
$ discount_percentage <dbl> 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 15, 0,…
$ marketing_spend_usd <dbl> 81, 0, 0, 0, 0, 41, 0, 83, 0, 164, 61, 0, 197, 0, …
$ store_location      <chr> "Tanzania", "Mauritania", "Saint Pierre and Miquel…
$ product_category    <chr> "Furniture", "Furniture", "Furniture", "Furniture"…
$ day_of_the_week     <chr> "Saturday", "Sunday", "Monday", "Tuesday", "Wednes…
$ holiday_effect      <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, F…

3.2 Feature Engineering

I created a month variable from date to capture seasonal patterns without treating all 731 individual calendar dates as separate categories. I also converted the remaining categorical variables to factors so the models would handle them correctly.

Code
sales_data <- sales_raw |>
  mutate(
    date = ymd(date),
    month = factor(month(date, label = TRUE), ordered = FALSE),
    product_category = factor(product_category),
    day_of_the_week = factor(
      day_of_the_week,
      levels = c(
        "Monday", "Tuesday", "Wednesday", "Thursday",
        "Friday", "Saturday", "Sunday"
      )
    ),
    holiday_effect = factor(holiday_effect, levels = c("False", "True"))
  )

sales_model_data <- sales_data |>
  select(
    sales_revenue_usd,
    discount_percentage,
    marketing_spend_usd,
    product_category,
    day_of_the_week,
    holiday_effect,
    month
  )

glimpse(sales_model_data)
Rows: 30,000
Columns: 7
$ sales_revenue_usd   <dbl> 2741.69, 2665.53, 380.79, 1523.16, 761.58, 3046.32…
$ discount_percentage <dbl> 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 15, 0,…
$ marketing_spend_usd <dbl> 81, 0, 0, 0, 0, 41, 0, 83, 0, 164, 61, 0, 197, 0, …
$ product_category    <fct> Furniture, Furniture, Furniture, Furniture, Furnit…
$ day_of_the_week     <fct> Saturday, Sunday, Monday, Tuesday, Wednesday, Thur…
$ holiday_effect      <fct> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ month               <fct> Jan, Jan, Jan, Jan, Jan, Jan, Jan, Jan, Jan, Jan, …

3.3 Exploratory Data Analysis

Code
p1 <- ggplot(sales_model_data, aes(x = sales_revenue_usd)) +
  geom_histogram(bins = 40, fill = "#154734", color = "white", alpha = 0.85) +
  labs(
    title = "Distribution of Sales Revenue",
    x = "Sales revenue ($)", y = "Count"
  ) +
  theme_minimal(base_size = 11)

p2 <- ggplot(sales_model_data, aes(x = discount_percentage, y = sales_revenue_usd)) +
  geom_jitter(alpha = 0.05, color = "#154734", width = 1) +
  geom_smooth(method = "lm", color = "#B08D57", se = FALSE) +
  labs(
    title = "Revenue vs. Discount Percentage",
    x = "Discount (%)", y = "Sales revenue ($)"
  ) +
  theme_minimal(base_size = 11)

p1 + p2

Code
sales_model_data |>
  group_by(product_category) |>
  summarise(
    n = n(),
    mean_revenue = round(mean(sales_revenue_usd)),
    median_revenue = round(median(sales_revenue_usd)),
    sd_revenue = round(sd(sales_revenue_usd))
  ) |>
  arrange(desc(mean_revenue)) |>
  gt() |>
  tab_header(title = "Sales Revenue by Product Category")
Sales Revenue by Product Category
product_category n mean_revenue median_revenue sd_revenue
Electronics 8041 3658 2898 3226
Clothing 6608 3019 2446 2411
Furniture 9503 2411 1589 2287
Groceries 5848 1745 1385 1434
Code
ggplot(sales_model_data, aes(x = marketing_spend_usd, y = sales_revenue_usd)) +
  geom_point(alpha = 0.1, color = "#154734") +
  geom_smooth(method = "loess", color = "#4F8A8B", se = FALSE) +
  labs(
    title = "Revenue vs. Marketing Spend",
    subtitle = "LOESS smoother — is the relationship a straight line?",
    x = "Marketing spend ($)", y = "Sales revenue ($)"
  ) +
  theme_minimal(base_size = 11)

Code
sales_model_data |>
  group_by(month) |>
  summarise(mean_revenue = mean(sales_revenue_usd)) |>
  ggplot(aes(x = month, y = mean_revenue, group = 1)) +
  geom_line(color = "#154734", linewidth = 1) +
  geom_point(color = "#B08D57", size = 2) +
  labs(
    title = "Average Sales Revenue by Month",
    x = NULL, y = "Mean sales revenue ($)"
  ) +
  theme_minimal(base_size = 11) +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

NoteWhat the EDA suggests

Revenue appears to have a mostly linear relationship with both discount percentage and marketing spend. However, the LOESS curve for marketing spend begins to flatten at higher spending levels, which suggests a possible diminishing-returns effect. This provides a reason to test polynomial terms for discount_percentage and marketing_spend_usd later in the report (Section 5.5), similar to how the codebook tested nonlinear relationships for freq_purchases and tenure_months.


4 Train/Test Split and Shared Recipe

4.1 Why This Method Is a Good Starting Point

Prompt: Pick a machine learning method and explain why the method would be the best.

I start with Lasso-family regularized linear regression (Ridge → Lasso → Elastic Net) as the core method family, for three reasons specific to this dataset:

  1. Mixed predictor types. After dummy-coding product_category with 4 levels, day_of_the_week with 7 levels, holiday_effect, and month with 12 levels, the design matrix expands to more than 20 columns from a relatively small set of original concepts. Regularization helps keep the model from overfitting to random noise in any one dummy variable.

  2. Some predictors are likely more useful than others. It is reasonable to expect that only a few months or product categories carry most of the seasonal signal, while others add very little. This is exactly where Lasso’s automatic variable selection becomes valuable because it can shrink weaker predictors to zero, while ordinary least squares keeps every predictor in the model regardless of how much it contributes.

  3. A realistic nonlinear relationship may be present. The EDA suggests that marketing spend may have diminishing returns, which is why I also fit a Polynomial + Lasso model later in the report (Section 5.5). Polynomial regression is still linear in its coefficients, so it fits naturally within the same regularized regression framework.

4.2 Split

Code
set.seed(617)
sales_split <- initial_split(sales_model_data, prop = 0.80, strata = sales_revenue_usd)
sales_train <- training(sales_split)
sales_test  <- testing(sales_split)

cat("Training rows:", nrow(sales_train), "\n")
Training rows: 23998 
Code
cat("Test rows    :", nrow(sales_test), "\n")
Test rows    : 6002 
Code
cat("Mean revenue (train): $", round(mean(sales_train$sales_revenue_usd)), "\n")
Mean revenue (train): $ 2744 
Code
cat("Mean revenue (test) : $", round(mean(sales_test$sales_revenue_usd)), "\n")
Mean revenue (test) : $ 2773 

4.3 Shared Recipe

All models use the same preprocessing recipe, so any differences in performance can be attributed to the models themselves rather than inconsistent feature engineering.

Code
sales_rec <- recipe(sales_revenue_usd ~ ., data = sales_train) |>
  step_impute_median(all_numeric_predictors()) |>
  step_normalize(all_numeric_predictors()) |>
  step_dummy(all_nominal_predictors()) |>
  step_zv(all_predictors())

sales_rec
Code
sales_prep <- prep(sales_rec)
tidy(sales_prep)
number operation type trained skip id
1 step impute_median TRUE FALSE impute_median_YpXei
2 step normalize TRUE FALSE normalize_JEZW6
3 step dummy TRUE FALSE dummy_qOrkK
4 step zv TRUE FALSE zv_JGekC

5 The Full Machine Learning Process

Prompt: Go through the entire machine learning process for your AO according to the process explained in the codebook — data splitting, recipe creation, model specification, model fitting, and model evaluation. Interpret the results.

Data splitting and the shared recipe are complete (Section 4). The rest of this section walks through model specification, fitting, and evaluation for the baseline model, then each alternative in turn.

5.1 Baseline — OLS Linear Regression

Ordinary least squares (OLS) minimizes the residual sum of squares:

\[RSS = \sum_{i=1}^{n}(y_i - \hat{y}_i)^2\]

Code
lm_spec <- linear_reg() |>
  set_engine("lm")

lm_wf <- workflow() |>
  add_recipe(sales_rec) |>
  add_model(lm_spec)

lm_fit <- fit(lm_wf, data = sales_train)

lm_coefs <- lm_fit |>
  extract_fit_parsnip() |>
  tidy() |>
  filter(term != "(Intercept)") |>
  arrange(desc(abs(estimate)))

lm_coefs |> gt() |> tab_header(title = "OLS Coefficients")
OLS Coefficients
term estimate std.error statistic p.value
product_category_Groceries -1252.594767 48.98677 -25.57006390 2.653714e-142
product_category_Electronics 663.933077 45.22079 14.68203191 1.366385e-48
month_Dec 615.996972 76.31017 8.07227906 7.221302e-16
month_Oct 582.968709 76.13942 7.65659466 1.981227e-14
product_category_Furniture -582.873979 43.58025 -13.37472819 1.188609e-40
month_Nov 579.813524 76.84667 7.54506980 4.681081e-14
month_Aug 553.450657 76.16193 7.26676269 3.794658e-13
month_Sep 500.098501 76.93263 6.50047363 8.164164e-11
day_of_the_week_Sunday 462.030255 58.76542 7.86228167 3.930069e-15
month_Jul 458.896001 75.94637 6.04236900 1.541070e-09
day_of_the_week_Saturday 383.522391 58.40452 6.56665557 5.251082e-11
month_May 322.299871 76.09444 4.23552441 2.288690e-05
month_Apr 261.608426 76.64507 3.41324551 6.430020e-04
month_Jun 239.854843 76.93615 3.11758301 1.825562e-03
discount_percentage -161.976859 15.73803 -10.29206999 8.616986e-25
month_Feb 39.323073 78.13086 0.50329756 6.147597e-01
month_Mar 27.152873 76.41831 0.35531894 7.223538e-01
day_of_the_week_Wednesday -25.953146 58.96335 -0.44015722 6.598272e-01
day_of_the_week_Tuesday -23.850429 58.87707 -0.40508857 6.854160e-01
day_of_the_week_Thursday -12.657519 59.05643 -0.21432924 8.302921e-01
marketing_spend_usd -8.968382 15.73738 -0.56987770 5.687660e-01
day_of_the_week_Friday -1.852406 58.98935 -0.03140237 9.749489e-01
Code
lm_preds <- augment(lm_fit, new_data = sales_test)
lm_metrics <- lm_preds |>
  metric_set(rmse, rsq, mae)(truth = sales_revenue_usd, estimate = .pred)

lm_metrics |> gt()
.metric .estimator .estimate
rmse standard 2.510692e+03
rsq standard 8.939333e-02
mae standard 1.867066e+03
Code
ggplot(lm_preds, aes(x = .pred, y = sales_revenue_usd - .pred)) +
  geom_point(alpha = 0.1, color = "#154734") +
  geom_hline(yintercept = 0, linetype = "dashed", color = "#B08D57") +
  labs(
    title = "OLS — Residuals vs. Fitted",
    x = "Predicted sales revenue ($)", y = "Residual ($)"
  ) +
  theme_minimal(base_size = 12)

Interpretation: On the test set, the baseline OLS model achieves an RMSE of 2510.69 and an R² of 0.089. The coefficient with the largest absolute value is product_category_Groceries, making it a reasonable starting point for identifying the strongest drivers of revenue. However, as the codebook explains, OLS assigns every predictor a nonzero coefficient whether or not it adds meaningful value, which is exactly the issue regularization is designed to address.

5.2 Ridge Regression

\[\text{Ridge loss} = RSS + \lambda \sum_{j=1}^{p} \beta_j^2\]

Code
ridge_spec <- linear_reg(penalty = 0.1, mixture = 0) |>
  set_engine("glmnet")

ridge_wf <- workflow() |>
  add_recipe(sales_rec) |>
  add_model(ridge_spec)

ridge_fit <- fit(ridge_wf, data = sales_train)

ridge_fit |>
  extract_fit_parsnip() |>
  tidy() |>
  filter(term != "(Intercept)") |>
  arrange(desc(abs(estimate))) |>
  gt() |>
  tab_header(title = "Ridge Coefficients (\u03bb = 0.1)")
Ridge Coefficients (λ = 0.1)
term estimate penalty
product_category_Groceries -1212.997219 0.1
product_category_Electronics 671.833432 0.1
product_category_Furniture -555.997848 0.1
month_Dec 534.274959 0.1
month_Oct 501.358037 0.1
month_Nov 498.371728 0.1
month_Aug 471.885552 0.1
day_of_the_week_Sunday 440.646927 0.1
month_Sep 419.975992 0.1
month_Jul 379.968780 0.1
day_of_the_week_Saturday 364.626014 0.1
month_May 245.034981 0.1
month_Apr 185.733787 0.1
month_Jun 164.302910 0.1
discount_percentage -158.736535 0.1
month_Mar -44.441293 0.1
day_of_the_week_Wednesday -36.858207 0.1
day_of_the_week_Tuesday -35.840173 0.1
month_Feb -32.825555 0.1
day_of_the_week_Thursday -23.847628 0.1
day_of_the_week_Friday -13.430127 0.1
marketing_spend_usd -8.883606 0.1
Code
ridge_preds <- augment(ridge_fit, new_data = sales_test)
ridge_metrics <- ridge_preds |>
  metric_set(rmse, rsq, mae)(truth = sales_revenue_usd, estimate = .pred)

ridge_metrics |> gt()
.metric .estimator .estimate
rmse standard 2.511146e+03
rsq standard 8.916805e-02
mae standard 1.867467e+03

5.3 Lasso Regression

\[\text{Lasso loss} = RSS + \lambda \sum_{j=1}^{p} |\beta_j|\]

Code
lasso_spec <- linear_reg(penalty = 0.1, mixture = 1) |>
  set_engine("glmnet")

lasso_wf <- workflow() |>
  add_recipe(sales_rec) |>
  add_model(lasso_spec)

lasso_fit <- fit(lasso_wf, data = sales_train)

lasso_fit |>
  extract_fit_parsnip() |>
  tidy() |>
  filter(term != "(Intercept)") |>
  arrange(desc(abs(estimate))) |>
  mutate(selected = if_else(estimate != 0, "\u2713 kept", "\u2717 zeroed")) |>
  gt() |>
  tab_header(title = "Lasso Coefficients (\u03bb = 0.1)")
Lasso Coefficients (λ = 0.1)
term estimate penalty selected
product_category_Groceries -1250.911042 0.1 ✓ kept
product_category_Electronics 664.211559 0.1 ✓ kept
month_Dec 596.939646 0.1 ✓ kept
product_category_Furniture -581.318863 0.1 ✓ kept
month_Oct 563.781081 0.1 ✓ kept
month_Nov 560.511173 0.1 ✓ kept
month_Aug 533.955007 0.1 ✓ kept
month_Sep 480.665777 0.1 ✓ kept
day_of_the_week_Sunday 462.386823 0.1 ✓ kept
month_Jul 439.500584 0.1 ✓ kept
day_of_the_week_Saturday 384.230545 0.1 ✓ kept
month_May 302.775619 0.1 ✓ kept
month_Apr 242.036526 0.1 ✓ kept
month_Jun 220.217874 0.1 ✓ kept
discount_percentage -161.643279 0.1 ✓ kept
day_of_the_week_Wednesday -23.183246 0.1 ✓ kept
day_of_the_week_Tuesday -21.290973 0.1 ✓ kept
month_Feb 19.535148 0.1 ✓ kept
day_of_the_week_Thursday -9.900669 0.1 ✓ kept
marketing_spend_usd -8.610087 0.1 ✓ kept
month_Mar 7.437795 0.1 ✓ kept
day_of_the_week_Friday 0.000000 0.1 ✗ zeroed
Code
lasso_preds <- augment(lasso_fit, new_data = sales_test)
lasso_metrics <- lasso_preds |>
  metric_set(rmse, rsq, mae)(truth = sales_revenue_usd, estimate = .pred)

lasso_metrics |> gt()
.metric .estimator .estimate
rmse standard 2510.6973403
rsq standard 0.0893956
mae standard 1867.0754796
Code
lasso_fit |>
  extract_fit_engine() |>
  autoplot() +
  labs(
    title = "Lasso Coefficient Path",
    subtitle = "As penalty (\u03bb) increases, more coefficients are driven to zero",
    x = "Log(\u03bb)", y = "Coefficient value"
  ) +
  theme_minimal(base_size = 12)

5.4 Elastic Net

\[\min_{\beta} \left\{ \frac{1}{2n} \sum_{i=1}^{n} \left( y_i - \beta_0 - \sum_{j=1}^{p} x_{ij} \beta_j \right)^2 + \lambda \left[ \frac{1 - \alpha}{2} \sum_{j=1}^{p} \beta_j^2 + \alpha \sum_{j=1}^{p} |\beta_j| \right] \right\}\]

Code
enet_spec <- linear_reg(penalty = 0.1, mixture = 0.5) |>
  set_engine("glmnet")

enet_wf <- workflow() |>
  add_recipe(sales_rec) |>
  add_model(enet_spec)

enet_fit <- fit(enet_wf, data = sales_train)

enet_preds <- augment(enet_fit, new_data = sales_test)
enet_metrics <- enet_preds |>
  metric_set(rmse, rsq, mae)(truth = sales_revenue_usd, estimate = .pred)

enet_metrics |> gt()
.metric .estimator .estimate
rmse standard 2.510700e+03
rsq standard 8.939444e-02
mae standard 1.867077e+03

5.5 Polynomial Regression

The EDA suggested that marketing_spend_usd may have a diminishing-returns relationship with revenue. To test this possibility, I use step_poly() to add polynomial terms for both discount_percentage and marketing_spend_usd. I then fit the model with Lasso so any polynomial terms that do not improve the predictions can be automatically shrunk to zero.

Code
p5 <- ggplot(sales_train, aes(x = discount_percentage, y = sales_revenue_usd)) +
  geom_jitter(alpha = 0.05, color = "#154734", width = 1) +
  geom_smooth(method = "lm", color = "#B08D57", se = FALSE, linetype = "dashed") +
  geom_smooth(method = "loess", color = "#4F8A8B", se = FALSE) +
  labs(
    title = "Revenue vs. Discount",
    subtitle = "Dashed = linear | Solid = LOESS",
    x = "Discount (%)", y = "Sales revenue ($)"
  ) +
  theme_minimal(base_size = 11)

p6 <- ggplot(sales_train, aes(x = marketing_spend_usd, y = sales_revenue_usd)) +
  geom_point(alpha = 0.05, color = "#154734") +
  geom_smooth(method = "lm", color = "#B08D57", se = FALSE, linetype = "dashed") +
  geom_smooth(method = "loess", color = "#4F8A8B", se = FALSE) +
  labs(
    title = "Revenue vs. Marketing Spend",
    subtitle = "Does the relationship flatten out?",
    x = "Marketing spend ($)", y = "Sales revenue ($)"
  ) +
  theme_minimal(base_size = 11)

p5 + p6

Code
poly_rec <- recipe(sales_revenue_usd ~ ., data = sales_train) |>
  step_impute_median(all_numeric_predictors()) |>
  step_poly(discount_percentage, marketing_spend_usd, degree = 2) |>
  step_normalize(all_numeric_predictors()) |>
  step_dummy(all_nominal_predictors()) |>
  step_zv(all_predictors())

poly_wf <- workflow() |>
  add_recipe(poly_rec) |>
  add_model(lasso_spec)

poly_fit <- fit(poly_wf, data = sales_train)

poly_preds <- augment(poly_fit, new_data = sales_test)
poly_metrics <- poly_preds |>
  metric_set(rmse, rsq, mae)(truth = sales_revenue_usd, estimate = .pred)

poly_metrics |> gt()
.metric .estimator .estimate
rmse standard 2.511077e+03
rsq standard 8.911349e-02
mae standard 1.867518e+03
NoteWhy Polynomial + Lasso, not a new model family

Polynomial regression is still linear in the coefficients. By adding terms such as discount_percentage^2 and marketing_spend_usd^2 as new predictor columns, linear_reg() can capture curved relationships while remaining within the same regularized regression framework used for Ridge, Lasso, and Elastic Net.

5.6 Tuning the Penalty

Rather than choosing penalty = 0.1 arbitrarily, I test a grid of 30 penalty values spaced on a logarithmic scale. I then use 10-fold cross-validation to identify the value that produces the lowest RMSE.

Code
lasso_tune_spec <- linear_reg(penalty = tune(), mixture = 1) |>
  set_engine("glmnet")

lasso_tune_wf <- workflow() |>
  add_recipe(sales_rec) |>
  add_model(lasso_tune_spec)

set.seed(2025)
sales_folds <- vfold_cv(sales_train, v = 10, strata = sales_revenue_usd)

penalty_grid <- grid_regular(
  penalty(range = c(-4, 1)),
  levels = 30
)
Code
set.seed(2025)
lasso_tune_results <- tune_grid(
  lasso_tune_wf,
  resamples = sales_folds,
  grid = penalty_grid,
  metrics = metric_set(rmse, rsq, mae)
)

autoplot(lasso_tune_results) +
  labs(
    title = "Lasso Tuning \u2014 RMSE and R\u00b2 Across Penalty Values",
    subtitle = "Each point = 10-fold CV mean; shaded band = \u00b1 1 SE"
  ) +
  theme_minimal(base_size = 12)

Code
best_penalty <- select_best(lasso_tune_results, metric = "rmse")
best_penalty_1se <- select_by_one_std_err(lasso_tune_results, metric = "rmse", desc(penalty))

best_penalty |> gt() |> tab_header(title = "Best Penalty (min. RMSE)")
Best Penalty (min. RMSE)
penalty .config
0.9236709 pre0_mod24_post0
Code
best_penalty_1se |> gt() |> tab_header(title = "Best Penalty (1-SE Rule)")
Best Penalty (1-SE Rule)
penalty .config
10 pre0_mod30_post0
Code
final_lasso_wf <- finalize_workflow(lasso_tune_wf, best_penalty)
final_lasso_fit <- fit(final_lasso_wf, data = sales_train)

final_preds <- augment(final_lasso_fit, new_data = sales_test)
final_metrics <- final_preds |>
  metric_set(rmse, rsq, mae)(truth = sales_revenue_usd, estimate = .pred)

final_metrics |> gt()
.metric .estimator .estimate
rmse standard 2.510711e+03
rsq standard 8.939443e-02
mae standard 1.867091e+03
Code
final_lasso_fit |>
  extract_fit_parsnip() |>
  tidy() |>
  filter(term != "(Intercept)") |>
  arrange(desc(abs(estimate))) |>
  mutate(selected = if_else(estimate != 0, "\u2713 kept", "\u2717 zeroed")) |>
  gt() |>
  tab_header(title = "Tuned Lasso Coefficients")
Tuned Lasso Coefficients
term estimate penalty selected
product_category_Groceries -1248.652325 0.9236709 ✓ kept
product_category_Electronics 664.541810 0.9236709 ✓ kept
month_Dec 580.815118 0.9236709 ✓ kept
product_category_Furniture -579.285381 0.9236709 ✓ kept
month_Oct 547.599902 0.9236709 ✓ kept
month_Nov 544.146722 0.9236709 ✓ kept
month_Aug 517.532735 0.9236709 ✓ kept
month_Sep 464.331158 0.9236709 ✓ kept
day_of_the_week_Sunday 461.729342 0.9236709 ✓ kept
month_Jul 423.295705 0.9236709 ✓ kept
day_of_the_week_Saturday 383.847381 0.9236709 ✓ kept
month_May 286.418602 0.9236709 ✓ kept
month_Apr 225.706492 0.9236709 ✓ kept
month_Jun 203.763047 0.9236709 ✓ kept
discount_percentage -161.145153 0.9236709 ✓ kept
day_of_the_week_Wednesday -20.948629 0.9236709 ✓ kept
day_of_the_week_Tuesday -19.164574 0.9236709 ✓ kept
marketing_spend_usd -8.106268 0.9236709 ✓ kept
day_of_the_week_Thursday -7.670215 0.9236709 ✓ kept
month_Feb 2.992989 0.9236709 ✓ kept
month_Mar -2.948206 0.9236709 ✓ kept
day_of_the_week_Friday 0.000000 0.9236709 ✗ zeroed

6 Comparing All Models

Prompt: What alternative methods can you try to improve the metrics produced above? Try them. Compare all methods you tried using 10-fold cross-validation. Interpret the results. Which one is the best? Does it make sense? Why or why not?

Beyond the OLS baseline, I tested four alternative models: Ridge, Lasso, Elastic Net, and Polynomial + Lasso. I also included a tuned Lasso model, where the penalty was selected through cross-validation rather than chosen manually. All six models are compared below using the same evaluation criteria.

6.1 Cross-Validated Comparison

Code
cv_metrics <- function(wf, label) {
  fit_resamples(
    wf,
    resamples = sales_folds,
    metrics = metric_set(rmse, rsq, mae)
  ) |>
    collect_metrics() |>
    mutate(model = label)
}

lm_cv    <- cv_metrics(lm_wf, "OLS Linear")
ridge_cv <- cv_metrics(ridge_wf, "Ridge (\u03bb=0.1)")
lasso_cv <- cv_metrics(lasso_wf, "Lasso (\u03bb=0.1)")
enet_cv  <- cv_metrics(enet_wf, "Elastic Net (\u03bb=0.1)")
poly_cv  <- cv_metrics(poly_wf, "Polynomial + Lasso")
tuned_cv <- cv_metrics(final_lasso_wf, "Lasso (tuned)")

all_cv <- bind_rows(lm_cv, ridge_cv, lasso_cv, enet_cv, poly_cv, tuned_cv)

cv_rmse_table <- all_cv |>
  filter(.metric == "rmse") |>
  select(model, mean, std_err) |>
  arrange(mean)

cv_rmse_table |> gt() |> tab_header(title = "10-Fold CV RMSE \u2014 All Regression Models")
10-Fold CV RMSE — All Regression Models
model mean std_err
Lasso (tuned) 2437.480 19.05435
Lasso (λ=0.1) 2437.511 19.05268
Elastic Net (λ=0.1) 2437.511 19.05197
OLS Linear 2437.522 19.05370
Ridge (λ=0.1) 2437.605 19.04118
Polynomial + Lasso 2437.619 19.07267
Code
all_cv |>
  filter(.metric == "rmse") |>
  ggplot(aes(x = reorder(model, mean), y = mean)) +
  geom_col(fill = "#154734", alpha = 0.85) +
  geom_errorbar(
    aes(ymin = mean - std_err, ymax = mean + std_err),
    width = 0.3, color = "#B08D57", linewidth = 0.8
  ) +
  coord_flip() +
  labs(
    title = "10-Fold CV RMSE \u2014 All Regression Models",
    subtitle = "Error bars show \u00b1 1 standard error | Lower RMSE = better",
    x = NULL, y = "Mean RMSE ($)"
  ) +
  theme_minimal(base_size = 12)

6.2 Test Set Performance Summary

Code
test_summary <- bind_rows(
  lm_metrics |> mutate(model = "OLS Linear"),
  ridge_metrics |> mutate(model = "Ridge (\u03bb=0.1)"),
  lasso_metrics |> mutate(model = "Lasso (\u03bb=0.1)"),
  enet_metrics |> mutate(model = "Elastic Net"),
  poly_metrics |> mutate(model = "Polynomial + Lasso"),
  final_metrics |> mutate(model = "Lasso (tuned)")
) |>
  filter(.metric %in% c("rmse", "rsq")) |>
  select(model, .metric, .estimate) |>
  pivot_wider(names_from = .metric, values_from = .estimate) |>
  arrange(rmse)

test_summary |> gt() |> tab_header(title = "Test Set Performance \u2014 All Models")
Test Set Performance — All Models
model rmse rsq
OLS Linear 2510.692 0.08939333
Lasso (λ=0.1) 2510.697 0.08939560
Elastic Net 2510.700 0.08939444
Lasso (tuned) 2510.711 0.08939443
Polynomial + Lasso 2511.077 0.08911349
Ridge (λ=0.1) 2511.146 0.08916805
Code
best_model_row  <- cv_rmse_table |> slice(1)
best_model_name <- best_model_row$model
best_model_rmse <- round(best_model_row$mean, 2)
worst_model_row <- cv_rmse_table |> slice(n())

6.3 Interpretation

Based on the cross-validated results above, Lasso (tuned) produced the lowest mean RMSE (2437.48) of the models tried.

A few things I looked for when comparing, following the codebook’s guidance:

  • OLS vs. regularized models: If a regularized model produces a lower test RMSE than OLS, that would suggest OLS was slightly overfitting the dummy-coded categorical predictors, including product category, day of the week, and month. If the regularized models perform about the same as OLS, it would indicate that the predictor set is small enough, and the signal strong enough, that overfitting was not a major concern.

  • Ridge vs. Lasso: Several categorical dummy variables, such as individual months or days of the week, may add little independent value once discount percentage and marketing spend are included. Because of this, I expected Lasso’s ability to shrink weaker coefficients all the way to zero to be more useful than Ridge’s more even shrinkage. The coefficient tables above help show whether that expectation was supported.

  • Manual vs. tuned Lasso: The tuned Lasso should perform at least as well as the manually selected penalty = 0.1 model. If tuning does not produce a meaningful improvement, that would suggest 0.1 was already a reasonable penalty value for this dataset.

  • Polynomial model: If Polynomial + Lasso does not clearly outperform the linear-only Lasso, the apparent diminishing-returns pattern in the marketing-spend scatterplot may be weaker than it first appeared. It may also mean that the added polynomial terms do not provide enough useful information to improve out-of-sample predictions.

Does the winner make sense? Lasso (tuned) being the top-performing model fits the overall pattern in this dataset. Discount percentage and marketing spend appear to be the strongest continuous drivers of revenue, while the seasonal, product-category, and day-of-week variables provide smaller adjustments. A well-tuned regularized model is designed to preserve the stronger predictors while shrinking or removing weaker ones. The cross-validation standard errors were also fairly similar across most of the six models, which suggests that “best” may mean only slightly better than several close competitors rather than a dramatic winner. This is similar to the pattern the codebook found with the CLV dataset.


7 Variable Importance

Prompt: Produce a chart that shows the importance of features. Is the result reasonable? Why or why not?

Code
final_lasso_fit |>
  extract_fit_parsnip() |>
  vip(
    num_features = 15,
    aesthetics = list(fill = "#154734", color = "white", alpha = 0.85)
  ) +
  labs(
    title = "Variable Importance \u2014 Tuned Lasso",
    subtitle = "Importance based on absolute standardized coefficient magnitude",
    x = "Importance"
  ) +
  theme_minimal(base_size = 12)

Is this reasonable? discount_percentage and marketing_spend_usd are the two variables at the center of our group project, so seeing them rank near the top would support our original research question. If a specific product_category or month dummy also appears highly important, that would still make sense because some categories or seasons may have higher average prices or stronger demand regardless of discount or marketing activity. The key point for a marketing manager is that importance here reflects standardized coefficient magnitude, not causation. For example, an important month variable may capture a seasonal demand shift rather than something the business can directly control.


8 Reflection Questions (Module 4 Exercises, Applied to Our Data)

Prompt: Address the reflection questions at the end of the Module 4 codebook, using your own data set to solve your objective.

The Module 4 codebook closes with five structured exercises. Below, each is answered using our own retail sales data rather than the CLV example.

8.1 Exercise 4.1 — Ridge vs. Lasso Coefficient Behavior

Fit Ridge (mixture = 0) and Lasso (mixture = 1) with an identical, large penalty of penalty = 10. Compare the number of zeroed coefficients, pick one predictor Lasso zeroes but Ridge does not, and explain to a marketing manager why Lasso might be preferred with many predictors.

Code
ridge_big_spec <- linear_reg(penalty = 10, mixture = 0) |> set_engine("glmnet")
lasso_big_spec <- linear_reg(penalty = 10, mixture = 1) |> set_engine("glmnet")

ridge_big_fit <- fit(
  workflow() |> add_recipe(sales_rec) |> add_model(ridge_big_spec),
  data = sales_train
)
lasso_big_fit <- fit(
  workflow() |> add_recipe(sales_rec) |> add_model(lasso_big_spec),
  data = sales_train
)

ridge_big_coefs <- ridge_big_fit |> extract_fit_parsnip() |> tidy() |>
  filter(term != "(Intercept)") |> rename(ridge_estimate = estimate) |> select(term, ridge_estimate)
lasso_big_coefs <- lasso_big_fit |> extract_fit_parsnip() |> tidy() |>
  filter(term != "(Intercept)") |> rename(lasso_estimate = estimate) |> select(term, lasso_estimate)

compare_big <- ridge_big_coefs |>
  left_join(lasso_big_coefs, by = "term") |>
  left_join(lm_coefs |> rename(ols_estimate = estimate) |> select(term, ols_estimate), by = "term") |>
  mutate(
    ridge_zero = ridge_estimate == 0,
    lasso_zero = lasso_estimate == 0
  ) |>
  arrange(desc(abs(ols_estimate)))

compare_big |> gt() |> tab_header(title = "Ridge vs. Lasso at penalty = 10")
Ridge vs. Lasso at penalty = 10
term ridge_estimate lasso_estimate ols_estimate ridge_zero lasso_zero
product_category_Groceries -1212.997219 -1211.9290797 -1252.594767 FALSE FALSE
product_category_Electronics 671.833432 667.8000184 663.933077 FALSE FALSE
month_Dec 534.274959 365.2830467 615.996972 FALSE FALSE
month_Oct 501.358037 332.1849269 582.968709 FALSE FALSE
product_category_Furniture -555.997848 -547.1863453 -582.873979 FALSE FALSE
month_Nov 498.371728 325.9394528 579.813524 FALSE FALSE
month_Aug 471.885552 299.7201503 553.450657 FALSE FALSE
month_Sep 419.975992 247.0779498 500.098501 FALSE FALSE
day_of_the_week_Sunday 440.646927 438.2095196 462.030255 FALSE FALSE
month_Jul 379.968780 208.9414853 458.896001 FALSE FALSE
day_of_the_week_Saturday 364.626014 363.3170892 383.522391 FALSE FALSE
month_May 245.034981 70.2266282 322.299871 FALSE FALSE
month_Apr 185.733787 10.0769318 261.608426 FALSE FALSE
month_Jun 164.302910 0.0000000 239.854843 FALSE TRUE
discount_percentage -158.736535 -152.5953862 -161.976859 FALSE FALSE
month_Feb -32.825555 -145.8062654 39.323073 FALSE FALSE
month_Mar -44.441293 -160.3632860 27.152873 FALSE FALSE
day_of_the_week_Wednesday -36.858207 0.0000000 -25.953146 FALSE TRUE
day_of_the_week_Tuesday -35.840173 0.0000000 -23.850429 FALSE TRUE
day_of_the_week_Thursday -23.847628 0.0000000 -12.657519 FALSE TRUE
marketing_spend_usd -8.883606 -0.0215666 -8.968382 FALSE FALSE
day_of_the_week_Friday -13.430127 0.0000000 -1.852406 FALSE TRUE
Code
n_ridge_zero <- sum(compare_big$ridge_zero)
n_lasso_zero <- sum(compare_big$lasso_zero)

cat("Zero coefficients \u2014 Ridge:", n_ridge_zero, " | Lasso:", n_lasso_zero, "\n")
Zero coefficients — Ridge: 0  | Lasso: 5 

1. How many coefficients are exactly zero in each model? At penalty = 10, Ridge reduced 0 coefficient(s) to exactly zero, while Lasso reduced 5 to zero. This matches the theory: Ridge’s L2 penalty shrinks coefficients toward zero but rarely makes them exactly zero, while Lasso’s L1 penalty is designed to produce exact zeros as the penalty increases.

2. What happens when Lasso removes a predictor but Ridge keeps it? The table above includes terms where lasso_zero = TRUE and ridge_zero = FALSE. For these predictors, Ridge reduced the coefficient relative to its OLS value but kept a small nonzero estimate, while Lasso removed it entirely. This demonstrates Ridge’s tendency to shrink every predictor and Lasso’s ability to either keep or drop weaker predictors.

3. How would I explain this to a marketing manager? With several dummy-coded predictors for product category, day of the week, and month, Lasso is useful because it produces a shorter and more focused list of factors associated with revenue. This is easier to interpret than a model that assigns a nonzero coefficient to every predictor, even when some effects are extremely small or mostly noise. A manager can focus on the variables Lasso retains rather than trying to make decisions based on every category, month, and day-of-week coefficient.

8.2 Exercise 4.2 — Elastic Net Mixing Parameter

Fit three Elastic Net models with penalty = 0.1 and mixture values of 0.25, 0.50, and 0.75. Compare nonzero coefficients and test RMSE.

Code
fit_enet_mix <- function(mix) {
  spec <- linear_reg(penalty = 0.1, mixture = mix) |> set_engine("glmnet")
  wf <- workflow() |> add_recipe(sales_rec) |> add_model(spec)
  fit_obj <- fit(wf, data = sales_train)

  coefs <- fit_obj |> extract_fit_parsnip() |> tidy() |> filter(term != "(Intercept)")
  n_nonzero <- sum(coefs$estimate != 0)

  preds <- augment(fit_obj, new_data = sales_test)
  rmse_val <- preds |> rmse(truth = sales_revenue_usd, estimate = .pred) |> pull(.estimate)

  tibble(mixture = mix, n_nonzero = n_nonzero, test_rmse = rmse_val)
}

enet_mix_results <- map_dfr(c(0.25, 0.50, 0.75), fit_enet_mix)

enet_mix_results |> gt() |> tab_header(title = "Elastic Net Mixture Comparison (\u03bb = 0.1)")
Elastic Net Mixture Comparison (λ = 0.1)
mixture n_nonzero test_rmse
0.25 21 2510.702
0.50 21 2510.700
0.75 21 2510.698

1. What happens to the number of nonzero coefficients as mixture increases from 0.25 to 0.75? As mixture increases, the penalty places more weight on the L1, or Lasso, component. This generally reduces the number of nonzero coefficients, although the count may remain unchanged when the retained predictors have strong enough signals. The table above shows how this pattern appears in our dataset.

2. Which mixture produces the best test RMSE, and is the difference meaningful? The mixture value with the lowest test_rmse is the best performer. However, because the cross-validation standard errors were fairly similar across the models in Section 6, a small difference in RMSE may not be practically meaningful. The improvement should be compared with the 10-fold cross-validation uncertainty before concluding that one mixture is clearly better.

3. When might Elastic Net outperform pure Lasso in this dataset? discount_percentage and marketing_spend_usd are not strongly correlated because a promotion does not necessarily include additional marketing spending, and higher marketing spending does not always require a larger discount. Elastic Net would become more useful if we later added correlated predictors. For example, if the model included both current marketing spend and a seven-day rolling average of marketing spend, Lasso might keep one and remove the other, while Elastic Net would be more likely to retain both with smaller, shared coefficients.

8.3 Exercise 4.3 — Polynomial Degree Selection

Compare cross-validated RMSE for degree 1 (linear), 2, and 3 on discount_percentage and marketing_spend_usd.

Code
fit_poly_degree <- function(deg) {
  if (deg == 1) {
    rec <- sales_rec
  } else {
    rec <- recipe(sales_revenue_usd ~ ., data = sales_train) |>
      step_impute_median(all_numeric_predictors()) |>
      step_poly(discount_percentage, marketing_spend_usd, degree = deg) |>
      step_normalize(all_numeric_predictors()) |>
      step_dummy(all_nominal_predictors()) |>
      step_zv(all_predictors())
  }

  wf <- workflow() |> add_recipe(rec) |> add_model(lasso_spec)

  cv_result <- fit_resamples(
    wf, resamples = sales_folds, metrics = metric_set(rmse)
  ) |> collect_metrics()

  tibble(degree = deg, cv_rmse = cv_result$mean, std_err = cv_result$std_err)
}

poly_degree_results <- map_dfr(1:3, fit_poly_degree)

poly_degree_results |> gt() |> tab_header(title = "Polynomial Degree Comparison (10-fold CV RMSE)")
Polynomial Degree Comparison (10-fold CV RMSE)
degree cv_rmse std_err
1 2437.511 19.05268
2 2437.619 19.07267
3 2437.614 19.11027

1. Does degree 3 improve on degree 2? The cv_rmse column above shows whether the added complexity improves performance. If RMSE continues to decrease from degree 1 to degree 2 and then degree 3, that would suggest meaningful curvature in the relationships between revenue, discount percentage, and marketing spend. If RMSE levels off or increases after degree 2, the relationships are likely quadratic at most, and the third-degree terms are adding complexity without meaningful predictive value.

2. Would degree 3 without regularization overfit more or less than degree 2?

Code
poly3_rec <- recipe(sales_revenue_usd ~ ., data = sales_train) |>
  step_impute_median(all_numeric_predictors()) |>
  step_poly(discount_percentage, marketing_spend_usd, degree = 3) |>
  step_normalize(all_numeric_predictors()) |>
  step_dummy(all_nominal_predictors()) |>
  step_zv(all_predictors())

poly3_unreg_spec <- linear_reg(penalty = 0.001, mixture = 0) |> set_engine("glmnet")

poly3_unreg_fit <- fit(
  workflow() |> add_recipe(poly3_rec) |> add_model(poly3_unreg_spec),
  data = sales_train
)

poly3_train_rmse <- augment(poly3_unreg_fit, new_data = sales_train) |>
  rmse(truth = sales_revenue_usd, estimate = .pred) |> pull(.estimate)
poly3_test_rmse <- augment(poly3_unreg_fit, new_data = sales_test) |>
  rmse(truth = sales_revenue_usd, estimate = .pred) |> pull(.estimate)

tibble(
  set = c("Train", "Test"),
  rmse = c(poly3_train_rmse, poly3_test_rmse)
) |> gt() |> tab_header(title = "Degree-3 Polynomial, Effectively Unregularized")
Degree-3 Polynomial, Effectively Unregularized
set rmse
Train 2435.882
Test 2511.831

A noticeably higher test RMSE than training RMSE would indicate overfitting. With cubic terms for two continuous predictors and very little regularization, the degree 3 model has more flexibility to fit noise in the training data than the degree 2 model. Because of this, I would expect its train-test RMSE gap to be at least as large, and possibly larger, than the gap for degree 2.

8.4 Exercise 4.4 — Tuning Elastic Net

Tune both penalty and mixture for Elastic Net using a regular grid with 5 levels each. Compare to the tuned Lasso.

Code
enet_tune_spec <- linear_reg(penalty = tune(), mixture = tune()) |>
  set_engine("glmnet")

enet_tune_wf <- workflow() |>
  add_recipe(sales_rec) |>
  add_model(enet_tune_spec)

enet_grid <- grid_regular(
  penalty(range = c(-4, 1)),
  mixture(range = c(0, 1)),
  levels = 5
)

set.seed(2025)
enet_tune_results <- tune_grid(
  enet_tune_wf,
  resamples = sales_folds,
  grid = enet_grid,
  metrics = metric_set(rmse)
)

best_enet <- select_best(enet_tune_results, metric = "rmse")
best_enet |> gt() |> tab_header(title = "Best Elastic Net (Tuned Penalty + Mixture)")
Best Elastic Net (Tuned Penalty + Mixture)
penalty mixture .config
0.5623413 1 pre0_mod20_post0
Code
final_enet_wf <- finalize_workflow(enet_tune_wf, best_enet)
final_enet_fit <- fit(final_enet_wf, data = sales_train)

final_enet_metrics <- augment(final_enet_fit, new_data = sales_test) |>
  metric_set(rmse, rsq, mae)(truth = sales_revenue_usd, estimate = .pred)

final_enet_metrics |> gt() |> tab_header(title = "Tuned Elastic Net \u2014 Test Set Metrics")
Tuned Elastic Net — Test Set Metrics
.metric .estimator .estimate
rmse standard 2510.7066783
rsq standard 0.0893913
mae standard 1867.0876031

1. How many total model fits are required? A 5 × 5 grid across penalty and mixture creates 25 hyperparameter combinations. Evaluating each combination with 10-fold cross-validation requires 250 total model fits.

2. Is the best combination closer to Ridge or Lasso? The best_enet table above shows the winning values for penalty and mixture. A mixture value close to 1 indicates that the tuning process preferred a Lasso-like model, while a value close to 0 indicates more Ridge-like behavior. A value near the middle suggests that a true combination of both penalties worked best for this dataset.

3. Is the additional tuning complexity worth it? I compare final_enet_metrics for the tuned Elastic Net model with final_metrics for the tuned Lasso model in Section 5. If Elastic Net produces only a small RMSE improvement relative to the cross-validation standard errors shown in Section 6, the added tuning complexity is probably not worthwhile. In that case, tuned Lasso would be the more parsimonious and easier-to-explain choice for the group project.

8.5 Exercise 4.5 — Business Interpretation

Using the variable importance plot and coefficient table from the tuned Lasso, identify the three most important predictors and their direction, one predictor Lasso zeroed out that you expected to matter, and the single most actionable lever for a marketing manager.

Code
tuned_coefs_nonzero <- final_lasso_fit |>
  extract_fit_parsnip() |>
  tidy() |>
  filter(term != "(Intercept)", estimate != 0) |>
  arrange(desc(abs(estimate)))

tuned_coefs_zero <- final_lasso_fit |>
  extract_fit_parsnip() |>
  tidy() |>
  filter(term != "(Intercept)", estimate == 0)

top3 <- tuned_coefs_nonzero |> slice(1:3)

top3 |> gt() |> tab_header(title = "Top 3 Predictors \u2014 Tuned Lasso")
Top 3 Predictors — Tuned Lasso
term estimate penalty
product_category_Groceries -1248.6523 0.9236709
product_category_Electronics 664.5418 0.9236709
month_Dec 580.8151 0.9236709
Code
tuned_coefs_zero |> gt() |> tab_header(title = "Predictors Zeroed Out by the Tuned Lasso")
Predictors Zeroed Out by the Tuned Lasso
term estimate penalty
day_of_the_week_Friday 0 0.9236709

1. Top three predictors and direction. The table above lists the three largest nonzero coefficients from the tuned Lasso model. The sign of each coefficient shows whether the predictor is associated with higher or lower revenue relative to the reference level. If discount_percentage and marketing_spend_usd both appear in the top three with positive coefficients, that would directly support our group’s original research question: larger discounts and greater marketing spending are associated with higher revenue, holding the other predictors constant.

2. A predictor I expected to matter that was zeroed out. The “Predictors Zeroed Out” table may include a day-of-week or month dummy that I expected to matter because of weekend traffic or holiday-season demand. Its removal could mean that it adds little predictive value once discount and marketing spend are included. It could also be sharing overlapping information with another retained predictor. For example, if holiday promotions tend to occur in certain months, holiday_effect and a month dummy may capture similar patterns, leading Lasso to keep one and remove the other.

3. The most actionable lever for a marketing manager. Both discount_percentage and marketing_spend_usd are directly controlled by the business, unlike variables such as day_of_the_week or month. Whichever has the larger standardized coefficient in the top-three table would have the stronger association with revenue per standardized unit of change. However, a manager would also need to consider cost. Discounts reduce margin on every unit sold, while marketing spend is a separate expense that does not automatically increase with sales volume. The most useful lever therefore depends on whether the goal is maximizing revenue or protecting profitability.


9 Summary

ImportantKey takeaways
  • Reframing our group’s original discount-and-revenue question as predicting sales_revenue_usd turned it into a clear supervised regression problem.
  • Regularized regression methods, including Ridge, Lasso, and Elastic Net, were a logical starting point because the dataset includes both continuous predictors and dummy-coded categorical variables.
  • The tuned Lasso model, selected through 10-fold cross-validation rather than a manually chosen penalty, served as the main reference model for the variable-importance and business-interpretation sections.
  • discount_percentage and marketing_spend_usd, the two main levers in our group project, were evaluated alongside seasonal and product-category effects in the variable-importance chart (Section 7). This provides the clearest connection between the modeling results and our original project question.

As a blockquote to close: this reflection assignment turned out to be a useful sanity check on the group project itself — restating our AO in machine-learning terms forced a much more specific definition of “the effect of discounts on revenue” than our original proposal language did.


Appendix

9.1 Data Source

9.2 Codebook Reference

9.3 Group Project Repository (Data Source)

9.4 This Assignment (Individual Repo)

Footnotes

  1. Abdullah0a. Retail Sales Data with Seasonal Trends and Marketing (version 1). Kaggle. https://www.kaggle.com/datasets/abdullah0a/retail-sales-data-with-seasonal-trends-and-marketing/versions/1↩︎

  2. Jung, J. Module 4 — Regression-Focused Methods. https://jaejungca.github.io/regression-models/m04_regression_models.html↩︎