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

  1. Load and clean the data

  2. Explore the relationships between the quantitative variables.

  3. Explore the relationships between order size and frequency with respect to time to complete.

  4. Establish a baseline for overall productivity.

  5. Transform and model data to make inferences and predictions.

  6. Summarize Findings

  7. 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

  1. 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 file
    order_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.

    Code
    theme_set(theme_bw()+
                theme(plot.title = 
                        element_text(size = 22,
                                     color = "steelblue"),
                      axis.title  = element_text(size = 20,
                                               color = "steelblue"),
                      axis.text = element_text(size=14,
                      face="bold")))
  2. Make all the column names clear, understandable, and consistent

  3. 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.

    Cases per min= Cases/Tim_min

    Complexity= Cases/SKU’s

    Code
    order_info_clean<-order_info_edit %>% 
      mutate(Time_to_complete_HMS = hms(Time_to_complete_HMS),
             Time_min = (period_to_seconds(Time_to_complete_HMS)/60),
             .after = Time_to_complete_HMS,
             Date=parse_date_time(Date,"mdy"),
             C_P_M=Cases/Time_min, cmplx= Cases/`SKU's`)
  4. 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 test
c_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?
Code
order_info_clean %>% 
  select(Cases,cmplx) %>% 
  mutate(ranges= cut(cmplx, breaks=seq(1,4,.25))) %>% 
  na.omit() %>% 
  group_by(ranges,Cases) %>% 
  count(Cases) %>%
  ggplot(aes(ranges,n))+
  geom_bar(stat = "identity",fill="#236ef6")+
  labs(x="Complexity Ranges",
       y="Count",
       title = "Number of Orders in Complexity Ranges")+
  scale_x_discrete(breaks = waiver(),
                   labels= c("1-\n1.24","1.25-\n1.4","1.5-\n1.74","1.75-\n1.9",
                             "2-\n2.24","2.25-\n2.4","2.5-\n2.74","2.75-\n2.9",
                             "3-\n3.24","3.25-\n3.4","3.5-\n3.74","3.74-\n3.9"))+
  theme(plot.title = 
          element_text(size = 22,
                       color = "steelblue"),
        axis.title  = element_text(size = 20,
                                   color = "steelblue"),
        axis.text = element_text(size=10,
                                 face="bold"))

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.
Code
c_t_cs_vs_cmplx<-order_info_clean %>% 
  na.omit(C_P_M)
cor(c_t_cs_vs_cmplx$cmplx,c_t_cs_vs_cmplx$Cases)
[1] 0.3520107

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 out
  scale()
  

distortions <- c()
K_range <- 1:10

for (k in K_range) {
  kmeans_model <- kmeans(scaled_order_info, centers = k)
  distortions <- c(distortions, kmeans_model$tot.withinss)
}

# Plot the elbow
plot(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.

Three clusters
Code
km_3 <- kmeans(scaled_order_info,3,iter.max = 100,nstart = 100)

fviz_cluster(km_3,scaled_order_info,main = "Scaled Plot")

Code
order_info_clean %>% 
  na.omit() %>% 
  select(everything()) %>% 
  filter(Cases <= 320) %>%
  mutate(cluster= as.factor(km_3$cluster)) %>% 
  ggplot(aes(x=Cases,y=`SKU's`))+
  geom_tile(aes(fill=cluster,height=5,width=7))+
  labs(title = "Clusters Assigned to Orders")

Code
order_info_clean %>% 
  na.omit() %>% 
  select(everything()) %>% 
  filter(Cases <= 320) %>%
  mutate(cluster= as.factor(km_3$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_3$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")

Four clusters
Code
km_4 <- kmeans(scaled_order_info,4,iter.max = 100,nstart = 100)

fviz_cluster(km_4,scaled_order_info)

Code
order_info_clean %>% 
  na.omit() %>% 
  select(everything()) %>% 
  filter(Cases <= 320) %>%
  mutate(cluster= as.factor(km_4$cluster)) %>% 
  ggplot(aes(x=Cases,y=`SKU's`))+
  geom_tile(aes(fill=cluster,height=5,width=7))+
  labs(title = "Clusters Assigned to Orders")

Code
order_info_clean %>% 
  na.omit() %>% 
  select(everything()) %>% 
  filter(Cases <= 320) %>%
  mutate(cluster= as.factor(km_4$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_4$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")

Five clusters
Code
km_5 <- kmeans(scaled_order_info,5,iter.max = 100,nstart = 100)

fviz_cluster(km_5,scaled_order_info)

Code
order_info_clean %>% 
  na.omit() %>% 
  select(everything()) %>% 
  filter(Cases <= 320) %>%
  mutate(cluster= as.factor(km_5$cluster)) %>% 
  ggplot(aes(x=Cases,y=`SKU's`))+
  geom_tile(aes(fill=cluster,height=5,width=7))+
  labs(title = "Clusters Assigned to Orders")

Code
order_info_clean %>% 
  na.omit() %>% 
  select(everything()) %>% 
  filter(Cases <= 320) %>%
  mutate(cluster= as.factor(km_5$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_5$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")

Six clusters
Code
km_6 <- kmeans(scaled_order_info,6,iter.max = 100,nstart = 100)

fviz_cluster(km_6,scaled_order_info)

Code
order_info_clean %>% 
  na.omit() %>% 
  select(everything()) %>% 
  filter(Cases <= 320) %>%
  mutate(cluster= as.factor(km_6$cluster)) %>% 
  ggplot(aes(x=Cases,y=`SKU's`))+
  geom_tile(aes(fill=cluster,height=5,width=7))+
  labs(title = "Clusters Assigned to Orders")

Code
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.

Two Clusters
Code
km_2 <- kmeans(scaled_order_info,2,iter.max = 100,nstart = 100)

fviz_cluster(km_2,scaled_order_info,main = "Scaled Plot")

Code
order_info_clean %>% 
  na.omit() %>% 
  select(everything()) %>% 
  filter(Cases <= 320) %>%
  mutate(cluster= as.factor(km_2$cluster)) %>% 
  ggplot(aes(x=Cases,y=`SKU's`))+
  geom_tile(aes(fill=cluster,height=5,width=7))+
  labs(title = "Clusters Assigned to Orders")

Code
order_info_clean %>% 
  na.omit() %>% 
  select(everything()) %>% 
  filter(Cases <= 320) %>%
  mutate(cluster= as.factor(km_2$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_2$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")

Seven Clusters
Code
km_7 <- kmeans(scaled_order_info,7,iter.max = 100,nstart = 100)

fviz_cluster(km_7,scaled_order_info,main = "Scaled Plot")

Code
order_info_clean %>% 
  na.omit() %>% 
  select(everything()) %>% 
  filter(Cases <= 320) %>%
  mutate(cluster= as.factor(km_7$cluster)) %>% 
  ggplot(aes(x=Cases,y=`SKU's`))+
  geom_tile(aes(fill=cluster,height=5,width=7))+
  labs(title = "Clusters Assigned to Orders")

Code
order_info_clean %>% 
  na.omit() %>% 
  select(everything()) %>% 
  filter(Cases <= 320) %>%
  mutate(cluster= as.factor(km_7$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_7$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")

After the kmeans model producing two clusters was chosen, the clusters were assigned to the data.

Code
order_info_clstrd <- order_info_clean %>% 
  na.omit() %>% 
  select(everything()) %>% 
  filter(Cases <= 320) %>% 
  mutate(cluster= as.factor(km_2$cluster))

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")

Code
order_info_clstrd %>% 
  select(C_P_M,cluster) %>% 
  group_by(cluster) %>% 
  summarise(mean = mean(C_P_M),median = median(C_P_M)) %>% 
  gt() %>% 
  cols_label(cluster ~ "Cluster", mean~ "Avg C_P_M", median ~ "Median C_P_M")
Cluster Avg C_P_M Median C_P_M
1 1.958104 1.881657
2 1.899288 1.867945
Code
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")

Code
order_info_clstrd %>% 
  select(Time_min,cluster) %>% 
  group_by(cluster) %>% 
  summarise(mean = mean(Time_min),median = median(Time_min)) %>% 
  gt() %>% 
  cols_label(cluster ~ "Cluster", mean~ "Avg Time", median ~ "Median Time")
Cluster Avg Time Median Time
1 22.13351 23.08333
2 51.84702 50.33333
Code
order_info_clstrd %>% 
  ggplot(aes(x=cluster,y=Cases))+
  geom_boxplot(fill = "#236ef6",outlier.colour = "red")+
  labs(title = "Summary by Cluster of Cases Count")

Code
order_info_clstrd %>% 
  select(Cases,cluster) %>% 
  group_by(cluster) %>% 
  summarise(mean = mean(Cases),median = median(Cases)) %>% 
  gt() %>% 
  cols_label(cluster ~ "Cluster", mean~ "Avg Cases", median ~ "Median Cases")
Cluster Avg Cases Median Cases
1 42.26804 45
2 96.50000 91
Code
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")

Code
order_info_clstrd %>% 
  select(`SKU's`,cluster) %>% 
  group_by(cluster) %>% 
  summarise(mean = mean(`SKU's`),median = median(`SKU's`)) %>% 
  gt() %>% 
  cols_label(cluster ~ "Cluster", mean~ "Avg SKU's", median ~ "Median SKU's")
Cluster Avg SKU's Median SKU's
1 22.37113 23
2 45.89286 44
Code
order_info_clstrd %>% 
  ggplot(aes(x=cluster,y=cmplx))+
  geom_boxplot(fill = "#236ef6",outlier.colour = "red")+
  labs(title = "Summary by Cluster of Complexity")

Code
order_info_clstrd %>% 
  select(cmplx,cluster) %>% 
  group_by(cluster) %>% 
  summarise(mean = mean(cmplx),median = median(cmplx)) %>% 
  gt() %>% 
  cols_label(cluster ~ "Cluster", mean~ "Avg Cmplx", median ~ "Median Cmplx")
Cluster Avg Cmplx Median Cmplx
1 1.944819 1.875000
2 2.175072 2.089103

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

  1. 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.

  2. 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.

  3. An ensemble model of a classification algorithm and a liner regression algorithm should be the best way to predict time to complete.

  4. 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

  1. a seed was set

    Code
    set.seed(42)##because I'm a hitch hikers fan!
  2. an empty list of length 100 was created to store the results of the cross validation.

    Code
    cv_vec <- vector("list", length = 100)
  3. 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.

    Code
    cv_fun <- function(
        data = order_info_clstrd)
      {
      train_proportion <- .8
      num_rows <- nrow(data)
      num_train_rows <- round(train_proportion*num_rows)
    
      train_indicies <- sample(1:num_rows,size = num_train_rows,replace = FALSE)
    
    
    
        training_data <- data %>% 
      select(Cases,`SKU's`,cmplx,cluster) %>% 
      slice(train_indicies)
    
    
    
      test_data <- data %>% 
      select(Cases, `SKU's`, cmplx, cluster) %>% 
      slice(-train_indicies)
    
      cluster_model <- naiveBayes(cluster ~ Cases + `SKU's`, data = training_data)
    
      clstr_pred <- predict(cluster_model,test_data)
    
      return(test_data_pred <- test_data %>% 
             mutate(mdl_prd=clstr_pred,
             mdl_evl=mdl_prd==cluster))
    
    }
  4. A loop was constructed to perform this function over each of the indices of the empty list.

    Code
    for (i in seq_along(cv_vec)) {
      cv_vec[[i]] <- cv_fun()
    } 
  5. The data frames were combined at the end.

    Code
    mdl_rslts <- bind_rows(cv_vec)
  6. 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 table
conf_matrix_table <- conf_mtrx_obj$table

# Plot a confusion matrix using ggplot2
ggplot(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.

Code
#Test the model
order_info_clstr_1 <- order_info_clstrd %>% 
  na.omit() %>%
  select(Cases,`SKU's`,cmplx,cluster,C_P_M,Time_min) %>% 
  filter(cluster == 1) 

order_info_clstr_2 <- order_info_clstrd %>% 
  na.omit() %>%
  select(Cases,`SKU's`,cmplx,cluster,C_P_M,Time_min) %>% 
  filter(cluster == 2) 


  train_proportion <- .8
  
  num_rows <- nrow(order_info_clstr_2)
  
  num_train_rows <- round(train_proportion*num_rows)

  train_indicies <- sample(1:num_rows,size = num_train_rows,replace = FALSE)



training_data_brm_1 <- order_info_clstr_1 %>% 
  slice(train_indicies)



test_data_brm_1 <- order_info_clstr_1 %>% 
  slice(-train_indicies)

The second step is to set the prior distributions on my population level parameters.

Code
prior_set <- set_prior("normal(0,1)", class= "b")

Third is to write the model.

Code
liner_model1 <- brm(C_P_M~ cmplx ,
                    data = training_data_brm_1,
                    prior = prior_set)
Compiling Stan program...
Start sampling

SAMPLING FOR MODEL 'anon_model' NOW (CHAIN 1).
Chain 1: 
Chain 1: Gradient evaluation took 4.1e-05 seconds
Chain 1: 1000 transitions using 10 leapfrog steps per transition would take 0.41 seconds.
Chain 1: Adjust your expectations accordingly!
Chain 1: 
Chain 1: 
Chain 1: Iteration:    1 / 2000 [  0%]  (Warmup)
Chain 1: Iteration:  200 / 2000 [ 10%]  (Warmup)
Chain 1: Iteration:  400 / 2000 [ 20%]  (Warmup)
Chain 1: Iteration:  600 / 2000 [ 30%]  (Warmup)
Chain 1: Iteration:  800 / 2000 [ 40%]  (Warmup)
Chain 1: Iteration: 1000 / 2000 [ 50%]  (Warmup)
Chain 1: Iteration: 1001 / 2000 [ 50%]  (Sampling)
Chain 1: Iteration: 1200 / 2000 [ 60%]  (Sampling)
Chain 1: Iteration: 1400 / 2000 [ 70%]  (Sampling)
Chain 1: Iteration: 1600 / 2000 [ 80%]  (Sampling)
Chain 1: Iteration: 1800 / 2000 [ 90%]  (Sampling)
Chain 1: Iteration: 2000 / 2000 [100%]  (Sampling)
Chain 1: 
Chain 1:  Elapsed Time: 0.022 seconds (Warm-up)
Chain 1:                0.024 seconds (Sampling)
Chain 1:                0.046 seconds (Total)
Chain 1: 

SAMPLING FOR MODEL 'anon_model' NOW (CHAIN 2).
Chain 2: 
Chain 2: Gradient evaluation took 5.2e-05 seconds
Chain 2: 1000 transitions using 10 leapfrog steps per transition would take 0.52 seconds.
Chain 2: Adjust your expectations accordingly!
Chain 2: 
Chain 2: 
Chain 2: Iteration:    1 / 2000 [  0%]  (Warmup)
Chain 2: Iteration:  200 / 2000 [ 10%]  (Warmup)
Chain 2: Iteration:  400 / 2000 [ 20%]  (Warmup)
Chain 2: Iteration:  600 / 2000 [ 30%]  (Warmup)
Chain 2: Iteration:  800 / 2000 [ 40%]  (Warmup)
Chain 2: Iteration: 1000 / 2000 [ 50%]  (Warmup)
Chain 2: Iteration: 1001 / 2000 [ 50%]  (Sampling)
Chain 2: Iteration: 1200 / 2000 [ 60%]  (Sampling)
Chain 2: Iteration: 1400 / 2000 [ 70%]  (Sampling)
Chain 2: Iteration: 1600 / 2000 [ 80%]  (Sampling)
Chain 2: Iteration: 1800 / 2000 [ 90%]  (Sampling)
Chain 2: Iteration: 2000 / 2000 [100%]  (Sampling)
Chain 2: 
Chain 2:  Elapsed Time: 0.025 seconds (Warm-up)
Chain 2:                0.022 seconds (Sampling)
Chain 2:                0.047 seconds (Total)
Chain 2: 

SAMPLING FOR MODEL 'anon_model' NOW (CHAIN 3).
Chain 3: 
Chain 3: Gradient evaluation took 8e-06 seconds
Chain 3: 1000 transitions using 10 leapfrog steps per transition would take 0.08 seconds.
Chain 3: Adjust your expectations accordingly!
Chain 3: 
Chain 3: 
Chain 3: Iteration:    1 / 2000 [  0%]  (Warmup)
Chain 3: Iteration:  200 / 2000 [ 10%]  (Warmup)
Chain 3: Iteration:  400 / 2000 [ 20%]  (Warmup)
Chain 3: Iteration:  600 / 2000 [ 30%]  (Warmup)
Chain 3: Iteration:  800 / 2000 [ 40%]  (Warmup)
Chain 3: Iteration: 1000 / 2000 [ 50%]  (Warmup)
Chain 3: Iteration: 1001 / 2000 [ 50%]  (Sampling)
Chain 3: Iteration: 1200 / 2000 [ 60%]  (Sampling)
Chain 3: Iteration: 1400 / 2000 [ 70%]  (Sampling)
Chain 3: Iteration: 1600 / 2000 [ 80%]  (Sampling)
Chain 3: Iteration: 1800 / 2000 [ 90%]  (Sampling)
Chain 3: Iteration: 2000 / 2000 [100%]  (Sampling)
Chain 3: 
Chain 3:  Elapsed Time: 0.023 seconds (Warm-up)
Chain 3:                0.023 seconds (Sampling)
Chain 3:                0.046 seconds (Total)
Chain 3: 

SAMPLING FOR MODEL 'anon_model' NOW (CHAIN 4).
Chain 4: 
Chain 4: Gradient evaluation took 1.1e-05 seconds
Chain 4: 1000 transitions using 10 leapfrog steps per transition would take 0.11 seconds.
Chain 4: Adjust your expectations accordingly!
Chain 4: 
Chain 4: 
Chain 4: Iteration:    1 / 2000 [  0%]  (Warmup)
Chain 4: Iteration:  200 / 2000 [ 10%]  (Warmup)
Chain 4: Iteration:  400 / 2000 [ 20%]  (Warmup)
Chain 4: Iteration:  600 / 2000 [ 30%]  (Warmup)
Chain 4: Iteration:  800 / 2000 [ 40%]  (Warmup)
Chain 4: Iteration: 1000 / 2000 [ 50%]  (Warmup)
Chain 4: Iteration: 1001 / 2000 [ 50%]  (Sampling)
Chain 4: Iteration: 1200 / 2000 [ 60%]  (Sampling)
Chain 4: Iteration: 1400 / 2000 [ 70%]  (Sampling)
Chain 4: Iteration: 1600 / 2000 [ 80%]  (Sampling)
Chain 4: Iteration: 1800 / 2000 [ 90%]  (Sampling)
Chain 4: Iteration: 2000 / 2000 [100%]  (Sampling)
Chain 4: 
Chain 4:  Elapsed Time: 0.022 seconds (Warm-up)
Chain 4:                0.023 seconds (Sampling)
Chain 4:                0.045 seconds (Total)
Chain 4: 

Lastly, the model is applied to the testing data and the results are viewed.

Code
pred_mdl <- predict(liner_model1,test_data_brm_1)

test_data_brm_1 %>%
  select(everything()) %>% 
  mutate(pred_cpm = pred_mdl[,1]) %>% 
  mutate(pred_time= Cases / pred_cpm,.after = Time_min) %>% 
  gt()
Cases SKU's cmplx cluster C_P_M Time_min pred_time pred_cpm
22 12 1.833333 1 1.569560 14.01667 11.43982 1.923107
26 18 1.444444 1 1.904762 13.65000 15.40624 1.687628
28 17 1.647059 1 1.757322 15.93333 15.45339 1.811900
30 19 1.578947 1 1.715920 17.48333 16.90393 1.774736
37 29 1.275862 1 1.360294 27.20000 23.38118 1.582469
37 13 2.846154 1 2.785445 13.28333 14.56674 2.540033
38 29 1.310345 1 1.666667 22.80000 23.63478 1.607800
42 17 2.470588 1 2.189401 19.18333 18.17385 2.311013
43 23 1.869565 1 1.663443 25.85000 22.13579 1.942555
46 19 2.421053 1 1.922006 23.93333 20.16593 2.281075
46 21 2.190476 1 1.746835 26.33333 21.45907 2.143616
47 29 1.620690 1 1.626298 28.90000 26.18280 1.795072
48 33 1.454545 1 1.600000 30.00000 28.27561 1.697576
48 16 3.000000 1 2.608696 18.40000 18.27853 2.626032
48 33 1.454545 1 1.708185 28.10000 28.37588 1.691578
49 17 2.882353 1 2.348243 20.86667 19.15032 2.558704
50 20 2.500000 1 2.063274 24.23333 21.47421 2.328375
50 23 2.173913 1 2.166065 23.08333 23.54941 2.123195
51 27 1.888889 1 1.793669 28.43333 26.13042 1.951748
51 25 2.040000 1 1.733711 29.41667 24.89719 2.048424
51 23 2.217391 1 2.383178 21.40000 23.73131 2.149059
52 27 1.925926 1 1.570206 33.11667 26.25451 1.980612
52 25 2.080000 1 1.957340 26.56667 25.15148 2.067472
52 30 1.733333 1 1.846154 28.16667 27.89216 1.864323
52 15 3.466667 1 2.694301 19.30000 17.86131 2.911320
53 36 1.472222 1 1.539206 34.43333 31.05455 1.706674
53 25 2.120000 1 1.881657 28.16667 25.33172 2.092238
53 26 2.038462 1 1.540698 34.40000 25.97243 2.040625
53 31 1.709677 1 1.529582 34.65000 28.67831 1.848086
55 37 1.486486 1 1.628022 33.78333 32.04005 1.716602
55 28 1.964286 1 2.574103 21.36667 27.40644 2.006828
55 27 2.037037 1 2.037037 27.00000 26.85009 2.048410
56 27 2.074074 1 3.435583 16.30000 27.14732 2.062819
56 29 1.931034 1 1.881299 29.76667 28.31543 1.977720
56 29 1.931034 1 2.252011 24.86667 28.37107 1.973842
56 37 1.513514 1 2.118537 26.43333 32.37226 1.729876
56 29 1.931034 1 1.882353 29.75000 28.30541 1.978421
57 24 2.375000 1 2.005865 28.41667 25.28929 2.253919
57 19 3.000000 1 2.581132 22.08333 21.70482 2.626144
58 30 1.933333 1 1.699219 34.13333 29.14047 1.990359
58 19 3.052632 1 2.833876 20.46667 21.78715 2.662120
60 27 2.222222 1 2.070155 28.98333 27.77620 2.160122
60 34 1.764706 1 2.031603 29.53333 31.84393 1.884190
60 24 2.500000 1 2.101576 28.55000 25.83143 2.322752
61 27 2.259259 1 2.604982 23.41667 28.00617 2.178092
61 31 1.967742 1 1.875000 32.53333 30.44972 2.003302
62 35 1.771429 1 1.723818 35.96667 32.77666 1.891590
64 32 2.000000 1 1.743869 36.70000 31.59284 2.025775
66 23 2.869565 1 2.695711 24.48333 25.84519 2.553666
67 20 3.350000 1 2.875536 23.30000 23.54066 2.846139
68 34 2.000000 1 1.544869 44.01667 33.60323 2.023615
75 23 3.260870 1 2.613240 28.70000 26.82131 2.796284

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.