[ad_1]
In the present day, I want to present you an instance of the discrete-event simulation strategy. We’ll mannequin the Buyer Help staff and determine what technique to make use of to enhance its efficiency. However first, let me share a little bit of my private story.
I first discovered about discrete simulations at college. One among my topics was Queueing theory, and to get a last grade for it, I needed to implement the airport simulation and calculate some KPIs. Sadly, I missed all of the seminars as a result of I used to be already working full-time, so I had no thought concerning the concept behind this matter and easy methods to strategy it.
I used to be decided to get a superb mark, so I discovered a guide, learn it, understood the fundamentals and spent a few evenings on implementation. It was fairly difficult since I hadn’t been coding for a while, however I figured it out and bought my A grade.
At this level (as usually occurs with college students), I had a sense that this info would not be useful for my future work. Nonetheless, later, I realised that many analytical duties could be solved with this strategy. So, I want to share it with you.
One of the crucial obvious use circumstances for agent-based simulations is Operational analytics. Most merchandise have buyer assist the place shoppers can get assist. A CS staff usually appears at such metrics as:
- common decision time — how a lot time handed from the client reaching out to CS and getting the primary reply,
- dimension of the queue that reveals what number of duties we’ve got in a backlog proper now.
And not using a correct mannequin, it might be difficult to know how our modifications (i.e. introducing evening shifts or simply growing the variety of brokers) will have an effect on the KPIs. Simulations will assist us do it.
So, let’s not waste our time and transfer on.
Let’s begin from the very starting. We shall be modelling the system. The system is a set of entities (for instance, folks, servers and even mechanical instruments) that work together with one another to realize some logical objective (i.e. answering a buyer query or passing border management in an airport).
You could possibly outline the system with the wanted granularity degree, relying in your analysis objective. For instance, in our case, we want to examine how the modifications to brokers’ effectivity and schedules might have an effect on common CS ticket decision time. So, the system shall be only a set of brokers. Nonetheless, if we want to mannequin the potential of outsourcing some tickets to completely different outsourcing corporations, we might want to embrace these companions in our mannequin.
The system is described by a set of variables — for instance, the variety of tickets in a queue or the variety of brokers working in the mean time in time. These variables outline the system state.
There are two sorts of techniques:
- discrete — when the system state modifications instantaneously, for instance, the brand new ticket has been added to a queue or an agent has completed their shift.
- steady — when the system is continually evolving. One such instance is a flying aircraft, wherein coordinates, velocity, peak, and different parameters change on a regular basis throughout flight.
For our activity, we are able to deal with the system as discrete and use the discrete-event simulation strategy. It is a case when the system can change at solely a countable variety of time limits. These time factors are the place occasions happen and immediately change the system state.
So, the entire strategy is predicated on occasions. We’ll generate and course of occasions one after the other to simulate how the system works. We are able to use the idea of a timeline to construction occasions.
Since this course of is dynamic, we have to hold monitor of the present worth of simulated time and be capable of advance it from one worth to a different. The variable in a simulation mannequin that reveals the present time is commonly referred to as the simulation clock.
We additionally want a mechanism to advance simulated time. There are two approaches to advance time:
- next-event time advance — we’re shifting from one occasion timestamp to the following one,
- fixed-increment time advance — we choose the interval, for instance, 1 minute, and shift clocks every time for this era.
I believe the primary strategy is less complicated to know, implement and debug. So, I’ll follow it for this text.
Let’s evaluate a easy instance to know the way it works. We’ll talk about a simplified case of the CS tickets queue.
We begin the simulation, initialising the simulation clock. Typically, folks use zero because the preliminary worth. I choose to make use of real-life knowledge and the precise date occasions.
This is the preliminary state of our system. We’ve two occasions on our timeline associated to 2 incoming buyer requests.
The subsequent step is to advance the simulation clock to the primary occasion on our timeline — the client request at 9:15.
It is time to course of this occasion. We must always discover an agent to work on this request, assign the request to them, and generate an occasion to complete the duty. Occasions are the principle drivers of our simulation, so it is okay if one occasion creates one other one.
Trying on the up to date timeline, we are able to see that essentially the most imminent occasion is just not the second buyer request however the completion of the primary activity.
So, we have to advance our clock to 9:30 and course of the following occasion. The completion of the request will not create new occasions, so after that, we’ll transfer to the second buyer request.
We’ll repeat this technique of shifting from one occasion to a different till the tip of the simulation.
To keep away from endless processes, we have to outline the stopping standards. On this case, we are able to use the next logic: if no extra occasions are on the timeline, we must always cease the simulation. On this simplified instance, our simulation will cease after ending the second activity.
We have mentioned the speculation of discrete occasion simulations and understood the way it works. Now, it is time to follow and implement this strategy in code.
Goal-oriented programming
In my day-to-day job, I often use a procedural programming paradigm. I create capabilities for some repetitive duties, however moderately than that, my code is sort of linear. It is fairly commonplace strategy for data-wrangling duties.
On this instance, we might use Objective-Oriented Programming. So, let’s spend a while revising this matter should you haven’t used courses in Python earlier than or want a refresher.
OOP is predicated on the idea of objects. Objects consist of information (some options which are referred to as attributes) and actions (capabilities or strategies). The entire program describes the interactions between completely different objects. For instance, if we’ve got an object representing a CS agent, it could actually have the next properties:
- attributes: title, date when an agent began working, common time they spend on duties or present standing (
"out of workplace"
,"engaged on activity"
or"free"
). - strategies: return the title, replace the standing or begin processing a buyer request.
To signify such an object, we are able to use Python courses. Let’s write a easy class for a CS agent.
class CSAgent:
# initialising class
def __init__(self, title, average_handling_time):
# saving parameters talked about throughout object creation
self.title = title
self.average_handling_time = average_handling_time
# specifying fixed worth
self.function = 'CS agent'
print('Created %s with title %s' % (self.function, self.title))def get_name(self):
return self.title
def get_handling_time(self):
return self.average_handling_time
def update_handling_time(self, average_handling_time):
print('Updating time from %.2f to %.2f' % (self.average_handling_time,
average_handling_time))
self.average_handling_time = average_handling_time
This class defines every agent’s title, common dealing with time, and function. I’ve additionally added a few capabilities that may return inside variables following the incapsulation sample. Additionally, we’ve got the update_handling_time
operate that enables us to replace the agent’s efficiency.
We have created a category (an object that explains any sort of CS agent). Let’s make an occasion of the item — the agent John Doe.
john_agent = CSAgent('John Doe', 12.3)
# Created CS agent with title John Doe
After we created an occasion of the category, the operate __init__
was executed. We are able to use __dict__
property to current class fields as a dictionary. It usually could be helpful, for instance, if you wish to convert an inventory of objects into a knowledge body.
print(john_agent.__dict__)
# {'title': 'John Doe', 'average_handling_time': 12.3, 'function': 'CS agent'}
We are able to attempt to execute a technique and replace the agent’s efficiency.
john_agent.update_handling_time(5.4)
# Updating time from 12.30 to five.40print(john_agent.get_handling_time())
# 5.4
One of many elementary ideas of OOP that we are going to use as we speak is inheritance. Inheritance permits us to have a high-level ancestor class and use its options within the descendant courses. Think about we wish to haven’t solely CS brokers but in addition KYC brokers. We are able to create a high-level Agent
class with widespread performance and outline it solely as soon as for each KYC and CS brokers.
class Agent:
# initialising class
def __init__(self, title, average_handling_time, function):
# saving parameters talked about throughout object creation
self.title = title
self.average_handling_time = average_handling_time
self.function = function
print('Created %s with title %s' % (self.function, self.title))def get_name(self):
return self.title
def get_handling_time(self):
return self.average_handling_time
def update_handling_time(self, average_handling_time):
print('Updating time from %.2f to %.2f' % (self.average_handling_time,
average_handling_time))
self.average_handling_time = average_handling_time
Now, we are able to create separate courses for these agent sorts and outline barely completely different __init__
and get_job_description
capabilities.
class KYCAgent(Agent):
def __init__(self, title, average_handling_time):
tremendous().__init__(title, average_handling_time, 'KYC agent')def get_job_description(self):
return 'KYC (Know Your Buyer) brokers assist to confirm paperwork'
class CSAgent(Agent):
def __init__(self, title, average_handling_time):
tremendous().__init__(title, average_handling_time, 'CS agent')
def get_job_description(self):
return 'CS (Buyer Help) reply buyer questions and assist resolving their issues'
To specify inheritance, we talked about the bottom class in brackets after the present class title. With tremendous()
, we are able to name the bottom class strategies, for instance, __init__
to create an object with a customized function
worth.
Let’s create objects and examine whether or not they work as anticipated.
marie_agent = KYCAgent('Marie', 25)
max_agent = CSAgent('Max', 10)print(marie_agent.__dict__)
# {'title': 'Marie', 'average_handling_time': 25, 'function': 'KYC agent'}
print(max_agent.__dict__)
# {'title': 'Max', 'average_handling_time': 10, 'function': 'CS agent'}
Let’s replace Marie’s dealing with time. Despite the fact that we haven’t applied this operate within the KYCAgent
class, it makes use of the implementation from the bottom class and works fairly properly.
marie_agent.update_handling_time(22.5)
# Updating time from 25.00 to 22.50
We are able to additionally name the strategies we outlined within the courses.
print(marie_agent.get_job_description())
# KYC (Know Your Buyer) brokers assist to confirm paperworkprint(max_agent.get_job_description())
# CS (Buyer Help) reply buyer questions and assist resolving their issues
So, we have coated the fundamentals of the Goal-oriented paradigm and Python courses. I hope it was a useful refresher.
Now, it’s time to return to our activity and the mannequin we’d like for our simulation.
Structure: courses
When you haven’t used OOP so much earlier than, switching your mindset from procedures to things may be difficult. It takes a while to make this mindset shift.
One of many life hacks is to make use of real-world analogies (i.e. it is fairly clear that an agent is an object with some options and actions).
Additionally, do not be afraid to make a mistake. There are higher or worse program architectures: some shall be simpler to learn and assist over time. Nonetheless, there are a whole lot of debates about the most effective practices, even amongst mature software program engineers, so I wouldn’t hassle making an attempt to make it excellent an excessive amount of for analytical ad-hoc analysis.
Let’s take into consideration what objects we’d like in our simulation:
System
— essentially the most high-level idea we’ve got in our activity. The system will signify the present state and execute the simulation.- As we mentioned earlier than, the system is a set of entities. So, the following object we’d like is
Agent
. This class will describe brokers engaged on duties. - Every agent may have its schedule: hours when this agent is working, so I’ve remoted it right into a separate class
Schedule
. - Our brokers shall be engaged on buyer requests. So, it is a no-brainer— we have to signify them in our system. Additionally, we’ll retailer an inventory of processed requests within the
System
object to get the ultimate stats after the simulation. - If no free agent picks up a brand new buyer request, will probably be put right into a queue. So, we may have a
RequestQueue
as an object to retailer all buyer requests with the FIFO logic (First In, First Out). - The next necessary idea is
TimeLine
that represents the set of occasions we have to course of ordered by time. TimeLine
will embrace occasions, so we may even create a categoryOccasion
for them. Since we may have a bunch of various occasion sorts that we have to course of otherwise, we are able to leverage the OOP inheritance. We’ll talk about occasion sorts in additional element within the subsequent part.
That is it. I’ve put all of the courses and hyperlinks between them right into a diagram to make clear it. I take advantage of such charts to have a high-level view of the system earlier than beginning the implementation — it helps to consider the structure early on.
As you may need seen, the diagram is just not tremendous detailed. For instance, it doesn’t embrace all subject names and strategies. It is intentional. This schema shall be used as a helicopter view to information the event. So, I do not wish to spend an excessive amount of time writing down all the sector and technique names as a result of these particulars may change in the course of the implementation.
Structure: occasion sorts
We have coated this system structure, and now it is time to consider the principle drivers of our simulation — occasions.
Let’s talk about what occasions we have to generate to maintain our system working.
- The occasion I’ll begin with is the “Agent Prepared” occasion. It reveals that an agent begins their work and is able to decide up a activity (if we’ve got any ready within the queue).
- We have to know when brokers begin working. These working hours can depend upon an agent and the day of the week. Probably, we’d even wish to change the schedules in the course of the simulation. It is fairly difficult to create all “Agent Prepared” occasions once we initialise the system (particularly since we do not understand how a lot time we have to end the simulation). So, I suggest a recurrent “Plan Brokers Schedule” occasion to create ready-to-work occasions for the following day.
- The opposite important occasion we’d like is a “New Buyer Request” — an occasion that reveals that we bought a brand new CS contact, and we have to both begin engaged on it or put it in a queue.
- The final occasion is “Agent Completed Process“, which reveals that the agent completed the duty he was engaged on and is probably prepared to select up a brand new activity.
That is it. These 4 occasions are sufficient to run the entire simulation.
Much like courses, there are not any proper or mistaken solutions for system modelling. You may use a barely completely different set of occasions. For instance, you possibly can add a “Begin Process” occasion to have it explicitly.
You will discover the total implementation on GitHub.
We have outlined the high-level construction of our answer, so we’re prepared to begin implementing it. Let’s begin with the center of our simulation — the system class.
Initialising the system
Let’s begin with the __init__
technique for the system class.
First, let’s take into consideration the parameters we want to specify for the simulation:
brokers
— set of brokers that shall be working within the CS staff,queue
— the present queue of buyer requests (if we’ve got any),initial_date
— since we agreed to make use of the precise timestamps as a substitute of relative ones, I’ll specify the date once we begin simulations,logging
— flag that defines whether or not we want to print some information for debugging,customer_requests_df
— knowledge body with details about the set of buyer requests we want to course of.
Apart from enter parameters, we may even create the next inside fields:
current_time
— the simulation clock that we are going to initialise as 00:00:00 of the preliminary date specified,timeline
object that we are going to use to outline the order of occasions,processed_request
— an empty record the place we’ll retailer the processed buyer requests to get the information after simulation.
It is time to take the required actions to initialise a system. There are solely two steps left:
- Plan brokers work for the primary day. I’ll generate and course of a corresponding occasion with an preliminary timestamp.
- Load buyer requests by including corresponding “New Buyer Request” occasions to the timeline.
This is the code that does all these actions to initialise the system.
class System:
def __init__(self, brokers, queue, initial_date,
customer_requests_df, logging = True):
initial_time = datetime.datetime(initial_date.yr, initial_date.month,
initial_date.day, 0, 0, 0)
self.brokers = brokers
self.queue = RequestQueue(queue)
self.logging = logging
self.current_time = initial_timeself._timeline = TimeLine()
self.processed_requests = ()
initial_event = PlanScheduleEvent('plan_agents_schedule', initial_time)
initial_event.course of(self)
self.load_customer_request_events(customer_requests_df)
It isn’t working but because it has hyperlinks to non-implemented courses and strategies, however we’ll cowl all of it one after the other.
Timeline
Let’s begin with the courses we used within the system definition. The primary one is TimeLine
. The one subject it has is the record of occasions. Additionally, it implements a bunch of strategies:
- including occasions (and guaranteeing that they’re ordered chronologically),
- returning the following occasion and deleting it from the record,
- telling what number of occasions are left.
class TimeLine:
def __init__(self):
self.occasions = ()def add_event(self, occasion:Occasion):
self.occasions.append(occasion)
self.occasions.type(key = lambda x: x.time)
def get_next_item(self):
if len(self.occasions) == 0:
return None
return self.occasions.pop(0)
def get_remaining_events(self):
return len(self.occasions)
Buyer requests queue
The opposite class we utilized in initialisation is RequestQueue
.
There are not any surprises: the request queue consists of buyer requests. Let’s begin with this constructing block. We all know every request’s creation time and the way a lot time an agent might want to work on it.
class CustomerRequest:
def __init__(self, id, handling_time_secs, creation_time):
self.id = id
self.handling_time_secs = handling_time_secs
self.creation_time = creation_timedef __str__(self):
return f'Buyer Request {self.id}: {self.creation_time.strftime("%Y-%m-%d %H:%M:%S")}'
It is a easy knowledge class that comprises solely parameters. The one new factor right here is that I’ve overridden the __str__
technique to alter the output of a print operate. It is fairly helpful for debugging. You possibly can examine it your self.
test_object = CustomerRequest(1, 600, datetime.datetime(2024, 5, 1, 9, 42, 1))
# with out defining __str__
print(test_object)
# <__main__.CustomerRequest object at 0x280209130># with customized __str__
print(test_object)
# Buyer Request 1: 2024-05-01 09:42:01
Now, we are able to transfer on to the requests queue. Equally to the timeline, we have applied strategies so as to add new requests, calculate requests within the queue and get the next request from the queue.
class RequestQueue:
def __init__(self, queue = None):
if queue is None:
self.requests = ()
else:
self.requests = queuedef get_requests_in_queue(self):
return len(self.requests)
def add_request(self, request):
self.requests.append(request)
def get_next_item(self):
if len(self.requests) == 0:
return None
return self.requests.pop(0)
Brokers
The opposite factor we have to initialise the system is brokers. First, every agent has a schedule — a interval when they’re working relying on a weekday.
class Schedule:
def __init__(self, time_periods):
self.time_periods = time_periodsdef is_within_working_hours(self, dt):
weekday = dt.strftime('%A')
if weekday not in self.time_periods:
return False
hour = dt.hour
time_periods = self.time_periods(weekday)
for interval in time_periods:
if (hour >= interval(0)) and (hour < interval(1)):
return True
return False
The one technique we’ve got for a schedule is whether or not on the specified second the agent is working or not.
Let’s outline the agent class. Every agent may have the next attributes:
id
andtitle
— primarily for logging and debugging functions,schedule
— the agent’s schedule object we’ve simply outlined,request_in_work
— hyperlink to buyer request object that reveals whether or not an agent is occupied proper now or not.effectiveness
— the coefficient that reveals how environment friendly the agent is in comparison with the anticipated time to resolve the actual activity.
We’ve the next strategies applied for brokers:
- understanding whether or not they can tackle a brand new activity (whether or not they’re free and nonetheless working),
- begin and end processing the client request.
class Agent:
def __init__(self, id, title, schedule, effectiveness = 1):
self.id = id
self.schedule = schedule
self.title = title
self.request_in_work = None
self.effectiveness = effectivenessdef is_ready_for_task(self, dt):
if (self.request_in_work is None) and (self.schedule.is_within_working_hours(dt)):
return True
return False
def start_task(self, customer_request):
self.request_in_work = customer_request
customer_request.handling_time_secs = int(spherical(self.effectiveness * customer_request.handling_time_secs))
def finish_task(self):
self.request_in_work = None
Loading preliminary buyer requests to the timeline
The one factor we’re lacking from the system __init__
operate (in addition to the occasions processing that we are going to talk about intimately a bit later) is load_customer_request_events
operate implementation. It is fairly easy. We simply want so as to add it to our System
class.
class System:
def load_customer_request_events(self, df):
# filter requests earlier than the beginning of simulation
filt_df = df(df.creation_time >= self.current_time)
if filt_df.form(0) != df.form(0):
if self.logging:
print('Consideration: %d requests have been filtered out since they're outdated' % (df.form(0) - filt_df.form(0)))# create new buyer request occasions for every file
for rec in filt_df.sort_values('creation_time').to_dict('data'):
customer_request = CustomerRequest(rec('id'), rec('handling_time_secs'),
rec('creation_time'))
self.add_event(NewCustomerRequestEvent(
'new_customer_request', rec('creation_time'),
customer_request
))
Cool, we have discovered the first courses. So, let’s transfer on to the implementation of the occasions.
Processing occasions
As mentioned, I’ll use the inheritance strategy and create an Occasion
class. For now, it implements solely __init__
and __str__
capabilities, however probably, it could actually assist us present extra performance for all occasions.
class Occasion:
def __init__(self, event_type, time):
self.kind = event_type
self.time = timedef __str__(self):
if self.kind == 'agent_ready_for_task':
return '%s (%s) - %s' % (self.kind, self.agent.title, self.time)
return '%s - %s' % (self.kind, self.time)
Then, I implement a separate subclass for every occasion kind that may have a bit completely different initialisation. For instance, for the AgentReady
occasion, we even have an Agent
object. Greater than that, every Occasion class implements course of
technique that takes system
as an enter.
class AgentReadyEvent(Occasion):
def __init__(self, event_type, time, agent):
tremendous().__init__(event_type, time)
self.agent = agentdef course of(self, system: System):
# get subsequent request from the queue
next_customer_request = system.queue.get_next_item()
# begin processing request if we had some
if next_customer_request is just not None:
self.agent.start_task(next_customer_request)
next_customer_request.start_time = system.current_time
next_customer_request.agent_name = self.agent.title
next_customer_request.agent_id = self.agent.id
if system.logging:
print('<%s> Agent %s began to work on request %d' % (system.current_time,
self.agent.title, next_customer_request.id))
# schedule end processing occasion
system.add_event(FinishCustomerRequestEvent('finish_handling_request',
system.current_time + datetime.timedelta(seconds = next_customer_request.handling_time_secs),
next_customer_request, self.agent))
class PlanScheduleEvent(Occasion):
def __init__(self, event_type, time):
tremendous().__init__(event_type, time)
def course of(self, system: System):
if system.logging:
print('<%s> Scheeduled brokers for as we speak' % (system.current_time))
current_weekday = system.current_time.strftime('%A')
# create agent prepared occasions for all brokers engaged on this weekday
for agent in system.brokers:
if current_weekday not in agent.schedule.time_periods:
proceed
for time_periods in agent.schedule.time_periods(current_weekday):
system.add_event(AgentReadyEvent('agent_ready_for_task',
datetime.datetime(system.current_time.yr, system.current_time.month,
system.current_time.day, time_periods(0), 0, 0),
agent))
# schedule subsequent planning
system.add_event(PlanScheduleEvent('plan_agents_schedule', system.current_time + datetime.timedelta(days = 1)))
class FinishCustomerRequestEvent(Occasion):
def __init__(self, event_type, time, customer_request, agent):
tremendous().__init__(event_type, time)
self.customer_request = customer_request
self.agent = agent
def course of(self, system):
self.agent.finish_task()
# log end time
self.customer_request.finish_time = system.current_time
# save processed request
system.processed_requests.append(self.customer_request)
if system.logging:
print('<%s> Agent %s completed request %d' % (system.current_time, self.agent.title, self.customer_request.id))
# decide up the following request if agent proceed working and we've got one thing within the queue
if self.agent.is_ready_for_task(system.current_time):
next_customer_request = system.queue.get_next_item()
if next_customer_request is just not None:
self.agent.start_task(next_customer_request)
next_customer_request.start_time = system.current_time
next_customer_request.agent_name = self.agent.title
next_customer_request.agent_id = self.agent.id
if system.logging:
print('<%s> Agent %s began to work on request %d' % (system.current_time,
self.agent.title, next_customer_request.id))
system.add_event(FinishCustomerRequestEvent('finish_handling_request',
system.current_time + datetime.timedelta(seconds = next_customer_request.handling_time_secs),
next_customer_request, self.agent))
class NewCustomerRequestEvent(Occasion):
def __init__(self, event_type, time, customer_request):
tremendous().__init__(event_type, time)
self.customer_request = customer_request
def course of(self, system: System):
# examine whether or not we've got a free agent
assigned_agent = system.get_free_agent(self.customer_request)
# if not put request in a queue
if assigned_agent is None:
system.queue.add_request(self.customer_request)
if system.logging:
print('<%s> Request %d put in a queue' % (system.current_time, self.customer_request.id))
# if sure, begin processing it
else:
assigned_agent.start_task(self.customer_request)
self.customer_request.start_time = system.current_time
self.customer_request.agent_name = assigned_agent.title
self.customer_request.agent_id = assigned_agent.id
if system.logging:
print('<%s> Agent %s began to work on request %d' % (system.current_time, assigned_agent.title, self.customer_request.id))
system.add_event(FinishCustomerRequestEvent('finish_handling_request',
system.current_time + datetime.timedelta(seconds = self.customer_request.handling_time_secs),
self.customer_request, assigned_agent))
That is really it with the occasions processing enterprise logic. The one bit we have to end is to place every thing collectively to run our simulation.
Placing all collectively within the system class
As we mentioned, the System
class shall be answerable for operating the simulations. So, we’ll put the remaining nuts and bolts there.
This is the remaining code. Let me briefly stroll you thru the details:
is_simulation_finished
defines the stopping standards for our simulation — no requests are within the queue, and no occasions are within the timeline.process_next_event
will get the following occasion from the timeline and executescourse of
for it. There is a slight nuance right here: we’d find yourself in a state of affairs the place our simulation by no means ends due to recurring “Plan Brokers Schedule” occasions. That is why, in case of processing such an occasion kind, I examine whether or not there are every other occasions within the timeline and if not, I do not course of it since we need not schedule brokers anymore.run_simulation
is the operate that guidelines our world, however since we’ve got fairly an honest structure, it is a few traces: we examine whether or not we are able to end the simulation, and if not, we course of the following occasion.
class System:
# defines the stopping standards
def is_simulation_finished(self):
if self.queue.get_requests_in_queue() > 0:
return False
if self._timeline.get_remaining_events() > 0:
return False
return True# wrappers for timeline strategies to incapsulate this logic
def add_event(self, occasion):
self._timeline.add_event(occasion)
def get_next_event(self):
return self._timeline.get_next_item()
# returns free agent if we've got one
def get_free_agent(self, customer_request):
for agent in self.brokers:
if agent.is_ready_for_task(self.current_time):
return agent
# finds and processes the following occasion
def process_next_event(self):
occasion = self.get_next_event()
if self.logging:
print('# Processing occasion: ' + str(occasion))
if (occasion.kind == 'plan_agents_schedule') and self.is_simulation_finished():
if self.logging:
print("FINISH")
else:
self.current_time = occasion.time
occasion.course of(self)
# principal operate
def run_simulation(self):
whereas not self.is_simulation_finished():
self.process_next_event()
It was an extended journey, however we have finished it. Wonderful job! Now, we’ve got all of the logic we’d like. Let’s transfer on to the humorous half and use our mannequin for evaluation.
You will discover the total implementation on GitHub.
I’ll use an artificial Buyer Requests dataset to simulate completely different Ops setups.
To begin with, let’s run our system and take a look at metrics. I’ll begin with 15 brokers who’re working common hours.
# initialising brokers
regular_work_week = Schedule(
{
'Monday': ((9, 12), (13, 18)),
'Tuesday': ((9, 12), (13, 18)),
'Wednesday': ((9, 12), (13, 18)),
'Thursday': ((9, 12), (13, 18)),
'Friday': ((9, 12), (13, 18))
}
)brokers = ()
for id in vary(15):
brokers.append(Agent(id + 1, 'Agent %s' % id, regular_work_week))
# inital date
system_initial_date = datetime.date(2024, 4, 8)
# initialising the system
system = System(brokers, (), system_initial_date, backlog_df, logging = False)
# operating the simulation
system.run_simulation()
Because of the execution, we bought all of the stats in system.processed_requests
. Let’s put collectively a few helper capabilities to analyse outcomes simpler.
# convert outcomes to knowledge body and calculate timings
def get_processed_results(system):
processed_requests_df = pd.DataFrame(record(map(lambda x: x.__dict__, system.processed_requests)))
processed_requests_df = processed_requests_df.sort_values('creation_time')
processed_requests_df('creation_time_hour') = processed_requests_df.creation_time.map(
lambda x: x.strftime('%Y-%m-%d %H:00:00')
)processed_requests_df('resolution_time_secs') = record(map(
lambda x, y: int(x.strftime('%s')) - int(y.strftime('%s')),
processed_requests_df.finish_time,
processed_requests_df.creation_time
))
processed_requests_df('waiting_time_secs') = processed_requests_df.resolution_time_secs - processed_requests_df.handling_time_secs
processed_requests_df('waiting_time_mins') = processed_requests_df('waiting_time_secs')/60
processed_requests_df('handling_time_mins') = processed_requests_df.handling_time_secs/60
processed_requests_df('resolution_time_mins') = processed_requests_df.resolution_time_secs/60
return processed_requests_df
# calculating queue dimension with 5 minutes granularity
def get_queue_stats(processed_requests_df):
queue_stats = ()
current_time = datetime.datetime(system_initial_date.yr, system_initial_date.month, system_initial_date.day, 0, 0, 0)
whereas current_time <= processed_requests_df.creation_time.max() + datetime.timedelta(seconds = 300):
queue_size = processed_requests_df((processed_requests_df.creation_time <= current_time) & (processed_requests_df.start_time > current_time)).form(0)
queue_stats.append(
{
'time': current_time,
'queue_size': queue_size
}
)
current_time = current_time + datetime.timedelta(seconds = 300)
return pd.DataFrame(queue_stats)
Additionally, let’s make a few charts and calculate weekly metrics.
def analyse_results(system, show_charts = True):
processed_requests_df = get_processed_results(system)
queue_stats_df = get_queue_stats(processed_requests_df)stats_df = processed_requests_df.groupby('creation_time_hour').mixture(
{'id': 'rely', 'handling_time_mins': 'imply', 'resolution_time_mins': 'imply',
'waiting_time_mins': 'imply'}
)
if show_charts:
fig = px.line(stats_df(('id')),
labels = {'worth': 'requests', 'creation_time_hour': 'request creation time'},
title = '<b>Variety of requests created</b>')
fig.update_layout(showlegend = False)
fig.present()
fig = px.line(stats_df(('waiting_time_mins', 'handling_time_mins', 'resolution_time_mins')),
labels = {'worth': 'time in minutes', 'creation_time_hour': 'request creation time'},
title = '<b>Decision time</b>')
fig.present()
fig = px.line(queue_stats_df.set_index('time'),
labels = {'worth': 'variety of requests in queue'},
title = '<b>Queue dimension</b>')
fig.update_layout(showlegend = False)
fig.present()
processed_requests_df('interval') = processed_requests_df.creation_time.map(
lambda x: (x - datetime.timedelta(x.weekday())).strftime('%Y-%m-%d')
)
queue_stats_df('interval') = queue_stats_df('time').map(
lambda x: (x - datetime.timedelta(x.weekday())).strftime('%Y-%m-%d')
)
period_stats_df = processed_requests_df.groupby('interval')
.mixture({'id': 'rely', 'handling_time_mins': 'imply',
'waiting_time_mins': 'imply',
'resolution_time_mins': 'imply'})
.be a part of(queue_stats_df.groupby('interval')(('queue_size')).imply())
return period_stats_df
# execution
analyse_results(system)
Now, we are able to use this operate to analyse the simulation outcomes. Apparently, 15 brokers will not be sufficient for our product since, after three weeks, we’ve got 4K+ requests in a queue and a median decision time of round ten days. Clients can be very sad with our service if we had simply 15 brokers.
Let’s learn how many brokers we’d like to have the ability to address the demand. We are able to run a bunch of simulations with the completely different variety of brokers and examine outcomes.
tmp_dfs = ()for num_agents in tqdm.tqdm(vary(15, 105, 5)):
brokers = ()
for id in vary(num_agents):
brokers.append(Agent(id + 1, 'Agent %s' % id, regular_work_week))
system = System(brokers, (), system_initial_date, backlog_df, logging = False)
system.run_simulation()
tmp_df = analyse_results(system, show_charts = False)
tmp_df('num_agents') = num_agents
tmp_dfs.append(tmp_df)
We are able to see that from ~25–30 brokers, metrics for various weeks are roughly the identical, so there’s sufficient capability to deal with incoming requests and queue is just not rising week after week.
If we mannequin the state of affairs when we’ve got 30 brokers, we are able to see that the queue is empty from 13:50 until the tip of the working day from Tuesday to Friday. Brokers spend Monday processing the large queue we’re gathering throughout weekends.
With such a setup, the common decision time is 500.67 minutes, and the common queue size is 259.39.
Let’s attempt to consider the doable enhancements for our Operations staff:
- we are able to rent one other 5 brokers,
- we are able to begin leveraging LLMs and cut back dealing with time by 30%,
- we are able to shift brokers’ schedules to supply protection throughout weekends and late hours.
Since we now have a mannequin, we are able to simply estimate all of the alternatives and decide essentially the most possible one.
The primary two approaches are easy. Let’s talk about how we are able to shift the brokers’ schedules. All our brokers are working from Monday to Friday from 9 to 18. Let’s attempt to make their protection a bit bit extra equally distributed.
First, we are able to cowl later and earlier hours, splitting brokers into two teams. We may have brokers working from 7 to 16 and from 11 to twenty.
Second, we are able to cut up them throughout working days extra evenly. I used fairly an easy strategy.
In actuality, you possibly can go even additional and allocate fewer brokers on weekends since we’ve got means much less demand. It could possibly enhance your metrics even additional. Nonetheless, the extra impact shall be marginal.
If we run simulations for all these eventualities, surprisingly, we’ll see that KPIs shall be means higher if we simply change brokers’ schedules. If we rent 5 extra folks or enhance brokers’ efficiency by 30%, we cannot obtain such a major enchancment.
Let’s examine how modifications in brokers’ schedules have an effect on our KPIs. Decision time grows just for circumstances outdoors working hours (from 20 to 7), and queue dimension by no means reaches 200 circumstances.
That is a superb end result. Our simulation mannequin has helped us prioritise operational modifications as a substitute of hiring extra folks or investing in LLM instrument improvement.
We have mentioned the fundamentals of this strategy on this article. If you wish to dig deeper and use it in follow, listed below are a pair extra ideas that may be helpful:
- Earlier than beginning to use such fashions in manufacturing, it’s price testing them. Essentially the most easy means is to mannequin your present state of affairs and examine the principle KPIs. In the event that they differ so much, then your system doesn’t signify the true world properly sufficient, and it’s worthwhile to make it extra correct earlier than utilizing it for decision-making.
- The present metrics are customer-focused. I’ve used common decision time as the first KPI to make choices. In enterprise, we additionally care about prices. So, it is price taking a look at this activity from an operational perspective as properly, i.e. measure the proportion of time when brokers haven’t got duties to work on (which implies we’re paying them for nothing).
- In actual life, there may be spikes (i.e. the variety of buyer requests has doubled due to a bug in your product), so I like to recommend you utilize such fashions to make sure that your CS staff can deal with such conditions.
- Final however not least, the mannequin I’ve used was fully deterministic (it returns the identical end result on each run), as a result of dealing with time was outlined for every buyer request. To raised perceive metrics variability, you possibly can specify the distribution of dealing with occasions (relying on the duty kind, day of the week, and many others.) for every agent and get dealing with time from this distribution at every iteration. Then, you possibly can run the simulation a number of occasions and calculate the boldness intervals of your metrics.
So, let’s briefly summarise the details we’ve mentioned as we speak:
- We’ve discovered the fundamentals of the discrete-event simulation strategy that helps to mannequin discrete techniques with a countable variety of occasions.
- We’ve revised the object-oriented programming and courses in Python since this paradigm is extra appropriate for this activity than the widespread procedural code knowledge analysts often use.
- We’ve constructed the mannequin of the CS staff and have been in a position to estimate the influence of various potential enhancements on our KPIs (decision time and queue dimension).
Thank you numerous for studying this text. You probably have any follow-up questions or feedback, please depart them within the feedback part.
All the photographs are produced by the writer until in any other case said.
[ad_2]
Source link