Chapter 5 Business Decision Making
Finally, we discuss searching for the best drive-through design, where a design corresponds to a set of decisions, including the staffing level at each station and whether or not to implement a large-scale renovation project. Because the “best” design depends on the definitions of business objective, we investigate two alternatives.
Minimize operational cost while meeting performance constraints.
- One way to resolve issues of drive-through congestion (as mentioned in Chapter 1) is to utilize minimal operational resouces while bounding key performance metrics such as customers’ waiting time and length of the car line. A survey of drive-through customers suggests that a waiting time of 13 minutes is acceptable, similar to the stats reported in QSR Magazine. Moreover, a system carline of 8 or less can keep the cars a sufficient distance from the main road of the Marketplace.
Minimize total cost consisting of operational cost and lost sales due to inferior performance (e.g., long car line).
- Alternatively, one may quantify the cost of violating performance constraints by counting lost sales (i.e., customer abandonment) and minimize the sum of operational cost and cost of lost sales, which implicitly considers service quality when trading off various costs. Specifically, a customer, upon arrival at the drive-through, decides to balk if the system carline exceeds 8.
To represent the decision problems above, we introduce a few mathematical notations. All cost numbers are in thousands.
\(I^{reno}=1\) if rebuilding the facility (with a one-time cost of one million dollars: \(c^{reno}=1000\)), and 0 otherwise.
\(N^{cashier}\) and \(N^{cook}\) are the number of workers at the ordering station and the pickup station, respectively.
\(c^{cashier}\) and \(c^{cook}\) are the costs of one cashier and one cook per year, respectively. At an hourly rate of $15 per worker with 8 working hours and 260 workdays per year, the cost of one worker per year is \(c^{cashier}=c^{cook}=31.2\).
A design of the drive-through corresponds to a set of values for the decision variables concerning the renovation (\(I^{reno}\)) and the staffing level (\(N^{cashier}\), \(N^{cook}\)). Based on the previous bottleneck analysis, we propose the following designs for consideration (i.e., we perform a grid search):
\(I^{reno}=0,\ N^{cashier}=2\), \(N^{cook}\in\{3,4,5\}\).
\(I^{reno}=1,\ N^{cashier}=2\), \(N^{cook}\in\{3,4,5\}\).
5.1 Operational cost with performance constraints
In this example, the best design minimizes 10-year operational cost subject to performance constraints. Note that whether or not renovation takes place will affect which simulator we apply to evaluate system performance. If \(I^{reno}=0\) (1), we shall run the simulator of the “old” (“new”) Chick-fil-A.
\[\begin{align} \min_{I^{reno}, N^{cashier},N^{cook}}\quad &I^{reno}\cdot c^{reno}+(N^{cashier}\cdot c^{cashier}+N^{cook}\cdot c^{cook})\cdot 10\\ \text{subject to: } &\text{Average flow time}\leq 13 \text{ minutes}\\ &\text{Average system carline}\leq 8\text{ cars} \end{align}\]We next evaluate various designs with simulation.
Designs=data.frame(I=rep(0,3),Ncashier=rep(2,3),Ncook=3:5,
flow_time=rep(0,3),system_carline=rep(0,3))
Designs=rbind(Designs,Designs)
Designs[4:6,'I']=1
for(ii in 1:nrow(Designs)){
if(Designs[ii,'I']==0){ # before renovation
out=DES_before(num_cashier = Designs[ii,'Ncashier'],num_cook = Designs[ii,'Ncook'],
space_pickup = 6)
}else{ # after renovation
out=DES_after(num_cashier = Designs[ii,'Ncashier'],num_cook = Designs[ii,'Ncook'],
space_pickup = 10)
}
Designs[ii,'flow_time']=out$flow_time
Designs[ii,'system_carline']=out$system_carline
}
The following table summarizes all the designs and their performance metrics.
Designs$Meet_Constraints=1*(Designs$flow_time<=13)*(Designs$system_carline<=8)
Designs$Cost=1000*Designs$I+15*8*260*10/1000*(Designs$Ncashier+Designs$Ncook)
Designs
## I Ncashier Ncook flow_time system_carline Meet_Constraints Cost
## 1 0 2 3 18.563590 10.469911 0 1560
## 2 0 2 4 6.389379 4.466745 1 1872
## 3 0 2 5 5.467007 3.900928 1 2184
## 4 1 2 3 19.073694 8.819207 0 2560
## 5 1 2 4 6.439617 3.361279 1 2872
## 6 1 2 5 5.455356 2.889992 1 3184
5.1.1 Discussion
If the objective is to minimize ten-year operational cost while meeting the flow time and carline constraints, the best design is given by \(I^{reno}=0,N^{cashier}=2,N^{cook}=4\), that is, there is no need to rebuild Chick-fil-A with considerable cost. This conclusion highlights the value of using DES to predict system performance and prescribe the best cost-effective decision.
The previous bottleneck analysis provides important insights that guide the selection of potential designs. In other words, with an understanding of constraining resources, we can restrict the search space of the optimization problem, which in practice would significantly reduce the computational cost and is a fundamental technique in operations research.
The conclusion we draw depends on the optimization problem defined by the objective function and the constraints. If we only require the flow time to be under 20 and the system carline to be under 9, then the design \(I^{reno}=1,N^{cashier}=2,N^{cook}=3\) also meets the constraints. This particular design can be optimal if the unit staffing cost \(c^{cook}\) is sufficiently high. Hence, collaborating with practitioners and identifying objectives and constraints play a key role in deriving relevant solutions based on a comprehensive understanding of the business operations.
5.2 Tota cost including lost sales
To account for lost sales, we first refine our simulators to include customer balking upon arrival. We introduce a new parameter reflecting consumer tolerance of the car line.
# Upon arrival, a customer balks (decides not to join the drive-through)
# if the system carline exceeds MAX_line
MAX_line = 8
The simulator of Chick-fil-A before renovation with customer abandonment is as follows.
################################################################################
########### Simulator (with abandonment) before renovation ########################################
################################################################################
DES_AB_before=function(num_cashier,num_cook,space_pickup, MAX_line){
customer = trajectory("Customer's path") %>%
branch(function() (max(get_server_count(env,"lane1"),get_server_count(env,"lane2"))+
get_server_count(env,"lane_pickup")>=MAX_line)*1+1, continue = c(TRUE,FALSE),
trajectory("Join car line"),
trajectory("Customer balks") %>%
seize("main_road",amount=1) %>% # Try to seize a cashier for placing the order
timeout(function() 0.001) %>%
release("main_road",amount=1)
) %>%
set_attribute("lane", function() {
(get_server_count(env, "lane1")>=get_server_count(env, "lane2"))*1+1}
) %>%
select(function() {
paste0("lane",get_attribute(env, "lane"))
}) %>%
seize_selected(1) %>% # Choose the shortest ordering lane
seize("cashier",amount=1) %>% # Try to seize a cashier for placing the order
timeout(function() {rexp(1, mu_cashier)}) %>%
release("cashier",amount=1) %>%
seize("lane_pickup",amount=1) %>% # Try to seize a car space in pickup lane
release_selected(1) %>% # Once going to the pickup lane, the customer releases the ordering lane
seize("cook",amount=1) %>% # Try to seize a cook for preparing the order
timeout(function() {rexp(1, mu_cook)}) %>%
release("cook",amount=1) %>%
release("lane_pickup") # After the order is ready, the car immediately leaves the system
dummy = trajectory() %>% # A dummy trajectory for recording the system carline
set_attribute("carline_ordering",function() {
carline1 = max(get_server_count(env,"lane1"),get_server_count(env,"lane2"))}) %>%
set_attribute("carline_pickup",function() {
carline2 = get_server_count(env,"lane_pickup")})
env = simmer()
num_rep = 500
Data_before=data.frame(repetition=1:num_rep,system_carline=1:num_rep,flow_time=1:num_rep,
served_arrivals=1:num_rep, balked_arrivals=1:num_rep)
for(ii in 1:num_rep){
print(paste0("Repetition ",ii))
env = simmer()
env %>%
add_resource("main_road", 20) %>% # Enough spaces on the Main road for cars to balk
add_resource("cashier", num_cashier) %>% # 2 Cashiers for taking orders
add_resource("lane1", capacity=space_ordering) %>% # Ample car spaces in ordering lane1
add_resource("lane2", capacity=space_ordering) %>% # Ample car spaces in ordering lane2
add_resource("lane_pickup", capacity=space_pickup) %>% # 6 car spaces in pick up lane
add_resource("cook", num_cook) %>% # 3 Cooks for preparing the orders
add_generator("Customer", customer, function() rexp(1, arrival_rate), mon=2) %>% # Customer's arrival process
add_generator("Dummy recorder", dummy, function() 1, mon=2) %>% # Dummy trajectory records every 1 minute
run(simTime)
## average system carline length
df_att = get_mon_attributes(env)
df_att = df_att[substr(df_att$name,1,1)=='D',] # only look at dummy recorder
ordering = df_att[df_att$key=="carline_ordering",'value']
pickup = df_att[df_att$key=="carline_pickup",'value']
system_carline = ordering + pickup
Data_before[ii,'system_carline']=mean(system_carline[500:length(system_carline)])
## average flow time
df_arr = get_mon_arrivals(env)
df_arr = df_arr[substr(df_arr$name,1,1)=='C',] # picking customer's data only
df_arr_abandon = df_arr[df_arr$activity_time==0.001,] # customers who balk upon arrival
df_arr = df_arr[df_arr$activity_time!=0.001,] # customers who enter the drive-through
df_arr$flow_time = df_arr$end_time - df_arr$start_time
Data_before[ii,'flow_time']=mean(df_arr$flow_time[200:nrow(df_arr)])
Data_before[ii, 'served_arrivals']=nrow(df_arr)/simTime
Data_before[ii, 'balked_arrivals']=nrow(df_arr_abandon)/simTime # lost sales per minute
}
return(list(flow_time=mean(Data_before$flow_time),system_carline=mean(Data_before$system_carline),
served_arrivals=mean(Data_before$served_arrivals),balked_arrivals=mean(Data_before$balked_arrivals)))
}
The simulator of Chick-fil-A after renovation with customer abandonment is as follows.
################################################################################
########### Simulator after renovation ########################################
################################################################################
DES_AB_after=function(num_cashier,num_cook,space_pickup, MAX_line){
customer = trajectory("Customer's path") %>%
branch(function() (max(get_server_count(env,"lane1")+get_server_count(env,"lane1pickup"),
get_server_count(env,"lane2")+get_server_count(env,"lane2pickup"))>=MAX_line)*1+1,
continue = c(TRUE,FALSE),
trajectory("Join car line"),
trajectory("Customer balks") %>%
seize("main_road",amount=1) %>% # Try to seize a cashier for placing the order
timeout(function() 0.001) %>%
release("main_road",amount=1)
) %>%
set_attribute("lane", function() {
(get_server_count(env, "lane1")>=get_server_count(env, "lane2"))*1+1}
) %>%
select(function() {
paste0("lane",get_attribute(env, "lane"))
}) %>%
seize_selected(1) %>% # Choose the shortest ordering lane
seize("cashier",amount=1) %>% # Try to seize a cashier for placing the order
timeout(function() {rexp(1, mu_cashier)}) %>%
release("cashier",amount=1) %>%
select(function() {ifelse(get_attribute(env, "lane")==1,"lane1pickup","lane2pickup")}, id=2) %>%
seize_selected(amount=1, id=2) %>% # Try to go on to the corresponding pickup lane
release_selected(1) %>% # Once going to the pickup lane, release the ordering lane
seize("cook",amount=1) %>% # Try to seize a cook for preparing the order
timeout(function() {rexp(1, mu_cook)}) %>%
release("cook",amount=1) %>%
release_selected(amount=1, id=2) # After the order is ready, the car immediately leaves the system
dummy = trajectory() %>% # A dummy trajectory for recording the system carline of lane1 and lane2
set_attribute("carline_lane1",function() {
carline1 = get_server_count(env,"lane1")+get_server_count(env,"lane1pickup")}) %>%
set_attribute("carline_lane2",function() {
carline2 = get_server_count(env,"lane2")+get_server_count(env,"lane2pickup")})
env = simmer()
num_rep = 500
Data_after=data.frame(repetition=1:num_rep,system_carline=1:num_rep,flow_time=1:num_rep,
served_arrivals=1:num_rep, balked_arrivals=1:num_rep)
for(ii in 1:num_rep){
print(paste0("Repetition ",ii))
env = simmer()
env %>%
add_resource("main_road", 20) %>% # Enough spaces on the Main road for cars to balk
add_resource("cashier", num_cashier) %>% # Cashiers for taking orders
add_resource("lane1", capacity=space_ordering) %>% # Ample car spaces in ordering lane1
add_resource("lane2", capacity=space_ordering) %>% # Ample car spaces in ordering lane2
add_resource("lane1pickup", capacity=space_pickup) %>% # Car spaces in pick up lane1
add_resource("lane2pickup", capacity=space_pickup) %>% # Car spaces in pick up lane2
add_resource("cook", num_cook) %>% # 3 Cooks for preparing the orders
add_generator("Customer", customer, function() rexp(1, arrival_rate), mon=2) %>% # Customer's arrival process
add_generator("Dummy recorder", dummy, function() 1, mon=2) %>% # Dummy trajectory records every 1 minute
run(simTime)
## average system carline length
df_att = get_mon_attributes(env)
df_att = df_att[substr(df_att$name,1,1)=='D',] # Extract values for the dummy recorder
carline1 = df_att[df_att$key=='carline_lane1',]
carline2 = df_att[df_att$key=='carline_lane2',]
system_carline = pmax(carline1$value,carline2$value)
Data_after[ii,'system_carline']=mean(system_carline[500:length(system_carline)])
## average flow time
df_arr = get_mon_arrivals(env)
df_arr = df_arr[substr(df_arr$name,1,1)=='C',] # picking customer's data only
df_arr_abandon = df_arr[df_arr$activity_time==0.001,] # customers who balk upon arrival
df_arr = df_arr[df_arr$activity_time!=0.001,] # customers who enter the drive-through
df_arr$flow_time = df_arr$end_time - df_arr$start_time
Data_after[ii,'flow_time']=mean(df_arr$flow_time[200:nrow(df_arr)])
Data_after[ii, 'served_arrivals']=nrow(df_arr)/simTime
Data_after[ii, 'balked_arrivals']=nrow(df_arr_abandon)/simTime # lost sales per minute
}
return(list(flow_time=mean(Data_after$flow_time),system_carline=mean(Data_after$system_carline),
served_arrivals=mean(Data_after$served_arrivals),balked_arrivals=mean(Data_after$balked_arrivals)))
}
For both simulators, the output includes
customer’ average flow time (“flow_time”),
average system carline (“system_carline”),
sales per minute (“served_arrivals”),
lost sales per minute (“balked_arrivals”).
Using refined simulators, we solve the following with the objective being the sum of 10-year operational cost and 10-year cost of lost sales. Suppose each lost sale on average is worth $15
\[\begin{align} \min_{I^{reno}, N^{cashier},N^{cook}}\quad &I^{reno}\cdot c^{reno}+(N^{cashier}\cdot c^{cashier}+N^{cook}\cdot c^{cook})\cdot 10 +\\ &A(I^{reno})\cdot 480\text{min/day} \cdot 260\text{days/year} \cdot 10\text{years} \cdot \$15/1000 \end{align}\]where \(A(I^{reno})\) is lost sales per minute, one of the output from simulators “DES_AB_before” (if \(I^{reno}=0\)) or “DES_AB_after” (if \(I^{reno}=1\)).
We compute the total cost of all candidate designs.
################################################################################
########### Business decision making 2 ##########################
################################################################################
Designs=data.frame(I=rep(0,3),Ncashier=rep(2,3),Ncook=3:5,
flow_time=rep(0,3),system_carline=rep(0,3),
served_arrivals=rep(0,3),balked_arrivals=rep(0,3))
Designs=rbind(Designs,Designs)
Designs[4:6,'I']=1
for(ii in 1:nrow(Designs)){
if(Designs[ii,'I']==0){ # before renovation
out=DES_AB_before(num_cashier = Designs[ii,'Ncashier'],num_cook = Designs[ii,'Ncook'],
space_pickup = 6, MAX_line = 8)
}else{ # after renovation
out=DES_AB_after(num_cashier = Designs[ii,'Ncashier'],num_cook = Designs[ii,'Ncook'],
space_pickup = 10, MAX_line = 8)
}
Designs[ii,'flow_time']=out$flow_time
Designs[ii,'system_carline']=out$system_carline
Designs[ii,'served_arrivals']=out$served_arrivals
Designs[ii,'balked_arrivals']=out$balked_arrivals
}
Designs$Operational_cost = 1000*Designs$I+15*8*260*10/1000*(Designs$Ncashier+Designs$Ncook)
Designs$Lost_sales = Designs$balked_arrivals*480*260*10
Designs$Total_cost = Designs$Operational_cost + Designs$Lost_sales*15/1000
write.csv(Designs, "Performance metrics with abandonments.csv", row.names = FALSE)
The following table summarizes all the designs and their performance metrics, where we generate the number of sales and lost sales per minute, and costs over 10 years.
## I Ncashier Ncook served_arrivals balked_arrivals Operational_cost Lost_sales Total_cost
## 1 0 2 3 0.711381 0.086937 1560 108497.376 3187.461
## 2 0 2 4 0.760144 0.036065 1872 45009.120 2547.137
## 3 0 2 5 0.778189 0.020768 2184 25918.464 2572.777
## 4 1 2 3 0.739975 0.056133 2560 70053.984 3610.810
## 5 1 2 4 0.785289 0.011615 2872 14495.520 3089.433
## 6 1 2 5 0.794377 0.003632 3184 4532.736 3251.991
Interestingly, the best design, \(I^{reno}=0,N^{cashier}=2,N^{cook}=4\), is identical to the one given by the previous problem in 5.1, though the objective functions are formulated differently. We can think of the design problem with hard performance constraints (5.1) as a simplified approximation of a problem minimizing total cost (5.2). Because it is generally challenging to calculate abandonment and estimate its economic value, optimizing service operations with target performances can be relatively easier to implement and, hence, practically appealing.