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:

  • Teller services : Deposit, withdrawal, and others.
  • Foreign Exchange services: This service takes longer due to very rigid regulations that requires verification of a bunch of documents to complete the service.

There is only one queue that provides both types of services, but the time to service is as follows:

  • Teller services follow a triangular model with a mean of 4mins, a lower end of 3 mins, and an upper value of 5 mins
  • Exchange services follow a triangular model with a mean of 15 mins, with a low of 10 and high of 20

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:

In [1]:
from IPython.display import Image
Image(filename = "flowchart.png", width=600, height=700)
Out[1]:

Simulate the process for the appropriate number of iterations

In [2]:
#Import required libraries
import simpy
import numpy as np
import pandas as pd
import random
In [3]:
#set seed and simulation time
seed = 500
simulation_time = 6 #Bank operational hours
In [4]:
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'])
In [5]:
#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))
In [6]:
#Function to return time generated for teller service
def teller_service():
    return np.random.triangular(3,4,5)
In [7]:
#Function to return time generated for foreign exchange servive
def exchange_service():
    return np.random.triangular(10,15,20)
In [8]:
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))
In [9]:
#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'] )
        
        
In [10]:
#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
In [11]:
np.random.seed(6758)
model(22,5,1)

#the simulation is run 22 times to mimic the no of working days in a month
Out[11]:
avg_wait exchange_wait teller_wait
0 3.438570 2.153817 3.652695
1 5.146405 7.320324 4.723699
2 0.459407 NaN 0.459407
3 19.611699 15.333748 20.574239
4 6.170323 6.463084 6.113659
5 6.134031 1.493575 6.847947
6 3.772680 0.925818 4.231852
7 2.515474 4.471126 2.337687
8 8.627018 7.563380 8.829616
9 8.165604 12.298386 7.247208
10 3.600993 3.279148 3.669263
11 2.037696 3.763828 1.995595
12 7.878683 4.523633 8.528048
13 4.664192 4.581946 4.682184
14 3.417308 3.869937 3.378511
15 2.130338 4.149052 1.824472
16 2.323174 0.587516 2.819077
17 6.708009 6.940985 6.651773
18 1.506237 0.827953 1.574065
19 14.291889 14.050653 14.355372
20 7.167206 12.944591 6.629775
21 1.915909 1.292858 2.090363

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.

In [12]:
report
Out[12]:
Customer Id Service Arrival Time Token Time Service Start Time Service Stop Time Token Time (Secs) Service Time (Mins) Wait Time (Mins)
0 Customer_1 Teller 10.705124 11.205124 11.205124 14.946960 30.0 3.741836 0.000000
1 Customer_2 Exchange 23.506774 24.006774 24.006774 39.609977 30.0 15.603203 0.000000
2 Customer_3 Teller 25.497957 25.997957 39.609977 43.827011 30.0 4.217034 13.612020
3 Customer_4 Teller 43.940237 44.440237 44.440237 47.855378 30.0 3.415141 0.000000
4 Customer_5 Teller 49.281346 49.781346 49.781346 53.791057 30.0 4.009711 0.000000
5 Customer_6 Teller 57.653302 58.153302 58.153302 61.623163 30.0 3.469860 0.000000
6 Customer_7 Teller 65.741402 66.241402 66.241402 69.751366 30.0 3.509965 0.000000
7 Customer_8 Teller 78.320151 78.820151 78.820151 83.172206 30.0 4.352055 0.000000
8 Customer_9 Teller 116.802826 117.302826 117.302826 121.242047 30.0 3.939221 0.000000
9 Customer_10 Teller 118.343328 118.843328 121.242047 125.264733 30.0 4.022685 2.398719
10 Customer_11 Exchange 118.745259 119.245259 125.264733 138.260644 30.0 12.995911 6.019474
11 Customer_12 Teller 134.456173 134.956173 138.260644 141.825504 30.0 3.564860 3.304471
12 Customer_13 Teller 140.074867 140.574867 141.825504 145.265842 30.0 3.440337 1.250638
13 Customer_14 Teller 142.480753 142.980753 145.265842 148.743558 30.0 3.477716 2.285088
14 Customer_15 Exchange 150.606939 151.106939 151.106939 166.469518 30.0 15.362580 0.000000
15 Customer_16 Teller 167.917274 168.417274 168.417274 172.180809 30.0 3.763534 0.000000
16 Customer_17 Exchange 196.361104 196.861104 196.861104 211.498436 30.0 14.637332 0.000000
17 Customer_18 Teller 211.594867 212.094867 212.094867 215.442014 30.0 3.347148 0.000000
18 Customer_19 Teller 212.269358 212.769358 215.442014 219.400984 30.0 3.958969 2.672656
19 Customer_20 Exchange 215.870449 216.370449 219.400984 232.398469 30.0 12.997485 3.030535
20 Customer_21 Teller 252.084917 252.584917 252.584917 257.421828 30.0 4.836911 0.000000
21 Customer_22 Teller 254.826694 255.326694 257.421828 260.683074 30.0 3.261246 2.095134
22 Customer_23 Exchange 269.321676 269.821676 269.821676 283.619732 30.0 13.798056 0.000000
23 Customer_24 Teller 270.143661 270.643661 283.619732 287.876438 30.0 4.256706 12.976071
24 Customer_25 Teller 283.422747 283.922747 287.876438 291.667130 30.0 3.790692 3.953691
25 Customer_26 Exchange 300.682981 301.182981 301.182981 318.492859 30.0 17.309878 0.000000
26 Customer_27 Teller 313.895567 314.395567 318.492859 322.479660 30.0 3.986801 4.097292
27 Customer_28 Teller 318.756368 319.256368 322.479660 326.021247 30.0 3.541587 3.223292
28 Customer_29 Teller 325.131234 325.631234 326.021247 329.736962 30.0 3.715714 0.390013
29 Customer_30 Teller 332.624721 333.124721 333.124721 337.494682 30.0 4.369961 0.000000
30 Customer_31 Teller 341.102466 341.602466 341.602466 345.464143 30.0 3.861677 0.000000
31 Customer_32 Teller 354.548756 355.048756 355.048756 359.474867 30.0 4.426111 0.000000
In [13]:
#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())
Avg wait: 1.9159091867982805
Avg exchange wait: 1.2928583826374052
Avg teller wait: 2.0903634119633256

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.

In [14]:
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.
 
Out[14]:
<matplotlib.axes._subplots.AxesSubplot at 0x2269a136340>
In [18]:
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.
Out[18]:
<matplotlib.axes._subplots.AxesSubplot at 0x2269a3ff310>
In [16]:
#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.
Out[16]:
<matplotlib.axes._subplots.AxesSubplot at 0x2269a24cfd0>
In [17]:
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.
Out[17]:
<matplotlib.axes._subplots.AxesSubplot at 0x2269a4d9850>

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