please visit https://sjbrou.github.io/Supply_Chain_Data_Analysis/ for an interactive version with better visualizations!
Data selection
We analyze, forecast and interpret the Superstore sales provided by Tableau using different statistical and machine learning methods.
The dataset provided contains information about products, sales and profits of a fictitious US company. The dataset contains about 10,000 rows with 1,850 unique product names and 17 product subcategories, covering four consecutive years on a daily basis.
We describe our work in the PDF version. However, we would like to recommend reading our quarto manuscript here as it contains the relevant R code in the Article Notebook.
Data Pre-processing
The superstore data set we selected is of high quality: At first glance (which needs to be verified during the visualization), the data appears to have been recorded regularly and without interruptions. There is no sign of a sudden structural change. Since the data are consumer products, it should contain both trends and seasonality. Nevertheless, we have included hypothetical steps to demonstrate our understanding of the data preprocessing procedure. In detail, we did:
In [1]:
# Clear workspacerm(list =ls())# Function to load (and install if necessary) dependenciesinstall_and_load <-function(packages) {install.packages(setdiff(packages, rownames(installed.packages())), dependencies =TRUE)invisible(lapply(packages, require, character.only =TRUE))}install_and_load(c("tidyverse", "readxl", "ggplot2", "lubridate", "stats", "Amelia","forecast", "tseries", "plotly", "stringr", "knitr", "kableExtra"))
Loading required package: tidyverse
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr 1.1.4 ✔ readr 2.1.5
✔ forcats 1.0.0 ✔ stringr 1.5.1
✔ ggplot2 3.5.1 ✔ tibble 3.2.1
✔ lubridate 1.9.3 ✔ tidyr 1.3.1
✔ purrr 1.0.2
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag() masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
Loading required package: readxl
Loading required package: Amelia
Warning: package 'Amelia' was built under R version 4.3.3
Loading required package: Rcpp
##
## Amelia II: Multiple Imputation
## (Version 1.8.3, built: 2024-11-07)
## Copyright (C) 2005-2024 James Honaker, Gary King and Matthew Blackwell
## Refer to http://gking.harvard.edu/amelia/ for more information
##
Loading required package: forecast
Warning: package 'forecast' was built under R version 4.3.3
Registered S3 method overwritten by 'quantmod':
method from
as.zoo.data.frame zoo
Loading required package: tseries
Warning: package 'tseries' was built under R version 4.3.3
Loading required package: plotly
Attaching package: 'plotly'
The following object is masked from 'package:ggplot2':
last_plot
The following object is masked from 'package:stats':
filter
The following object is masked from 'package:graphics':
layout
Loading required package: knitr
Warning: package 'knitr' was built under R version 4.3.3
Loading required package: kableExtra
Attaching package: 'kableExtra'
The following object is masked from 'package:dplyr':
group_rows
Remove whitespaces from column names
Remove the Row_ID column as it can be inferred by it’s index
Remove all columns with a single unique value, as storing these would be redundant
Ensure machine-readable date formats in yyyy-mm-dd as these usually differ per locale.
Ensure proper decimal separators
Calculate the number of missing values (both NA and empty string ““) per column.
In [2]:
# Load the datasuppressWarnings({data <-read_excel("data/sample_-_superstore.xls")}) # The Postal code column is stored as 'text' but coerced to numeric, causing warnings which we suppress# Improve column namescolnames(data) <-str_replace_all(colnames(data), " ", "_")colnames(data) <-str_replace_all(colnames(data), "-", "_")# Remove the 'Row_ID' column as it can be inferred by it's indexdata <-subset(data, select =-`Row_ID`)# Remove all columns that have only one unique value, as storing these would be redundantdata <- data[, sapply(data, function(col) length(unique(col)) >1)]# Ensure a machine-readable date format as these are usually horrible in excel filesdata$Order_Date <-as.Date(data$Order_Date, format ="%Y-%m-%d")data$Ship_Date <-as.Date(data$Ship_Date, format ="%Y-%m-%d")# The readxl package by default uses the correct decimal separator (as opposed to base R)# Calculate the number of missing values per column.# Origional dates and R date objects are in unix time, which return NA when compared to text (empty string). These dates are stored as 'double' datatype, Thus we check character columns for empty strings, and all columns for NA values. missing_values <-sapply(data, function(col) {if (inherits(col, "Date")) {sum(is.na(col)) } elseif (is.character(col)) {sum(is.na(col) | col =="") } else {sum(is.na(col)) }})# sum(missing_values) returns 0!# Optionally, print the missing values as a nice tablemissing_values_table <-data.frame(Column =names(missing_values),Missing_or_Empty = missing_values)# Note that there are no missing values, thus we do not print them# kable(missing_values_table, caption = "Missing or Empty Values in Columns", format = "pipe")rm(missing_values, missing_values_table)
After these steps (and transposing the table for better document formatting), the data looks as follows:
In [3]:
kable(t(head(data, 3)), caption ="First 3 Rows of the Data (Transposed)", format ="markdown")
First 3 Rows of the Data (Transposed)
Order_ID
CA-2016-152156
CA-2016-152156
CA-2016-138688
Order_Date
2016-11-08
2016-11-08
2016-06-12
Ship_Date
2016-11-11
2016-11-11
2016-06-16
Ship_Mode
Second Class
Second Class
Second Class
Customer_ID
CG-12520
CG-12520
DV-13045
Customer_Name
Claire Gute
Claire Gute
Darrin Van Huff
Segment
Consumer
Consumer
Corporate
City
Henderson
Henderson
Los Angeles
State
Kentucky
Kentucky
California
Postal_Code
42420
42420
90036
Region
South
South
West
Product_ID
FUR-BO-10001798
FUR-CH-10000454
OFF-LA-10000240
Category
Furniture
Furniture
Office Supplies
Sub_Category
Bookcases
Chairs
Labels
Product_Name
Bush Somerset Collection Bookcase
Hon Deluxe Fabric Upholstered Stacking Chairs, Rounded Back
Self-Adhesive Address Labels for Typewriters by Universal
Sales
261.96
731.94
14.62
Quantity
2
3
2
Discount
0
0
0
Profit
41.9136
219.5820
6.8714
We did not find any missing values, confirming the quality of the data set. There is some more processing to do, for instance the removal of outliers. However, by doing so we impose our own assumptions on the data. Let’s start by evaluating the descriptive statistics of our data and check if further processing is required.
In [4]:
descriptive_statistics <-function(column) {if (is.numeric(column)) { stats <-list(Min =min(column, na.rm =TRUE), # Note that handling NA values increases robustness (and I copied the funciton from some of my earlier work)Max =max(column, na.rm =TRUE),Mean =mean(column, na.rm =TRUE),Median =median(column, na.rm =TRUE),StdDev =sd(column, na.rm =TRUE) ) } elseif (inherits(column, "Date")) { stats <-list(Earliest =format(min(column, na.rm =TRUE), "%Y-%m-%d"),Latest =format(max(column, na.rm =TRUE), "%Y-%m-%d") ) } elseif (is.character(column)) { stats <-list(Unique =length(unique(column)),Mode =names(sort(table(column), decreasing =TRUE)[1]) ) } else { stats <-NULL }return(stats)}# Call function on dataframedescriptive_stats <-lapply(data, descriptive_statistics)# Separate to tables dependent on data typenumeric_stats <-as.data.frame(do.call(rbind, lapply(names(data), function(col_name) {if (is.numeric(data[[col_name]])) {c(Column = col_name, descriptive_stats[[col_name]]) }})), stringsAsFactors =FALSE)date_stats <-as.data.frame(do.call(rbind, lapply(names(data), function(col_name) {if (inherits(data[[col_name]], "Date")) {c(Column = col_name, descriptive_stats[[col_name]]) }})), stringsAsFactors =FALSE)character_stats <-as.data.frame(do.call(rbind, lapply(names(data), function(col_name) {if (is.character(data[[col_name]])) {c(Column = col_name, descriptive_stats[[col_name]]) }})), stringsAsFactors =FALSE)
In [5]:
kable( numeric_stats,caption ="Descriptive Statistics for Numeric Columns",format ="pipe")
Descriptive Statistics for Numeric Columns
Column
Min
Max
Mean
Median
StdDev
Postal_Code
1040
99301
55190.38
56430.5
32063.69
Sales
0.444
22638.48
229.858
54.49
623.2451
Quantity
1
14
3.789574
3
2.22511
Discount
0
0.8
0.1562027
0.2
0.206452
Profit
-6599.978
8399.976
28.6569
8.6665
234.2601
kable( date_stats,caption ="Descriptive Statistics for Date Columns",format ="pipe")
Descriptive Statistics for Date Columns
Column
Earliest
Latest
Order_Date
2014-01-03
2017-12-30
Ship_Date
2014-01-07
2018-01-05
We inspect the orders with the lowest and highest Sales amount (in USD). The most expensive orders were professional printers, cameras and teleconferencing units with high unit prices. The orders with the lowest sales amount were often binders and had a high Discount rate.
Interestingly there are orders with a negative profit. They typically have high Discount rates and often concern the same item, such as the “Cubify CubeX 3D Printer Triple Head Print”. The orders with a negative Profit were often part of a larger order (for instance CA-2016-108196), and placed by customers with multiple orders. We suspect these negative Profit’s to be caused by items of lower quality that receive discounts, general discount codes, or volume discounts. However, due to the high discounts especially on orders with negative profit, we assume these to be valid orders.
** Some negative profit products **
In figure x we plotted the quantities of the most sold products. Unfortunately, the sold quantities of individual products were too low to determine any meaningful trends.
In [6]:
# Optionally: print top 10 sale quantity barplot# # Sum of Quantity for top products# top_products <- data %>%# group_by(Product_Name) %>%# summarize(total_quantity = sum(Quantity, na.rm = TRUE)) %>%# arrange(desc(total_quantity)) %>%# slice_head(n = 10) %>% # mutate(ProdName8 = substr(Product_Name, 1, 8)) # Truncate product names to the first 8 characters. Long names mess up formatting# # # Plot# ggplot(top_products, aes(x = reorder(ProdName8, -total_quantity), y = total_quantity)) +# geom_bar(stat = "identity", fill = "steelblue") +# labs(title = "Top 20 Most Sold Products",# x = "Product ID",# y = "Total Quantity") +# theme_minimal() +# coord_flip()# Aggregate quantity by Product Name and Order Datetime_series_data <- data %>%group_by(Product_Name, Order_Date) %>%summarize(total_quantity =sum(Quantity, na.rm =TRUE)) %>%ungroup()
`summarise()` has grouped output by 'Product_Name'. You can override using the
`.groups` argument.
# Filter by total quantity sold top_products <- time_series_data %>%group_by(Product_Name) %>%summarize(total_quantity =sum(total_quantity)) %>%arrange(desc(total_quantity)) %>%slice_head(n =10)# Filter the time-series datafiltered_time_series_data <- time_series_data %>%filter(Product_Name %in% top_products$Product_Name) %>%mutate(ProdName10 =substr(Product_Name, 1, 10)) # Product names can be quite long and mess up layouts. Lets only plot the first 10 chars.# Here we do some special plotting. We want to show the plot with only one selected line by default, but make sure that the other 9 top sold products can be selected. We first create the ggplotly object, and than modify the visibility of the traces
In [7]:
p_ly <-ggplotly(ggplot(filtered_time_series_data, aes(x = Order_Date, y = total_quantity, color = ProdName10)) +geom_line(size =1) +labs(title ="Quantity Sold Over Time per Product",x ="Order Date",y ="Quantity Sold") +theme_minimal() +theme(legend.position ="bottom") +scale_color_discrete(name ="Product Name"))
Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
ℹ Please use `linewidth` instead.
for (i inseq_along(p_ly$x$data)) {if (i ==1) { p_ly$x$data[[i]]$visible <-TRUE } else { p_ly$x$data[[i]]$visible <-"legendonly" }}p_ly
Figure X Sale quantity of the most popular products
Our proposed workaround is to aggregate Product_Name by Sub_Category, and treat it as a single product for the rest of the assignment, which we plotted in figure X.
In [8]:
top_categories <- data %>%count(Sub_Category, sort =TRUE)top_10_categories <- top_categories$Sub_Category[0:10]# Filter the data for top 10 productstop_10_data <- data %>%filter(Sub_Category %in% top_10_categories)# calculate sales per monthtop_10_data <- top_10_data %>%mutate(Month =floor_date(Order_Date, unit ="month"))# Aggregate data by month for each sub-categorytop_10_data_aggregated <- top_10_data %>%group_by(Month, Sub_Category) %>%summarise(Sales_Count =n(), .groups ='drop')# Some special interactive plot formatting (see previous plot)p_ly <-ggplotly(ggplot(top_10_data_aggregated, aes(x = Month, y = Sales_Count, color = Sub_Category, group = Sub_Category)) +geom_line(size =1) +geom_point(size =2) +labs(title ="Monthly Sales for the Top 3 Most Sold Sub Categories",x ="Month",y ="Sales Count",color ="Sub Category") +theme_minimal())for (i inseq_along(p_ly$x$data)) {if (i ==1) { p_ly$x$data[[i]]$visible <-TRUE } else { p_ly$x$data[[i]]$visible <-"legendonly" }}p_ly
This aggregated Quantity starts to show trends and seasonality, and is much more useful to base predictions on! We will use these aggregated sub-categories for the rest of the assignment.
To properly finish our data preprocessing we ran some statistics on Quantity aggregated by Sub_Category. Table x contains some descriptive statistics.
The statistics for Quantity aggregated by Sub_Category looks valid. We can visualize it as histogram and check for anomalies. Figure y contains histograms of Quantity per Sub_Category.
In [10]:
sub_categories <-unique(data$Sub_Category)p <-plot_ly()for (i inseq_along(sub_categories)) { sub <- sub_categories[i] subset_data <- data %>%filter(Sub_Category == sub) p <-add_trace( p,x = subset_data$Quantity,type ="histogram",name = sub,visible =ifelse(i ==1, TRUE, FALSE) )}# We add a drop down menu for Sub_Category as toggling visibility in default ggplot2 adds the histograms up. Instead we want to be able to show each histogram seperately. dropdown_buttons <-lapply(seq_along(sub_categories), function(i) {list(method ="update",args =list(list(visible =lapply(seq_along(sub_categories), function(j) j == i)),list(xaxis =list(title ="Quantity", autorange =TRUE), yaxis =list(title ="Frequency", autorange =TRUE)) ),label = sub_categories[i] )})# Style drop down layoutp <- p %>%layout(title ="Distribution of Quantity Sold per Order by Sub Category",xaxis =list(title ="Quantity"),yaxis =list(title ="Frequency"),showlegend =FALSE, # Drop down instead of legendupdatemenus =list(list(type ="dropdown",buttons = dropdown_buttons,direction ="down",x =0.99,y =0.99,showactive =TRUE,xanchor ="left",yanchor ="top" ) ) )p
The histograms show that the quantities a right-skewed distributed. This is to be expected since most orders contain only a small number of items. We will not remove the outliers with large quantities since they appear valid..
Forecasting Method Evaluation
Forecasting top 3 product categories (4a)
Let’s forecast sold quantities for the three most sold sub-categories:
The steps taken for data preparation were:
Identifying Top Subcategories: The top three subcategories are selected from our dataset based on their sold quantities. The top three were: Binders, furnishing and paper.
The sold quantities are aggregated monthly to create a time series object which we can use in the forecasting.
A KPSS showed that the data is non stationary. First-order differencing is applied to transform the data from non-stationary to stationary. The KPSS results in a p-value >0.05 showing the stationarity.
In [11]:
top_categories <- data %>%group_by(Sub_Category) %>%summarise(Total_Quantity =sum(Quantity)) %>%arrange(desc(Total_Quantity))top_3_subcategories <- top_categories$Sub_Category[0:3]top_3_data <- data %>%filter(Sub_Category %in% top_3_subcategories)# calculate sales per monthtop_3_data <- top_3_data %>%mutate(Month =floor_date(Order_Date, unit ="month"))# Aggregate data by month for each producttop_3_data_aggregated <- top_3_data %>%group_by(Month, Sub_Category) %>%summarise(Sales_Count =n(), .groups ='drop')# Create a time series object for each productts_data <- top_3_data_aggregated %>%pivot_wider(names_from = Sub_Category, values_from = Sales_Count, values_fill =0) %>%select(-Month) %>%as.matrix()ts_data <-ts(ts_data, start =c(2014, 1), end =c(2017, 12), frequency =12) # @Stan This appears to be duplicate code?ts_list <-list()for (subcategory in top_3_subcategories) {# Filter data for the subcategory subcategory_data <- top_3_data_aggregated %>%filter(Sub_Category == subcategory) ts_list[[subcategory]] <-ts(subcategory_data$Sales_Count,start =c(2014, 1),end =c(2017, 12),frequency =12)}# forecasting methods on top 3 sub-categoriesforecast_results <-list()for (subcategory innames(ts_list)) { ts_current <- ts_list[[subcategory]] train_size <-floor(0.7*length(ts_current)) train_ts <-window(ts_current, end =c(2014+ (train_size -1) %/%12, (train_size -1) %%12+1)) test_ts <-window(ts_current, start =c(2014+ train_size %/%12, train_size %%12+1)) arima_model <-auto.arima(train_ts) arima_forecast <-forecast(arima_model, h =length(test_ts)) arima_accuracy <-accuracy(arima_forecast, test_ts) hw_model <-HoltWinters(train_ts) hw_forecast <-forecast(hw_model, h =length(test_ts)) hw_accuracy <-accuracy(hw_forecast, test_ts) ets_model <-ets(train_ts) ets_forecast <-forecast(ets_model, h =length(test_ts)) ets_accuracy <-accuracy(ets_forecast, test_ts) forecast_results[[subcategory]] <-list(ARIMA =list(Model = arima_model, Forecast = arima_forecast, Accuracy = arima_accuracy),HoltWinters =list(Model = hw_model, Forecast = hw_forecast, Accuracy = hw_accuracy),ETS =list(Model = ets_model, Forecast = ets_forecast, Accuracy = ets_accuracy) )}# For formatting, we ommitted almost all output. You can uncomment the code and check your output if you like.# # Step 5: Print results# for (subcategory in names(forecast_results)) {# cat("\n\nResults for Sub_Category:", subcategory, "\n")# # cat("\nARIMA Accuracy:\n")# print(forecast_results[[subcategory]]$ARIMA$Accuracy)# # cat("\nHolt-Winters Accuracy:\n")# print(forecast_results[[subcategory]]$HoltWinters$Accuracy)# # cat("\nETS Accuracy:\n")# print(forecast_results[[subcategory]]$ETS$Accuracy)# }
Three models are applied to each subcategory to forecast it. The models we use are: ARIMA, Holt-Winters and ETS. We have chosen these models because of their level of suitability for discrete time series data with all different levels of trend and seasonality. To evaluate the methods and its effectiveness , the data is split into a training set (70%) and testing set (30%).
To assess the results, we use the following performance metrics: ME, RMSE, MAE and MAPE. They are calculated for the training and testing phases of the forecast.
In [13]:
# Lets plot it as an table# As the relevant data is stored in lists nested in lists nested in lists nested in lists, this was some workresults_table <-data.frame(Sub_Category =character(),Method =character(),Dataset =character(),ME =numeric(),RMSE =numeric(),MAE =numeric(),MPE =numeric(),MAPE =numeric(),MASE =numeric(),ACF1 =numeric(),Theil_U =numeric(),stringsAsFactors =FALSE)for (subcategory innames(forecast_results)) { subcategory_results <- forecast_results[[subcategory]]for (method inc("ARIMA", "HoltWinters", "ETS")) {if (!is.null(subcategory_results[[method]])) { accuracy_metrics <- subcategory_results[[method]]$Accuracyfor (dataset inrownames(accuracy_metrics)) { row <-data.frame(Sub_Category = subcategory,Method = method,Dataset = dataset,ME =round(accuracy_metrics[dataset, "ME"], 3),RMSE =round(accuracy_metrics[dataset, "RMSE"], 3),MAE =round(accuracy_metrics[dataset, "MAE"], 3),MPE =round(accuracy_metrics[dataset, "MPE"], 3),MAPE =round(accuracy_metrics[dataset, "MAPE"], 3),MASE =round(accuracy_metrics[dataset, "MASE"], 3),ACF1 =round(accuracy_metrics[dataset, "ACF1"], 3),Theil_U =ifelse("Theil's U"%in%colnames(accuracy_metrics), round(accuracy_metrics[dataset, "Theil's U"], 3), NA) ) results_table <-rbind(results_table, row) } } }}filtered_results <- results_table[results_table$Dataset =="Training set", ]knitr::kable(filtered_results, format ="html", caption ="Training Set Forecast Accuracy")
Training Set Forecast Accuracy
Sub_Category
Method
Dataset
ME
RMSE
MAE
MPE
MAPE
MASE
ACF1
Theil_U
1
Binders
ARIMA
Training set
0.771
4.643
2.982
0.687
12.912
0.485
0.044
NA
3
Binders
HoltWinters
Training set
0.867
5.215
3.790
-0.404
15.365
0.617
-0.389
NA
5
Binders
ETS
Training set
0.743
4.713
3.656
1.359
15.417
0.595
-0.223
NA
7
Paper
ARIMA
Training set
1.117
6.309
3.828
1.702
14.322
0.547
-0.007
NA
9
Paper
HoltWinters
Training set
1.510
7.280
4.855
3.325
16.413
0.694
-0.027
NA
11
Paper
ETS
Training set
0.609
6.205
4.475
-1.662
18.732
0.639
0.005
NA
13
Furnishings
ARIMA
Training set
0.005
3.874
2.568
-6.625
20.210
0.556
-0.202
NA
15
Furnishings
HoltWinters
Training set
0.914
4.165
3.475
7.490
24.756
0.752
-0.433
NA
17
Furnishings
ETS
Training set
0.737
3.467
2.852
0.430
20.852
0.617
-0.209
NA
As we can see on the forecasting results ARIMA performed well for binders. We can state this because of the lowest RMSE.
For the subcategory furnishings we can see that the ETS forecasting method is the most stable across the training and testing phase.
For the last subcategory and product paper the ETS model is again the most consistent, comparing the statistics for training and test set. The high variability in the test data leads to larger forecasting errors in all the 3 models.
Concerning the residual diagnostics, the checks show no real autocorrelation for ARIMA models. Which indicates a good fitting forecast.
Conclusion (4a)
The most effective model is not the same in all the subcategories. Each model was validated based on its ability to capture seasonality and trend. ARIMA performed better for Binders, while ETS performed better for Furnishings and Paper.
Clustering Subcategories and forecasting (4b)
Steps Taken:
Trend strength, random variation and seasonal strength were calculated for the subcategory using the time series we have.
Clustering:
The clustering method used is the hierarchical clustering method. To group subcategories into three different clusters based on features which are normalized. The hierarchical clustering gave the following results:
Cluster 1: Stronger seasonality
Cluster 2: moderate trend and seasonality
Cluster 3: Lower trend and seasonal strength
For each different cluster we also used the models introduced in 4A (ARIMA, Holt-Winters, ETS). These results where all aggregated at the level of each cluster so we can assess mean RMSE and MAPE for each different model.
top_3_subcategories <- top_categories$Sub_Category[1:3]kpss_results <-data.frame(Sub_Category =character(),KPSS_Statistic =numeric(),P_Value =numeric(),Null_Hypothesis =character(),stringsAsFactors =FALSE)# Perform KPSS Test for each subcategoryfor (subcategory in top_3_subcategories) {if (subcategory %in%names(ts_list)) { ts_current <- ts_list[[subcategory]] test_result <-kpss.test(ts_current) kpss_results <- kpss_results %>%add_row(Sub_Category = subcategory,KPSS_Statistic = test_result$statistic,P_Value = test_result$p.value,Null_Hypothesis =ifelse(test_result$p.value <0.05, "Rejected (Non-Stationary)", "Not Rejected (Stationary)") ) } else {cat("\nSub-Category not found in ts_list:", subcategory, "\n") }}
Warning in kpss.test(ts_current): p-value smaller than printed p-value
Warning in kpss.test(ts_current): p-value smaller than printed p-value
# Print nicely formattted tablekable(kpss_results, caption ="KPSS Test Results for Top 3 Subcategories", digits =3)
KPSS Test Results for Top 3 Subcategories
Sub_Category
KPSS_Statistic
P_Value
Null_Hypothesis
Binders
0.785
0.01
Rejected (Non-Stationary)
Paper
0.738
0.01
Rejected (Non-Stationary)
Furnishings
0.764
0.01
Rejected (Non-Stationary)
In [16]:
options(warn =-1)# # Step 6: Visualization of Forecasts# for (subcategory in names(forecast_results)) {# plot(forecast_results[[subcategory]]$ARIMA$Forecast, main = paste("ARIMA Forecast for", subcategory))# lines(test_ts, col = "red", lty = 2)# # plot(forecast_results[[subcategory]]$HoltWinters$Forecast, main = paste("Holt-Winters Forecast for", subcategory))# lines(test_ts, col = "red", lty = 2)# # plot(forecast_results[[subcategory]]$ETS$Forecast, main = paste("ETS Forecast for", subcategory))# lines(test_ts, col = "red", lty = 2)# }#more stationary tests# Perform KPSS Test for the top 3 subcategories# top_3_subcategories <- top_categories$Sub_Category[0:3]# # for (subcategory in top_3_subcategories) {# if (subcategory %in% names(ts_list)) {# ts_current <- ts_list[[subcategory]]# cat("\nKPSS Test for Sub-Category:", subcategory, "\n")# print(kpss.test(ts_current))# } else {# cat("\nSub-Category not found in ts_list:", subcategory, "\n")# }# }#Because the all the 3 subcategory are non stationary because of a P value which is <=0.05 we need to use differencing# Apply differencing to each of the top 3 subcategoriesdifferenced_series <-list()for (subcategory in top_3_subcategories) {if (subcategory %in%names(ts_list)) { ts_current <- ts_list[[subcategory]] # Get the time series for the subcategory ts_diff <-diff(ts_current, differences =1) # Apply first-order differencing differenced_series[[subcategory]] <- ts_diff # Store the differenced series# Recheck stationarity with KPSS test#cat("\nKPSS Test for Differenced Sub-Category:", subcategory, "\n")print(kpss.test(ts_diff)) } else {#cat("\nSub-Category not found in ts_list:", subcategory, "\n") }}
KPSS Test for Level Stationarity
data: ts_diff
KPSS Level = 0.10182, Truncation lag parameter = 3, p-value = 0.1
KPSS Test for Level Stationarity
data: ts_diff
KPSS Level = 0.061982, Truncation lag parameter = 3, p-value = 0.1
KPSS Test for Level Stationarity
data: ts_diff
KPSS Level = 0.098438, Truncation lag parameter = 3, p-value = 0.1
In [17]:
# Time differenced plots# # Now P value is larger then 0.05 so we have stationary data# # Plot the differenced series for each subcategory# for (subcategory in top_3_subcategories) {# if (subcategory %in% names(differenced_series)) {# ts_diff <- differenced_series[[subcategory]]# cat("\nPlotting Differenced Series for Sub-Category:", subcategory, "\n")# plot(ts_diff, main = paste("Differenced Series for Sub-Category:", subcategory),# ylab = "Differenced Values", xlab = "Time")# }# }# Combine differenced series plots for all top subcategoriescombined_diff_plot <-function(differenced_series, top_3_subcategories) {plot(NULL, xlim =range(time(differenced_series[[top_3_subcategories[1]]])), ylim =range(sapply(differenced_series[top_3_subcategories], range, na.rm =TRUE)),xlab ="Time", ylab ="Differenced Values",main ="Differenced Series for Top Subcategories") colors <-c("blue", "green", "purple")for (i inseq_along(top_3_subcategories)) { subcategory <- top_3_subcategories[i]if (subcategory %in%names(differenced_series)) { ts_diff <- differenced_series[[subcategory]]lines(ts_diff, col = colors[i], lwd =2, lty = i) } }legend("topright", legend = top_3_subcategories, col = colors, lty =1:length(top_3_subcategories), lwd =2)}combined_diff_plot(differenced_series, top_3_subcategories)
# final KPSS test# Perform KPSS Test for the differenced series in the top 3 subcategoriesfor (subcategory in top_3_subcategories) {if (subcategory %in%names(differenced_series)) { ts_current <- differenced_series[[subcategory]] # Get the differenced seriescat("\nKPSS Test for Differenced Sub-Category:", subcategory, "\n")print(kpss.test(ts_current)) } else {cat("\nSub-Category not found in differenced_series:", subcategory, "\n") }}
KPSS Test for Differenced Sub-Category: Binders
KPSS Test for Level Stationarity
data: ts_current
KPSS Level = 0.10182, Truncation lag parameter = 3, p-value = 0.1
KPSS Test for Differenced Sub-Category: Paper
KPSS Test for Level Stationarity
data: ts_current
KPSS Level = 0.061982, Truncation lag parameter = 3, p-value = 0.1
KPSS Test for Differenced Sub-Category: Furnishings
KPSS Test for Level Stationarity
data: ts_current
KPSS Level = 0.098438, Truncation lag parameter = 3, p-value = 0.1
# now they are all 0.1
Clustering (4b)
In [20]:
# 4B# 4B: Group Products into Clusters and Apply Forecasting Techniquestime_series_features <-data.frame()for (subcategory innames(ts_list)) { ts_current <- ts_list[[subcategory]]# Decompose the time series to extract features decomposition <-decompose(ts_current) trend_strength <-var(decomposition$trend, na.rm =TRUE) /var(ts_current, na.rm =TRUE) seasonal_strength <-var(decomposition$seasonal, na.rm =TRUE) /var(ts_current, na.rm =TRUE) random_strength <-var(decomposition$random, na.rm =TRUE) /var(ts_current, na.rm =TRUE)# Store extracted features time_series_features <-rbind(time_series_features,data.frame(SubCategory = subcategory,TrendStrength = trend_strength,SeasonalStrength = seasonal_strength,RandomStrength = random_strength))}# Step 2: Normalize the Features for Clusteringtime_series_features_scaled <- time_series_features %>%select(-SubCategory) %>%scale()# verify rows# nrow(time_series_features_scaled)# Step 3: Perform K-Means Clustering# Determine the number of clusters dynamicallyk <-min(3, nrow(time_series_features_scaled)) # Set k to the smaller of 3 or the number of rows# Hierarchical Clusteringdistance_matrix <-dist(time_series_features_scaled) # Calculate distance matrixhc <-hclust(distance_matrix) # Perform hierarchical clusteringtime_series_features$Cluster <-cutree(hc, k = k) # Cut tree into 'k' clusters# Add cluster information to the original datatime_series_features$Cluster <-cutree(hc, k = k)# Step 4: Apply Forecasting Techniques to Each Clusterforecast_results_by_cluster <-list()for (cluster_id inunique(time_series_features$Cluster)) {# Get subcategories in the current cluster subcategories_in_cluster <- time_series_features$SubCategory[time_series_features$Cluster == cluster_id]# Initialize storage for cluster results cluster_forecast_results <-list()for (subcategory in subcategories_in_cluster) {if (subcategory %in%names(ts_list)) { ts_current <- ts_list[[subcategory]] # Access the time series# Split the data into training and validation sets (70% training, 30% testing) train_size <-floor(0.7*length(ts_current)) train_ts <-window(ts_current, end =c(2014+ (train_size -1) %/%12, (train_size -1) %%12+1)) test_ts <-window(ts_current, start =c(2014+ train_size %/%12, train_size %%12+1))# 1. ARIMA arima_model <-auto.arima(train_ts) arima_forecast <-forecast(arima_model, h =length(test_ts)) arima_accuracy <-accuracy(arima_forecast, test_ts)# 2. Holt-Winters hw_model <-HoltWinters(train_ts) hw_forecast <-forecast(hw_model, h =length(test_ts)) hw_accuracy <-accuracy(hw_forecast, test_ts)# 3. ETS ets_model <-ets(train_ts) ets_forecast <-forecast(ets_model, h =length(test_ts)) ets_accuracy <-accuracy(ets_forecast, test_ts)# Store results for the subcategory cluster_forecast_results[[subcategory]] <-list(ARIMA =list(Model = arima_model, Forecast = arima_forecast, Accuracy = arima_accuracy),HoltWinters =list(Model = hw_model, Forecast = hw_forecast, Accuracy = hw_accuracy),ETS =list(Model = ets_model, Forecast = ets_forecast, Accuracy = ets_accuracy) ) } else {cat("\nSub-Category not found in ts_list:", subcategory, "\n") } }# Store results for the cluster forecast_results_by_cluster[[paste0("Cluster_", cluster_id)]] <- cluster_forecast_results}# Step 5: Print Forecasting Accuracy for Each Clusterfor (cluster_id innames(forecast_results_by_cluster)) {cat("\n\nResults for", cluster_id, "\n") cluster_results <- forecast_results_by_cluster[[cluster_id]]for (subcategory innames(cluster_results)) {cat("\nSub-Category:", subcategory, "\n")cat("\nARIMA Accuracy:\n")print(cluster_results[[subcategory]]$ARIMA$Accuracy)cat("\nHolt-Winters Accuracy:\n")print(cluster_results[[subcategory]]$HoltWinters$Accuracy)cat("\nETS Accuracy:\n")print(cluster_results[[subcategory]]$ETS$Accuracy) }}
Results for Cluster_1
Sub-Category: Binders
ARIMA Accuracy:
ME RMSE MAE MPE MAPE MASE
Training set 0.7706014 4.643476 2.982256 0.6865304 12.91204 0.4854835
Test set 5.9407398 10.783528 7.616473 10.3681817 17.32927 1.2398909
ACF1 Theil's U
Training set 0.04429472 NA
Test set 0.04929320 0.3573866
Holt-Winters Accuracy:
ME RMSE MAE MPE MAPE MASE
Training set 0.8668058 5.215491 3.789990 -0.4040374 15.36545 0.6169751
Test set 2.2473496 8.712049 6.243226 0.1597635 16.02219 1.0163391
ACF1 Theil's U
Training set -0.389045966 NA
Test set -0.001830843 0.2777843
ETS Accuracy:
ME RMSE MAE MPE MAPE MASE
Training set 0.743354 4.712561 3.656409 1.358692 15.41708 0.5952293
Test set 7.439784 12.216161 8.825094 10.667088 20.93561 1.4366433
ACF1 Theil's U
Training set -0.2225537 NA
Test set 0.0608554 0.3767484
Results for Cluster_2
Sub-Category: Paper
ARIMA Accuracy:
ME RMSE MAE MPE MAPE MASE
Training set 1.117384 6.309464 3.827714 1.702165 14.32157 0.5468163
Test set -9.014128 12.230774 10.375521 -30.103886 31.89519 1.4822173
ACF1 Theil's U
Training set -0.007064333 NA
Test set 0.108516273 0.6984566
Holt-Winters Accuracy:
ME RMSE MAE MPE MAPE MASE
Training set 1.509544 7.27986 4.854547 3.325281 16.41309 0.6935067
Test set -12.036865 14.48061 13.347292 -39.791842 41.51609 1.9067560
ACF1 Theil's U
Training set -0.02710216 NA
Test set 0.10006713 0.845232
ETS Accuracy:
ME RMSE MAE MPE MAPE MASE
Training set 0.60941117 6.204628 4.474990 -1.66197953 18.73240 0.6392842
Test set 0.08500424 11.304488 8.247017 -0.08462612 20.47598 1.1781453
ACF1 Theil's U
Training set 0.005205508 NA
Test set 0.341519997 0.582549
Results for Cluster_3
Sub-Category: Furnishings
ARIMA Accuracy:
ME RMSE MAE MPE MAPE MASE
Training set 0.0050974 3.873555 2.567868 -6.624673 20.20958 0.5559302
Test set 3.9523810 7.782677 6.238095 10.720079 22.75340 1.3505155
ACF1 Theil's U
Training set -0.20160077 NA
Test set -0.03570035 0.6102339
Holt-Winters Accuracy:
ME RMSE MAE MPE MAPE MASE
Training set 0.9137655 4.164677 3.475419 7.490317 24.75583 0.7524103
Test set 3.6371987 7.200985 5.673333 9.724318 20.50134 1.2282474
ACF1 Theil's U
Training set -0.43317305 NA
Test set 0.01804785 0.5689788
ETS Accuracy:
ME RMSE MAE MPE MAPE MASE
Training set 0.7370579 3.466690 2.851745 0.4301648 20.85220 0.617388
Test set 6.0973038 8.354163 6.690832 20.7276401 23.55317 1.448531
ACF1 Theil's U
Training set -0.2087083 NA
Test set 0.3729315 0.7455977
## Step 6: Visualize the Clusterslibrary(ggplot2)ggplot(time_series_features, aes(x = TrendStrength, y = SeasonalStrength, color =as.factor(Cluster))) +geom_point(size =3) +labs(title ="Clusters of Subcategories Based on Time-Series Features",x ="Trend Strength", y ="Seasonal Strength", color ="Cluster") +theme_minimal()
Ljung-Box test
data: Residuals from ARIMA(0,1,2)(0,1,0)[12]
Q* = 2.6295, df = 5, p-value = 0.7569
Model df: 2. Total lags used: 7
Residual Diagnostics for Sub-Category: Paper
Ljung-Box test
data: Residuals from ARIMA(0,1,1)(0,1,0)[12]
Q* = 6.6676, df = 6, p-value = 0.3527
Model df: 1. Total lags used: 7
Residual Diagnostics for Sub-Category: Furnishings
Ljung-Box test
data: Residuals from ARIMA(0,0,0)(0,1,0)[12] with drift
Q* = 9.5952, df = 7, p-value = 0.2127
Model df: 0. Total lags used: 7
# P-value is higher then 0.1 so we have stationary data, this is good#cluster level metricscluster_metrics <-data.frame()for (cluster_id innames(forecast_results_by_cluster)) { cluster_results <- forecast_results_by_cluster[[cluster_id]] cluster_rmse <-sapply(cluster_results, function(x) x$ARIMA$Accuracy["Test set", "RMSE"]) cluster_mape <-sapply(cluster_results, function(x) x$ARIMA$Accuracy["Test set", "MAPE"]) cluster_metrics <-rbind(cluster_metrics, data.frame(Cluster = cluster_id, MeanRMSE =mean(cluster_rmse), MeanMAPE =mean(cluster_mape)))}print(cluster_metrics)
Cluster 1 (e.g., Binders): ARIMA outperformed other methods due to significant autocorrelation and trend components.
Cluster 2 (e.g., Furnishings): ETS was the most accurate method, effectively balancing trend and seasonality.
Cluster 3 (e.g., Paper): ETS also performed best, with ARIMA showing higher error rates due to variability in random components.
Residual diagnostics were performed for all ARIMA models, confirming no significant autocorrelation (p > 0.05).
Cluster-Level Metrics based on mean RMSE and MAPE show: - Cluster 1 had the lowest RMSE using ARIMA. - Cluster 2 and 3 were better modeled with ETS
Conclusion (4b)
Clustering allows for tailored forecasting strategies. We conclude that for the given data set ARIMA is more effective for clusters with strong trends, while ETS is preferable for clusters with mixed seasonal and trend characteristics. The approach aligns with lecture notes, emphasizing the importance of adapting models based on time series characteristics.
Forecasting future values
Forecasting 3 products (5a)
In this session, we focused on evaluating different forecasting models (ARIMA, Holt-Winters, and ETS) for multiple sub-categories by analyzing their accuracy metrics, such as RMSE, MAPE, and residual diagnostics. Based on the evaluation results, we selected the best-performing model for each sub-category. We then used these models to forecast the future outcomes for each sub-category, projecting the data for the next year.
ETS(M,A,A)
Call:
ets(y = furnishings_ts)
Smoothing parameters:
alpha = 0.0438
beta = 0.0437
gamma = 2e-04
Initial states:
l = 15.4275
b = -0.1137
s = 13.3158 15.6269 -2.2962 10.1503 -5.0017 -2.448
-3.4406 -1.0728 -1.1262 -4.3688 -11.689 -7.6497
sigma: 0.2527
AIC AICc BIC
338.8888 359.2888 370.6992
Training set error measures:
ME RMSE MAE MPE MAPE MASE
Training set 0.6402485 3.793384 2.884208 -0.8302416 16.2441 0.5352139
ACF1
Training set 0.04613441
ets_forecast <-forecast(ets_model, h =12)print(ets_forecast)
Point Forecast Lo 80 Hi 80 Lo 95 Hi 95
Jan 2018 21.37433 14.45350 28.29515 10.789837 31.95882
Feb 2018 18.56644 12.52244 24.61044 9.322944 27.80994
Mar 2018 27.11574 18.26943 35.96205 13.586481 40.64500
Apr 2018 31.58782 21.22159 41.95404 15.734048 47.44158
May 2018 32.87189 21.95559 43.78819 16.176845 49.56693
Jun 2018 31.73384 20.95052 42.51717 15.242170 48.22551
Jul 2018 33.95710 22.18459 45.72960 15.952604 51.96159
Aug 2018 32.63192 20.83816 44.42569 14.594916 50.66893
Sep 2018 49.01546 31.92061 66.11032 22.871140 75.15979
Oct 2018 37.79884 23.38308 52.21460 15.751844 59.84584
Nov 2018 56.95159 36.44698 77.45621 25.592490 88.31070
Dec 2018 55.87232 34.96477 76.77986 23.896983 87.84765
plot(ets_forecast, main ="ETS Forecast for Furnishings (Next 12 Months)")
Applying to all data (5b)
In this session, we first grouped the sub-categories into clusters based on key time-series features, including trend strength, seasonal strength, and random strength, using hierarchical clustering. Once the clusters were formed, we applied and evaluated multiple forecasting models—ARIMA, Holt-Winters, and ETS—on each sub-category within its respective cluster, comparing their accuracy metrics such as RMSE and MAPE. Based on the evaluation results, we selected the best-performing model for each sub-category and used it to forecast the future outcomes within a year, leveraging the clustering to enhance the accuracy and relevance of our predictions.
In [22]:
#Cluster_Binders->Holt-Winterscluster_id <-1subcategory <-"Binders"hw_model <- forecast_results_by_cluster[[paste0("Cluster_", cluster_id)]][[subcategory]]$HoltWinters$Modelhw_forecast <-forecast(hw_model, h =12)print(hw_forecast)
Point Forecast Lo 80 Hi 80 Lo 95 Hi 95
Oct 2016 31.26733 24.51360 38.02106 20.938398 41.59627
Nov 2016 58.57211 51.81274 65.33147 48.234553 68.90966
Dec 2016 46.52209 39.75006 53.29412 36.165163 56.87901
Jan 2017 20.60516 13.81068 27.39965 10.213895 30.99643
Feb 2017 17.19081 10.36138 24.02023 6.746097 27.63551
Mar 2017 32.40031 25.52088 39.27975 21.879129 42.92150
Apr 2017 36.54419 29.59727 43.49111 25.919798 47.16858
May 2017 42.60581 35.57173 49.63990 31.848113 53.36351
Jun 2017 34.81156 27.66868 41.95444 23.887472 45.73565
Jul 2017 37.46761 30.19266 44.74256 26.341532 48.59368
Aug 2017 38.24466 30.81304 45.67627 26.878978 49.61034
Sep 2017 67.16755 59.55368 74.78141 55.523142 78.81195
plot(hw_forecast, main ="Holt-Winters Forecast for Binders (Next 12 Months)", xlab ="Time", ylab ="Forecasted Values")
#Cluster_paper->ETScluster_id <-2subcategory <-"Paper"ets_model <- forecast_results_by_cluster[[paste0("Cluster_", cluster_id)]][[subcategory]]$ETS$Modelets_forecast <-forecast(ets_model, h =12)print(ets_forecast)
Point Forecast Lo 80 Hi 80 Lo 95 Hi 95
Oct 2016 25.88021 14.987312 36.77312 9.2209588 42.53947
Nov 2016 59.89171 33.023650 86.75978 18.8005561 100.98287
Dec 2016 66.56848 34.945434 98.19153 18.2052047 114.93176
Jan 2017 14.00226 6.995420 21.00910 3.2862229 24.71830
Feb 2017 12.48362 5.931497 19.03574 2.4630129 22.50422
Mar 2017 29.00633 13.095755 44.91690 4.6732073 53.33945
Apr 2017 26.62997 11.410973 41.84896 3.3545236 49.90541
May 2017 42.52022 17.268520 67.77192 3.9010776 81.13936
Jun 2017 35.62513 13.690064 57.56019 2.0783432 69.17191
Jul 2017 25.03347 9.084935 40.98201 0.6422896 49.42466
Aug 2017 29.56385 10.109840 49.01785 -0.1884895 59.31618
Sep 2017 51.80976 16.651800 86.96771 -1.9596973 105.57921
plot(ets_forecast, main ="ETS Forecast for Paper (Next 12 Months)", xlab ="Time", ylab ="Forecasted Values")
#Cluster_Furnishings->Holt-Winterscluster_id <-3subcategory <-"Furnishings"hw_model <- forecast_results_by_cluster[[paste0("Cluster_", cluster_id)]][[subcategory]]$HoltWinters$Modelhw_forecast <-forecast(hw_model, h =12)print(hw_forecast)
Point Forecast Lo 80 Hi 80 Lo 95 Hi 95
Oct 2016 14.89552 9.559730 20.23131 6.735134 23.05590
Nov 2016 36.06019 30.724406 41.39598 27.899811 44.22058
Dec 2016 32.74939 27.413606 38.08518 24.589010 40.90978
Jan 2017 13.37457 8.038784 18.71036 5.214189 21.53496
Feb 2017 12.97277 7.636978 18.30855 4.812382 21.13315
Mar 2017 19.37898 14.043187 24.71476 11.218592 27.53936
Apr 2017 17.92073 12.584940 23.25652 9.760344 26.08111
May 2017 28.71390 23.378112 34.04969 20.553516 36.87428
Jun 2017 18.46131 13.125525 23.79710 10.300929 26.62170
Jul 2017 21.50610 16.170317 26.84189 13.345721 29.66649
Aug 2017 12.73905 7.403259 18.07484 4.578664 20.89943
Sep 2017 37.80356 32.467775 43.13935 29.643179 45.96395
plot(hw_forecast, main ="Holt-Winters Forecast for Furnishings (Next 12 Months)", xlab ="Time", ylab ="Forecasted Values")
Forecast interpretation
In [23]:
# There was some missing pieces of code that I did not want to remove, but added no value either, # Check for missing valuesmissing_values <-colSums(is.na(data))print(missing_values) # Print missing values for reference# heat maplibrary(Amelia)missmap(data, main ="Missing Data Pattern")#distribution of key variables#plot Quantityggplot(data, aes(x = Quantity)) +geom_histogram(binwidth =1, fill ="steelblue") +labs(title ="Distribution of Quantity", x ="Quantity", y ="Frequency") +theme_minimal()#plot salesggplot(data, aes(x = Sales)) +geom_histogram(binwidth =50, fill ="tomato") +labs(title ="Distribution of Sales", x ="Sales", y ="Frequency") +theme_minimal()# plot profitggplot(data, aes(x = Profit)) +geom_histogram(binwidth =10, fill ="purple") +labs(title ="Distribution of Profit", x ="Profit", y ="Frequency") +theme_minimal()# time based trendsdata$Order_Date <-as.Date(data$Order_Date, format ="%Y-%m-%d") # Ensure date formattime_series <- data %>%group_by(Order_Date) %>%summarize(total_sales =sum(Sales), total_profit =sum(Profit), total_quantity =sum(Quantity))ggplot(time_series, aes(x = Order_Date)) +geom_line(aes(y = total_sales, color ="Sales")) +geom_line(aes(y = total_profit, color ="Profit")) +geom_line(aes(y = total_quantity, color ="Quantity")) +labs(title ="Sales, Profit, and Quantity Over Time", x ="Date", y ="Value") +theme_minimal() +scale_color_manual(name ="Metrics", values =c("Sales"="blue", "Profit"="green", "Quantity"="red"))#sales by category and sub categorycategory_sales <- data %>%group_by(Category, Sub_Category) %>%summarize(total_sales =sum(Sales))ggplot(category_sales, aes(x =reorder(Sub_Category, -total_sales), y = total_sales, fill = Category)) +geom_bar(stat ="identity") +labs(title ="Sales by Category and Sub-Category", x ="Sub-Category", y ="Total Sales") +theme_minimal() +coord_flip()#Outliers detection#Quantityggplot(data, aes(x = Category, y = Quantity)) +geom_boxplot() +labs(title ="Boxplot of Quantity by Category", x ="Category", y ="Quantity")#salesggplot(data, aes(x = Category, y = Sales)) +geom_boxplot() +labs(title ="Boxplot of Sales by Category", x ="Category", y ="Sales")#profitggplot(data, aes(x = Category, y = Profit)) +geom_boxplot() +labs(title ="Boxplot of Profit by Category", x ="Category", y ="Profit")#Geo visualizationus_map <-map_data("state")if("State"%in%colnames(data)) { state_sales <- data %>%group_by(State) %>%summarize(total_sales =sum(Sales))# Convert state names to lowercase to match map data state_sales$State <-tolower(state_sales$State)# Merge state sales data with map data state_sales_map <-merge(us_map, state_sales, by.x ="region", by.y ="State", all.x =TRUE)# Plot sales by stateggplot(state_sales_map, aes(long, lat, group = group, fill = total_sales)) +geom_polygon(color ="white") +scale_fill_continuous(low ="lightblue", high ="darkblue", na.value ="gray90") +labs(title ="Sales by State", fill ="Total Sales") +theme_void() +coord_fixed(1.3)}# correlation matrixnumerical_data <- data %>%select(where(is.numeric))cor_matrix <-cor(numerical_data, use ="complete.obs")# Convert the correlation matrix to a long formatcor_data <-as.data.frame(as.table(cor_matrix))# Plot the correlation matrix using ggplot2ggplot(cor_data, aes(Var1, Var2, fill = Freq)) +geom_tile(color ="white") +scale_fill_gradient2(low ="blue", high ="red", mid ="white",midpoint =0, limit =c(-1, 1), space ="Lab",name="Correlation") +geom_text(aes(label =round(Freq, 2)), color ="black", size =4) +theme_minimal() +theme(axis.text.x =element_text(angle =45, vjust =1,size =12, hjust =1)) +coord_fixed() +labs(title ="Correlation Matrix of Key Variables", x ="", y ="")
In [24]:
#Aggregate sales per monthmonthly_sales <- data %>%mutate(Month =floor_date(Order_Date, "month")) %>%group_by(Month) %>%summarize(total_sales =sum(Sales))#Convert to time seriessales_ts <-ts(monthly_sales$total_sales, frequency =12, start =c(year(min(monthly_sales$Month)), month(min(monthly_sales$Month))))#Arima modelarima_model <-auto.arima(sales_ts)arima_forecast <-forecast(arima_model, h =12)autoplot(arima_forecast) +labs(title ="ARIMA Forecast for Monthly Sales")#Holts winter modelhw_model <-HoltWinters(sales_ts)hw_forecast <-forecast(hw_model, h =12)autoplot(hw_forecast) +labs(title ="Holt-Winters Forecast for Monthly Sales")# clustering for segmentationlibrary(cluster)#data clusteringclustering_data <- data %>%select(Sales, Quantity, Discount, Profit) %>%na.omit()set.seed(123)kmeans_model <-kmeans(clustering_data, centers =3)data$Cluster <-as.factor(kmeans_model$cluster)# visualize clustering resultggplot(data, aes(x = Sales, y = Profit, color = Cluster)) +geom_point(alpha =0.6) +labs(title ="K-Means Clustering of Sales and Profit", x ="Sales", y ="Profit") +theme_minimal()