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.
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")# 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…
Before drawing any conclusions, the data was checked for structural issues: missing values, duplicate records, correct data types, and outliers.
| 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
## Outlier bounds -> Lower: -746863 | Upper: 2720371
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.
overview <- sales %>%
summarise(
total_sales = sum(weekly_sales),
avg_weekly = mean(weekly_sales),
median_weekly = median(weekly_sales),
sd_weekly = sd(weekly_sales),
n_stores = n_distinct(store),
date_min = min(date),
date_max = max(date))
overviewAcross all 45 stores, total sales for the period reach approximately $6.74 billion, with an average weekly sales figure per store-week of about $1.05 million (median ≈ $961K). The gap between the mean and median, combined with a standard deviation of roughly $564K, indicates a right-skewed distribution — a relatively small number of very high-sales weeks pull the average upward. This is consistent with the outlier pattern identified above.
weekly_trend <- sales %>%
group_by(date) %>%
summarise(total_sales = sum(weekly_sales)) %>%
arrange(date)
ggplot(weekly_trend, aes(x = date, y = total_sales)) +
geom_line(color = "#0071CE", linewidth = 0.9) +
scale_y_continuous(labels = label_dollar(scale = 1e-6, suffix = "M")) +
labs(
title = "Total Weekly Sales Across All Stores (2010–2012)",
x = "Weeks", y = "Weekly Sales"
) +
theme_minimal(base_size = 12) +
theme(title = element_text(face = "bold"))The time series shows sharp, recurring spikes rather than a smooth upward or downward trend — these spikes align with late-November and December weeks (Thanksgiving and Christmas shopping periods) in each of the three years observed. Outside of these peaks, weekly sales fluctuate around a relatively stable baseline, suggesting that underlying demand is steady and the primary source of variation is calendar-driven seasonality, not organic growth or decline.
monthly_seasonality <- sales %>%
group_by(month) %>%
summarise(avg_sales = mean(weekly_sales))
ggplot(monthly_seasonality, aes(x = month, y = avg_sales, fill = month)) +
geom_col() +
scale_y_continuous(labels = label_dollar(scale = 1e-3, suffix = "K")) +
labs(
title = "Average Weekly Sales by Month (Seasonality)",
x = "", y = "Average Weekly Sales"
) +
theme_minimal(base_size = 12) +
theme(title = element_text(face = "bold"), legend.position = "none")December is the clear seasonal peak, with average weekly sales around $1.28M, roughly 39% higher than the weakest month, January (≈$924K). November is the second-strongest month (≈$1.15M), reinforcing that the Thanksgiving-to-Christmas window drives the bulk of seasonal lift. The remaining months (February through October) sit in a much narrower band, roughly $990K–$1.06M, confirming that seasonality in this business is concentrated at year-end rather than spread evenly across the calendar.
yearly_trend <- sales %>%
group_by(year, month) %>%
summarise(total_sales = sum(weekly_sales), .groups = "drop")
ggplot(yearly_trend, aes(x = month, y = total_sales, color = factor(year), group = year)) +
geom_line(linewidth = 1.5, alpha = 0.7) +
scale_y_continuous(labels = label_dollar(scale = 1e-6, suffix = "M")) +
labs(
title = "Monthly Sales Trend by Year",
x = "", y = "Total Sales", color = "Year"
) +
theme_minimal(base_size = 12) +
theme(title = element_text(face = "bold"))Total annual sales were $2.29B in 2010, $2.45B in 2011 (the strongest full year), and $2.00B in 2012. The 2012 figure should not be read as a decline in demand — the dataset only runs through late October 2012, so it excludes the November–December peak period that drives a disproportionate share of annual sales in the other two years. Comparing full calendar years (2010 vs. 2011) is the fairer read, and that comparison shows modest year-over-year growth of roughly 7%.
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):
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.
holiday_comparison <- sales %>%
group_by(holiday_flag) %>%
summarise(
avg_sales = mean(weekly_sales),
median_sales = median(weekly_sales),
n_weeks = n()
)
holiday_comparisonggplot(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
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.
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)None of the four macroeconomic variables show a meaningful linear relationship with weekly sales:
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.
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.