Data 604 Final Project: Queuing in a bank simulation
Farhana Zahir
18th July 2020
Rpubs:
Problem and Significance
Service quality is one of the major parameters that customers look at when evaluating a bank that they have an account with. This is often measured by the wait times that customers experience when receiving a service. The service quality department always aims to minimize the wait time with the number of resources that the budget permits in order to maximize customer satisfaction within the bank premises.It is very costly to do real life experiments and change no of resources as it entails hiring, training and minimum probation periods according to relevant labor laws. It is less costly to run a simulation to find out where the blocks are, and where resources need to be allocated.
This projects simulates a hypothetical bank that provides 2 simple services in the premises:
There is only one queue that provides both types of services, but the time to service is as follows:
The customer arrival rate is generated randomly using an exponential distribution with an arrival rate of 5 to 10 customers every hour. Each run of the simulation covers 6 hours (open hours of the bank)
Flow Chart model
The flowchart of the process is shown below:
from IPython.display import Image
Image(filename = "flowchart.png", width=600, height=700)
Simulate the process for the appropriate number of iterations
#Import required libraries
import simpy
import numpy as np
import pandas as pd
import random
#set seed and simulation time
seed = 500
simulation_time = 6 #Bank operational hours
np.random.seed(seed)
#Report here is being generated as a global variable
report = pd.DataFrame(columns = ['Customer Id', 'Service', 'Arrival Time', 'Token Time', 'Service Start Time', 'Service Stop Time'])
#Function to generate arrival, 5 to 10 customers per 60 mins
def generate_interarrival(): #Arrival rate 1 - 20 per hour
return np.random.exponential(60.0/np.random.randint(5,10))
#Function to return time generated for teller service
def teller_service():
return np.random.triangular(3,4,5)
#Function to return time generated for foreign exchange servive
def exchange_service():
return np.random.triangular(10,15,20)
def bank_run(env, teller, exchange):
global current_hour
i = 0 #counter for customer
while True: #running time 6 hours
Service = np.random.choice(['Teller', 'Exchange'], p=[0.85, 0.15]) #random.choices(services, weights=(10, 90)
i += 1
yield env.timeout(generate_interarrival())
env.process(customer(env, i, teller, exchange, Service))
#function generates report for each customer
def customer(env, customer, teller, exchange, Service):
global report #access the global variable
if Service == 'Exchange':
req = exchange
else:
req = teller
with exchange.request() as request:
arrival_time = env.now #starts the clock
#print(arrival_time, '\tCustomer {} arrives'.format(customer))
yield env.timeout(1/2) # Token generation time of 30 seconds
token_time = env.now #records time at which token is taken
#print(token_time, '\tCustomer {} took a token'.format(customer))
yield request
service_start_time = env.now #service starts
#print(service_start_time, '\tCustomer {} is being served at {}'.format(customer, Service))
if Service == 'Exchange':
yield env.timeout(exchange_service())
else:
yield env.timeout(teller_service())
service_stop_time = env.now
#record the data generated
row = pd.DataFrame([['Customer_{}'.format(customer), Service, arrival_time, token_time, service_start_time, service_stop_time]],
columns = ['Customer Id', 'Service', 'Arrival Time', 'Token Time','Service Start Time', 'Service Stop Time'])
report = report.append(row, ignore_index = True, sort = False)
#Calculations for wait time
report['Token Time (Secs)'] = 60 * ( report['Token Time'] - report['Arrival Time'])
report['Service Time (Mins)'] = (report['Service Stop Time'] - report['Service Start Time'])
report['Wait Time (Mins)'] = ( report['Service Start Time'] - report['Token Time'] )
#Running it in a model
def model(nruns, nteller, nexchange): #pass in no of runs for simulation, capacity teller, capacity exchange
#initialise
Teller=[]
Exchange=[]
avg_wait_time = []
exchange_wait_time = []
teller_wait_time = []
global report
for j in range(nruns):
report = report[0:0]
env = simpy.Environment()
env.initial_time = 0
# resource
teller = simpy.Resource(env, capacity=nteller) #assign teller as resource
exchange = simpy.Resource(env, capacity=nexchange) #assign exchange as resource
env.process(bank_run(env, teller, exchange))
env.run(until = 6 * 60) #run for 6 hours
Exchange=report[report.Service.isin(['Exchange'])] #separate to calculate mean later
Teller=report[report.Service.isin(['Teller'])]
avg_wait=report['Wait Time (Mins)'].mean() #Calculate average time
avg_wait_teller=Teller['Wait Time (Mins)'].mean()
avg_wait_exchange=Exchange['Wait Time (Mins)'].mean()
#print('Average wait time:', avg_wait)
#print('Average wait time teller:', avg_wait_teller)
#print('Average wait time exchange:', avg_wait_exchange)
avg_wait_time.append(avg_wait)
exchange_wait_time.append(avg_wait_exchange)
teller_wait_time.append(avg_wait_teller)
#Save in a dataframe
new_dict = {"avg_wait": avg_wait_time, "exchange_wait": exchange_wait_time, "teller_wait": teller_wait_time }
global new_df
new_df = pd.DataFrame(new_dict)
fig=new_df.boxplot(grid=False, widths=0.5)
return new_df
return fig
np.random.seed(6758)
model(22,5,1)
#the simulation is run 22 times to mimic the no of working days in a month
Verification and Validation
The model was verified by printing and checking intermediate outputs while writing the code. Each run of the simulation generates a report as belows. The service stop time is compares with the service start of the next customer and do not overlap.Given the time frame, it makes sense that 27-32 customers are being served in eacg run of simulation. There are several other varaibles I tested during each run to make sure there were no absurd values.
Validation was tougher as this is a hypothetical situation and I do not have a dataset to compare the results to. The flow of the customers in the report however closely mimic wait times in my personal experience in a bank.
report
#Measures used to verify output during each run
print('Avg wait:',report['Wait Time (Mins)'].mean())
Exchange=report[report.Service.isin(['Exchange'])]
print('Avg exchange wait:',Exchange['Wait Time (Mins)'].mean())
Teller=report[report.Service.isin(['Teller'])]
print('Avg teller wait:',Teller['Wait Time (Mins)'].mean())
Conclusions/ findings from the model
In my mind, an average wait time of more than 10 mins is unacceptable while waiting in a queue at a bank.
I ran the simulation multiple times and am sharing the results from a sample run with findings.
report['Wait Time (Mins)'].plot.hist(width = 1.2)
#Out of 31 customers served, 2 had a wait time of over 10 mins, that is within acceptable range.
report['Wait Time (Mins)'].plot.bar(figsize=(8.5,9))
#The plot below shows the customers with wait times, it is easy to identify which customers has a higher wait time.
#More interesting is the avg wait time during the month
new_df['avg_wait'].plot.bar(figsize=(5,5))
#Out of 22 days, average wait time exceeded 10 mins on 2 days. This is less than 10% of the time.
new_df.boxplot(grid=False, widths=0.5)
#The boxplot shows thee instances where the avg wait time during the day was too high(exceeded 10 mins).
#Most of the customers however have average wait times below 10.
Generate appropriate graphs (more than one) to illustrate the results and provide a PowerPoint presentation to share with your colleagues.
Some of the charts have already been presented above. This is a very simple model, and in reality more than 1 queue and many different kinds of services are required by bank customers. It would be interesting to model this with multiple queue.
Powerpoint: https://github.com/zahirf/Data604/blob/master/Bank%20Simulation%20using%20Simpy.pptx