The Effect of Sales Methods on Warehouse Productivity
Author
Nate Carter
Purpose
To estimate how much of an effect the way orders are sold has on warehouse productivity.
The Theory
The ratio of case count to SKU count for an order has a direct influence on the time it will take to complete the order. Positively effecting this ratio will increase efficiency
The Plan
Load and clean the data
Explore the relationships between the quantitative variables.
Explore the relationships between order size and frequency with respect to time to complete.
Establish a baseline for overall productivity.
Transform and model data to make inferences and predictions.
Summarize Findings
Reach conclusion
The Data
The data used for this analysis was collected by Nate Carter during working hours in the Atomic Distribution warehouse. The variables in the data include the date, the number of cases in an order, the number of SKU’s in an order, and the time it took to complete the order. The data set does contain cells with missing data, however it only makes up 7% of the total number of data points and all of them are in the time to complete column. The initial analysis will be run with them removed as they are too infrequent to have a real effect on the results. This data set provides key insights into the variables that effect the the case per minute metric, which has been established by Atomic as the key measure of productivity.
The ‘Date’ data is recorded in mm/dd/yy format.
The number of cases, the ‘Cases’ column is the total number of cases in a single order
The number of SKU’s, the ‘SKU’s’ column, is the total number of individual items in an order
The amount of time to complete an order is the ‘Time_to_complete_HMS’ column.
A completed order is defined as being picked, wrapped, and put away
Time was only measured while picking. During any other activity, such as stocking, call-outs, bathroom breaks, etc; the clock was stopped until picking could resume. This will give insight into the time on task metric, which gives insight into how the work day is divided up.
Also new columns were added for the metrics of cases/minute and complexity during the cleaning process. The date range is almost 6 months, ranging from December 2022, until May 2023.
Clean
Load the data and any packages I will use. In this case the TidyVerse package was used and both the original and the corrected files have been loaded into the session, but the analysis will be conducted with the edited version.
Below is the code I used to call the TidyVerse, Facto Extra, Caret, BRMS, and E1071 packages, and read in my data.
Code
library(tidyverse)library(factoextra)library(caret)library(e1071)library(brms)library(gt)##Edited file created after the cleaning process.order_info_edit<-read_csv("Atomic_case_count_data_Des22-Mar23_edited.csv")## Orginal fileorder_info<-read_csv("Atomic_case_count_data_Des22-Mar23.csv")
Below is the code I used to set the theme information for all my plots and graphs as to keep from typing it out every time a new plot or graph was made.
Make all the column names clear, understandable, and consistent
Convert any columns that need it to their appropriate data types. In this case the date column was converted from a character string into a date time object, and the Time_to_complete_HMS column was converted to a period object. Also the columns for Time to complete in minutes(Time_min), Cases per minute(C_P_M), and Complexity(cmplx) were created.
There were two data entry errors that were discovered during cleaning. These errors caused an NA to be produced when converting them to their proper data type. The errors were due to the original strings not being the correct length. In other words they did not have a two digit hour, minute, and second.
Code
## got warning "In argument: `Time_to_complete_HMS = hms(Time_to_complete_HMS)`.## Caused by warning in `.parse_hms()`:## ! Some strings failed to parse ## Filter the data set by string length to find the strings that ## 'failed to parse'parse_error<-order_info %>%select(Date,Time_to_complete_HMS) %>%na.omit(Time_to_complete_HMS) %>%mutate(time_cor =str_length(Time_to_complete_HMS)<max(str_length(Time_to_complete_HMS))) %>%filter(time_cor==TRUE) %>%print()
# A tibble: 2 × 3
Date Time_to_complete_HMS time_cor
<chr> <chr> <lgl>
1 1/26/2023 0:5:50 TRUE
2 4/24/2023 25:05 TRUE
They were corrected and a new csv file was created preserving the original. The edited csv file was used in cleaning and the processes was run again.
Explore
Question: What relationships have the strongest correlation with cases per minute?
Code
## the correlatin testc_t_cmplx<-order_info_clean %>%na.omit(C_P_M)cor(c_t_cmplx$C_P_M,c_t_cmplx$cmplx)
[1] 0.6717822
A correlation was suspected between cases per minute and the number of SKU’s on an item. This is what employees in the warehouse reported as being one of the most predictive factors in determining how long an order would take. There is a significant relationship between the complexity(number of cases in an order divided by the number of SKU’s in an order), and cases per minute. After performing a correlation test I got 0.6717822 , which demonstrates a positive relationship. This graph also shows that relationship.
Code
order_info_clean %>%select(cmplx, C_P_M) %>%ggplot(aes(C_P_M,cmplx))+geom_point(size=2,colour ="#236ef6",alpha=.5)+geom_smooth(method ="lm")+labs(x="Complexity",y=" Cases per Minute",title ="Complexity vs Cases per Minute",caption="Correlation = 0.6628186\n P Value <2.2e-16")
This indicates that increasing the complexity score on an order by increasing the case count and not the SKU count, doping the SKU’s with only 1 case, etc. will have a positive effect on the case per minute metric. It should be noted that increasing the case per minute rate will not necessarily reduce the amount of total time spent fulfilling orders. It will simply make it easier to do so. This, therefore, makes it one of the significant predictors of time to complete.
Code
c_t_case<-order_info_clean %>%na.omit(C_P_M)
The next relationship that was looked at was the one between cases per minute and the total number of cases in an order. It was thought to seem obvious that case count, which is part of the case per minute metric, would correlate strongly with cases per minute. However after performing a correlation test, I got 0.023416 , which demonstrates that the number of cases in an order has next to no correlation to cases per minute on its own.
Code
order_info_clean %>%ggplot(aes(Cases,C_P_M))+geom_point(size=2,colour="#236ef6",alpha=.5)+labs(x="Total Number of Cases",y="Cases Per Minute",title ="Cases per Minute vs Total cases per Order",caption ="Correlation coefficent=.02341597\nP value=.7732")
This indicates that order size does not affect cases per minute in any meaningful way. It will effect total time. At this point it is suspected that this is due to the distance between SKU’s in the warehouse, the organizational scheme, or perhaps something else entirely.
Code
c_t_sku<-order_info_clean %>%na.omit(C_P_M)
The next relationship to explore was cases per minute vs SKU’s. After performing a correlation test I got -0.2181536. This once again demonstrates that the number of SKU’s on its own, has little correlation on cases per minute. However it is noted that the correlation is negative and much stronger than cases was, and on it’s own can account for only 4% of the cases per minute metric. This lends itself to the theory that the ratio of cases to SKU’s will be a significant factor in effecting warehouse efficiency. In other words the relationship between these two matters.
Code
order_info_clean %>%ggplot(aes(`SKU's`,C_P_M))+geom_point(size=2,colour="#236ef6",alpha=.5)+labs(x="SKU's",y="Cases per Minute",title ="Cases per Minute Vs SKU's")
Answer: The relationship between case count and SKU count, aka Complexity
It has been assumed that the time to complete category is directly correlated with the cases per minute category and was not tested.
Complexity has a positive correlation to cases per minute where as cases and SKU’s on their own have very little. Even though the correlation of cases per minute with cases and SKU was negligible, cases was positive and SKU’s was negative. This means adding one case and one SKU to an order will raise the case count but it will lower and/or have little effect on the complexity. In an example, an order with 56 cases and 23 SKU’s will have a complexity of 2.43, if for what ever reason a new item is added to that order with only one case the complexity drops to 2.38 and if the same item is added to this order with two cases the complexity only drops to 2.42.
Note
Complexity is found by dividing the case count by the number of SKU’s in an order
Question: Where does the highest concentration of orders exist in relation to complexity?
When looking at the number of orders that exist with in specified ranges of complexity this chart shows that about 45% of the orders live the range of 1.43-1.97, and it is noted that the complexity distribution is a gamma distribution with a long right tail. This will come into play later when building the predictive models.
Answer: The most number of orders from this sample is concentrated between a complexity rating of 1.43 and 1.97
Question: Since complexity has a positive correlation with cases per minute does cases have any correlation with complexity.
This seemed obvious but I have already seen a situation in this study where something that seemed obvious wasn’t. When the relationship between complexity is plotted, a somewhat liner relationship is seen. The correlation between complexity and case count is .3504624 . This indicates that the relationship is moderate. However it is strong enough to be considered.
Code
order_info_clean %>%select(Cases, cmplx,`SKU's`) %>%na.omit(cplx) %>%ggplot(aes(Cases,cmplx))+geom_point(size=2,colour="#236ef6",alpha=.5)+geom_smooth(method ="lm", se=FALSE)+labs(y="Complexity",title ="Complexity vs Cases")
Answer: Case count has a measurable positive correlation with complexity.
This means that if an employee’s case count average goes up than the complexity rating of those orders will go up and thus their cases per minute will increase naturally as a function of the current system for order fulfillment. This function of the system already starts to explain why those who maintain a higher case count average will pick at a faster rate than an employee with a lower one all other things being equal. As a result establishing a baseline for current productivity, the cases per minute metric must be in relationship to complexity. Since complexity is a function of case count, smaller orders will naturally tend tword a lower case per minute than larger orders. Therefore in order to more accurately measure productivity cases per minute must be measured on a spectrum, not with a static number.
How does the data naturally group together?
In order to establish the spectrum by which to measure productivity I must be able to group orders together in a way that will make sense to both employees and stake holders alike. So determining how the orders fall into groups naturally is the next step. There are a number of methods that could be applied to this problem, all of which involve unsupervised clustering. The first step is to set up a kmeans model.
In order to estimate K in kmeans I will use the elbow method, by first removing the NA values and filtering out my outlier. Then scaling the data to a mean of 0 and a SD of 1.
Important
At this point in the study it has been determined that the contract for Mark Anthony brands will be sold to another company. The outlier data point, upon investigation, was determined to be made up entirely of Mark Anthony products. Thus it has been removed from the study.
Code
scaled_order_info <- order_info_clean %>%na.omit() %>%select(-Date,-Time_to_complete_HMS,-Time_min,-C_P_M) %>%filter(Cases<=320) %>%##The outlier was filtered outscale()distortions <-c()K_range <-1:10for (k in K_range) { kmeans_model <-kmeans(scaled_order_info, centers = k) distortions <-c(distortions, kmeans_model$tot.withinss)}# Plot the elbowplot(K_range, distortions, type='o', main='Elbow Method for Optimal k',xlab='Number of clusters (k)', ylab='Distortion')
With this method it appears that the 3 to 6 range of clusters should work. So now we plot the clusters and take a look at them.
order_info_clean %>%na.omit() %>%select(everything()) %>%filter(Cases <=320) %>%mutate(cluster=as.factor(km_6$cluster)) %>%ggplot(aes(x=Cases,fill=cluster))+geom_density(alpha=.5)+labs(title ="Dencity Plot of Cases by Cluster", x="Cases", y="Density")
Code
order_info_clean %>%na.omit() %>%select(everything()) %>%filter(Cases <=320) %>%mutate(cluster=as.factor(km_6$cluster)) %>%ggplot(aes(x=`SKU's`,fill=cluster))+geom_density(alpha=.5)+labs(title ="Dencity Plot of SKU's by cluster",x="SKU's", y="Density")
As seen above the range of 3 - 6 clusters did not yield favorable results. This is especially obvious in the density plots of cases and Sku’s. Therefore I will expand the search to include 2 and 7 clusters. This should help determine, if at all, which direction is appropriate.
The plots above show the best separation of the orders with 2 clusters. The next questions that come to mind is what is the difference between the two clusters and how will clustering the data help with model prediction? In the case per minute metric and complexity metrics the variation is small but it is there. How these differences will play out in the aggregate will be likely be observed in simulations.
Code
order_info_clstrd %>%ggplot(aes(x=cluster,y=C_P_M))+geom_boxplot(fill ="#236ef6",outlier.colour ="red")+labs(title ="Summary by Cluster of Cases per Minute")
order_info_clstrd %>%ggplot(aes(x=cluster,y=Time_min))+geom_boxplot(fill ="#236ef6",outlier.colour ="red")+labs(title ="Summary by Cluster of Time in Minutes")
order_info_clstrd %>%ggplot(aes(x=cluster,y=Cases))+geom_boxplot(fill ="#236ef6",outlier.colour ="red")+labs(title ="Summary by Cluster of Cases Count")
order_info_clstrd %>%ggplot(aes(x=cluster,y=`SKU's`))+geom_boxplot(fill ="#236ef6",outlier.colour ="red")+labs(title ="Summary by Cluster of SKU Count")
order_info_clstrd %>%ggplot(aes(x=cluster,y=cmplx))+geom_boxplot(fill ="#236ef6",outlier.colour ="red")+labs(title ="Summary by Cluster of Complexity")
Since the goal of establishing a more accurate measure of productivity that can be easily applied by staff and stake holders alike, the separation of the clusters by case and Sku count will be used. This way when some one looks at an order determining which cluster it belongs to will be more intuitive. However, the regression that predicts the time it will take will incorporate the complexity metric as well. Using kmeans to cluster the data has yielded two clusters that will work best going forward.
What I know so far
The complexity metric has the highest correlation with the cases per minute metric. Therefore I should be able to use complexity plus case count to predict total time.
The the best results from clustering come from two clusters, and those two clusters are highly dependent on case count and SKU count. Therefore I use case count, plus SKU count to classify the orders.
An ensemble model of a classification algorithm and a liner regression algorithm should be the best way to predict time to complete.
The overall effect of sales methodology will be observed in the aggregate and not necessarily on an order by order basis.
Build the Model
Goal of the model
The model will be used to used to create a base line productivity metric which can be then used to asses how much a change in the system has effected efficiency in simulated situations.
I have selected a naive Bayes classier to determine the cluster new orders will belong to, then I have selected a Bayesian Liner Regression model to predict the time based on the cluster.
I have built, trained, and tested my naive Bayes classifier model based on the results from the kmeans clustering algorithm with 2 clusters.
Important
Due to the quite distinct separation from the kmeans clustering it is expected for the model to achieve a high accuracy. These results, I believe, are acceptable.
To break this down here are the steps I took
a seed was set
Code
set.seed(42)##because I'm a hitch hikers fan!
an empty list of length 100 was created to store the results of the cross validation.
Code
cv_vec <-vector("list", length =100)
A custom function was written to randomly sample 80% of the data to be used for training and the rest of the data to be used for testing and return the test data frame to the empty list.
A loop was constructed to perform this function over each of the indices of the empty list.
Code
for (i inseq_along(cv_vec)) { cv_vec[[i]] <-cv_fun()}
The data frames were combined at the end.
Code
mdl_rslts <-bind_rows(cv_vec)
A confusion matrix was created for evaluation.
Code
conf_mtrx <-table(mdl_rslts$mdl_prd,mdl_rslts$cluster)conf_mtrx_obj <-confusionMatrix(conf_mtrx)# Extract confusion matrix tableconf_matrix_table <- conf_mtrx_obj$table# Plot a confusion matrix using ggplot2ggplot(data =as.data.frame(conf_matrix_table),aes(x = (Var2), y = (Var1), fill = Freq)) +geom_tile(color ="#236ef6") +scale_fill_gradient(low ="#a2c2fc", high ="blue") +labs(title ="Confusion Matrix",x ="Actual",y ="Predicted",fill ="Frequency") +geom_text(aes(label =sprintf("%d", Freq)), vjust =1) +scale_x_discrete(position ="top")+scale_y_discrete(limits=c("2","1"))
Code
print(conf_mtrx_obj)
Confusion Matrix and Statistics
1 2
1 1932 6
2 23 1139
Accuracy : 0.9906
95% CI : (0.9866, 0.9937)
No Information Rate : 0.6306
P-Value [Acc > NIR] : < 2.2e-16
Kappa : 0.98
Mcnemar's Test P-Value : 0.002967
Sensitivity : 0.9882
Specificity : 0.9948
Pos Pred Value : 0.9969
Neg Pred Value : 0.9802
Prevalence : 0.6306
Detection Rate : 0.6232
Detection Prevalence : 0.6252
Balanced Accuracy : 0.9915
'Positive' Class : 1
It is noted that these results may be “too good” and the model may be over fit. However it is also noted that since the clusters have very distinct boundaries this was also to be expected. I will proceed with building, training, and testing of the liner model. If I cannot achieve acceptable accuracy then I will start the trouble shooting here with the classifier model.
The next step is to build and train a BRMS model for each cluster.
First, separate the data into individual sets by their cluster assignment and seperate each cluster into training and testing data.
In the results of the test are not good enough to make any reliable decisions from. I have concluded that in order to control for distance traveled I will need to develop a metric that represents distance. In order to do this I will use the PDF scans of 50+ orders that were taken from the warehouse, assign each item a aisle in the warehouse using the center of the warehouse as 0 and assign positive and negative values for each aisle. Just like taking a birds eye view of the warehouse and placing a number line on it and then using an absolute value calculation to represent distance.