1 Introduction

This report presents an exploratory analysis of Walmart’s weekly sales data across 45 stores, covering the period from February 2010 to October 2012. The objective is to understand overall sales trends, seasonality, store-level performance variation, the impact of holiday weeks, and the relationship between weekly sales and macroeconomic indicators (temperature, fuel price, CPI, and unemployment).

Each section below combines the analysis code with an interpretation of what the output means for the business, so this document can be read as a standalone deliverable rather than requiring the reader to re-run the code to understand the findings.

2 Setup

required_packages <- c("tidyverse", "lubridate", "scales",
                        "corrplot", "janitor", "skimr")

installed <- rownames(installed.packages())
for (pkg in required_packages) {
  if (!(pkg %in% installed)) install.packages(pkg)
}

library(tidyverse)   # data wrangling + ggplot2
library(lubridate)   # date handling
library(scales)      # axis/number formatting
library(corrplot)    # correlation matrix visualization
library(janitor)      # clean column names
library(skimr)        # quick summary stats
setwd("C:/Users/CDD/Desktop/my files/Walmart sales analysis")

3 Data Import

# Update this path to match your local folder structure
Walmart_Sales_csv <- Walmart_Sales_csv <- read_csv("Walmart_Sales_csv.csv") %>% clean_names()


glimpse(Walmart_Sales_csv)
## Rows: 6,435
## Columns: 8
## $ store        <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
## $ date         <chr> "05/02/2010", "12/02/2010", "19/02/2010", "26/02/2010", "…
## $ weekly_sales <dbl> 1643691, 1641957, 1611968, 1409728, 1554807, 1439542, 147…
## $ holiday_flag <dbl> 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
## $ temperature  <dbl> 42.31, 38.51, 39.93, 46.63, 46.50, 57.79, 54.58, 51.45, 6…
## $ fuel_price   <dbl> 2.572, 2.548, 2.514, 2.561, 2.625, 2.667, 2.720, 2.732, 2…
## $ cpi          <dbl> 211.0964, 211.2422, 211.2891, 211.3196, 211.3501, 211.380…
## $ unemployment <dbl> 8.106, 8.106, 8.106, 8.106, 8.106, 8.106, 8.106, 8.106, 7…

4 Data Quality Checks

Before drawing any conclusions, the data was checked for structural issues: missing values, duplicate records, correct data types, and outliers.

# Structure and summary statistics
skim(Walmart_Sales_csv)
Data summary
Name Walmart_Sales_csv
Number of rows 6435
Number of columns 8
_______________________
Column type frequency:
character 1
numeric 7
________________________
Group variables None

Variable type: character

skim_variable n_missing complete_rate min max empty n_unique whitespace
date 0 1 10 10 0 143 0

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
store 0 1 23.00 12.99 1.00 12.00 23.00 34.00 45.00 ▇▇▇▇▇
weekly_sales 0 1 1046964.88 564366.62 209986.25 553350.10 960746.04 1420158.66 3818686.45 ▇▆▂▁▁
holiday_flag 0 1 0.07 0.26 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
temperature 0 1 60.66 18.44 -2.06 47.46 62.67 74.94 100.14 ▁▃▆▇▃
fuel_price 0 1 3.36 0.46 2.47 2.93 3.44 3.73 4.47 ▆▆▇▇▁
cpi 0 1 171.58 39.36 126.06 131.74 182.62 212.74 227.23 ▇▁▁▂▆
unemployment 0 1 8.00 1.88 3.88 6.89 7.87 8.62 14.31 ▂▇▆▁▁
# Missing values by column
missing_summary <- Walmart_Sales_csv %>%
  summarise(across(everything(), ~ sum(is.na(.)))) |>
  pivot_longer(everything(), names_to = "column", values_to = "n_missing")

missing_summary
# Duplicate rows
n_duplicates <- sum(duplicated(Walmart_Sales_csv))
cat("Number of duplicate rows:", n_duplicates, "\n")
## Number of duplicate rows: 0
# Parse date (DD-MM-YYYY) and engineer time-based fields
sales <- Walmart_Sales_csv %>%
  mutate(
    date          = dmy(date),
    holiday_flag  = factor(holiday_flag, levels = c(0, 1),
                            labels = c("Non-Holiday", "Holiday")),
    store         = factor(store),
    year          = year(date),
    month         = month(date, label = TRUE, abbr = TRUE),
    week          = isoweek(date)
  )

# Outlier check on Weekly_Sales (IQR method)
# Note: quartiles are computed on weekly_sales, the variable being tested —
# not on 'week' (the calendar week number), which would test the wrong field.
q1 <- quantile(sales$weekly_sales, 0.25)
q3 <- quantile(sales$weekly_sales, 0.75)
iqr <- q3 - q1
lower_bound <- q1 - 1.5 * iqr
upper_bound <- q3 + 1.5 * iqr

outliers <- sales %>% filter(weekly_sales < lower_bound | weekly_sales > upper_bound)
cat("Number of outlier rows (IQR method):", nrow(outliers), "\n")
## Number of outlier rows (IQR method): 34
cat("Outlier bounds -> Lower:", round(lower_bound, 0),
    "| Upper:", round(upper_bound, 0), "\n")
## Outlier bounds -> Lower: -746863 | Upper: 2720371

4.0.1 Interpretation

The dataset is clean at the structural level: there are no missing values and no duplicate rows across all 6,435 records, so no imputation or deduplication was required. The IQR method flags 34 weeks (about 0.5% of records) as statistically high outliers, all on the upper end — sales spikes above roughly $2.72M in a single store-week. These are not data errors; they correspond to unusually strong sales weeks (consistent with major holiday periods) and were retained rather than removed, since excluding them would understate genuine peak-demand behavior that matters for staffing and inventory planning.

6 Store-Level Performance

store_performance <- sales %>%
  group_by(store) %>%
  summarise(
    total_sales = sum(weekly_sales),
    avg_sales   = mean(weekly_sales),
    sd_sales    = sd(weekly_sales)
  ) %>%
  arrange(desc(total_sales))

top_stores <- store_performance %>% slice_head(n = 10)
ggplot(top_stores, aes(x = reorder(store, total_sales), y = total_sales, fill = store)) +
  geom_col() +
  coord_flip() +
  scale_y_continuous(labels = label_dollar(scale = 1e-6, suffix = "M")) +
  labs(title = "Top 10 Stores by Total Sales", x = "Store", y = "Total Sales") +
  theme_minimal(base_size = 12)

bottom_stores <- store_performance %>% slice_tail(n = 10)
ggplot(bottom_stores, aes(x = reorder(store, total_sales), y = total_sales, fill = store)) +
  geom_col() +
  coord_flip() +
  scale_y_continuous(labels = label_dollar(scale = 1e-6, suffix = "M")) +
  labs(title = "Bottom 10 Stores by Total Sales", x = "Store", y = "Total Sales") +
  theme_minimal(base_size = 12)

# Sales variability (consistency) by store — coefficient of variation
store_performance <- store_performance %>%
  mutate(cv = sd_sales / avg_sales) %>%
  arrange(desc(cv))

cat("Most volatile stores (highest coefficient of variation):\n")
## Most volatile stores (highest coefficient of variation):
head(store_performance, 5)

6.0.1 Interpretation

Store performance varies dramatically across the chain. Store 20 is the top performer with total sales of roughly $301M, closely followed by Store 4 ($300M) and Store 14 ($289M). At the other end, Store 33 totals just $37M and Store 44 totals $43M — an over 8x gap between the strongest and weakest stores. This spread is far too large to be explained by normal week-to-week variation and points to structural differences (store size/format, local market size, or regional demand) rather than operational execution alone.

Volatility tells a different story from raw size: Store 35 has the highest coefficient of variation (≈0.23), meaning its week-to-week sales swing the most relative to its own average — a signal for tighter demand forecasting and buffer stock at that location. By contrast, several lower-volume stores (e.g., Store 37, Store 30) are highly consistent week to week, even though their absolute sales are modest — these are predictable, not underperforming in a way that requires intervention.

7 Holiday Impact Analysis

holiday_comparison <- sales %>%
  group_by(holiday_flag) %>%
  summarise(
    avg_sales = mean(weekly_sales),
    median_sales = median(weekly_sales),
    n_weeks = n()
  )
holiday_comparison
ggplot(sales, aes(x = holiday_flag, y = weekly_sales, fill = holiday_flag)) +
  geom_boxplot(outlier.alpha = 0.3) +
  scale_y_continuous(labels = label_dollar(scale = 1e-3, suffix = "K")) +
  scale_fill_manual(values = c("Non-Holiday" = "#0071CE", "Holiday" = "#FFC220")) +
  labs(
    title = "Weekly Sales Distribution: Holiday vs Non-Holiday Weeks",
    x = "", y = "Weekly Sales"
  ) +
  theme_minimal(base_size = 12) +
  theme(legend.position = "none")

# Statistical test: is the difference significant?
holiday_ttest <- t.test(weekly_sales ~ holiday_flag, data = sales)
holiday_ttest
## 
##  Welch Two Sample t-test
## 
## data:  weekly_sales by holiday_flag
## t = -2.6801, df = 504, p-value = 0.007602
## alternative hypothesis: true difference in means between group Non-Holiday and group Holiday is not equal to 0
## 95 percent confidence interval:
##  -141473.17  -21789.85
## sample estimates:
## mean in group Non-Holiday     mean in group Holiday 
##                   1041256                   1122888

7.0.1 Interpretation

Holiday weeks average $1.12M in sales versus $1.04M for non-holiday weeks — a lift of approximately 7.8%. A Welch two-sample t-test confirms this difference is statistically significant (p ≈ 0.0076, well below the 0.05 threshold), so the holiday effect is a real, reliable pattern rather than noise, even though holiday weeks make up a small fraction of the dataset (450 of 6,435 weeks). This supports treating flagged holiday weeks as a distinct planning category for staffing, inventory, and promotions rather than forecasting them the same way as an ordinary week.

8 External Factors: Correlation Analysis

numeric_vars <- sales %>%
  select(weekly_sales, temperature, fuel_price, cpi, unemployment)

corr_matrix <- cor(numeric_vars, use = "complete.obs")
round(corr_matrix, 3)
##              weekly_sales temperature fuel_price    cpi unemployment
## weekly_sales        1.000      -0.064      0.009 -0.073       -0.106
## temperature        -0.064       1.000      0.145  0.177        0.101
## fuel_price          0.009       0.145      1.000 -0.171       -0.035
## cpi                -0.073       0.177     -0.171  1.000       -0.302
## unemployment       -0.106       0.101     -0.035 -0.302        1.000
corrplot(corr_matrix, method = "color", type = "upper",
         addCoef.col = "black", tl.col = "black", tl.srt = 45,
         title = "Correlation: Weekly Sales vs External Factors",
         mar = c(0, 0, 2, 0))

ggplot(sales, aes(x = temperature, y = weekly_sales)) +
  geom_point(alpha = 0.15, color = "#0071CE") +
  geom_smooth(method = "lm", color = "red", se = FALSE) +
  scale_y_continuous(labels = label_dollar(scale = 1e-3, suffix = "K")) +
  labs(title = "Weekly Sales vs Temperature", x = "Temperature (°F)", y = "Weekly Sales") +
  theme_minimal(base_size = 12) +
  theme(title = element_text(face = "bold"))

ggplot(sales, aes(x = unemployment, y = weekly_sales)) +
  geom_point(alpha = 0.15, color = "#0071CE") +
  geom_smooth(method = "lm", color = "red", se = FALSE) +
  labs(title = "Weekly Sales vs Unemployment Rate", x = "Unemployment (%)", y = "Weekly Sales") +
  theme_minimal(base_size = 12)

ggplot(sales, aes(x = cpi, y = weekly_sales)) +
  geom_point(alpha = 0.15, color = "#0071CE") +
  geom_smooth(method = "lm", color = "red", se = FALSE) +
  labs(title = "Weekly Sales vs CPI", x = "CPI", y = "Weekly Sales") +
  theme_minimal(base_size = 12)

8.0.1 Interpretation

None of the four macroeconomic variables show a meaningful linear relationship with weekly sales:

  • Temperature: r ≈ −0.06 (negligible)
  • Fuel Price: r ≈ +0.01 (negligible)
  • CPI: r ≈ −0.07 (negligible)
  • Unemployment: r ≈ −0.11 (weak at best)

Unemployment has the strongest correlation of the four, but even that is far too weak to be practically useful for forecasting. This is a genuine and important finding, not a gap in the analysis: it indicates that, within the range observed in this dataset, macroeconomic conditions are not meaningful short-term sales drivers for this business. Seasonality and holiday timing (Sections above) are far stronger and more actionable signals than any of these external indicators.

9 Summary of Key Insights

  1. Sales are stable, not trending — the business shows a steady baseline with sharp, predictable seasonal spikes rather than sustained organic growth or decline over the 2010–2012 window.
  2. December and November are the seasonal engine — average weekly sales in December are ~39% above the weakest month (January), and this concentration should anchor annual planning.
  3. Holiday weeks carry a real, statistically significant lift (+7.8% on average, p ≈ 0.008) — small in week-count but consistently impactful.
  4. Store performance is highly uneven — an 8x+ gap between the top and bottom stores, driven more by structural factors (location, format, size) than by short-term operational execution.
  5. Some stores are volatile, others are steady — volatility (CV) should be tracked separately from raw sales volume when deciding where to tighten forecasting.
  6. Macroeconomic indicators are not useful predictors here — temperature, fuel price, CPI, and unemployment all show negligible correlation with weekly sales; forecasting effort is better spent on seasonal and holiday signals.

10 Recommendations

  1. Align inventory and staffing with the seasonal calendar, concentrating incremental resources in November–December rather than spreading them evenly across the year.
  2. Treat holiday weeks as a distinct forecasting category with their own demand model or uplift factor (~+8%), rather than folding them into the general weekly average.
  3. Investigate the root causes behind bottom-decile stores (Store 33, Store 44, and similar) — likely candidates include store size, local market density, and regional competition — before applying any chain-wide fix.
  4. Prioritize demand-forecasting review cycles for high-volatility stores (e.g., Store 35, Store 7, Store 15), where week-to-week swings are largest relative to their own baseline.
  5. De-prioritize macroeconomic indicators in short-term forecasting models given their negligible correlation with sales in this dataset; revisit only if a longer time horizon or more extreme economic conditions are being modeled.
  6. Extend this analysis with a time-series forecasting model (e.g., ETS, ARIMA, or Prophet) per store, using the seasonal and holiday patterns identified here as the primary predictive features.

Report generated as part of an exploratory data analysis exercise on the Walmart weekly sales dataset (Kaggle). All figures reflect data from February 2010 through October 2012.