Home Machine Learning Sensible Pc Simulations for Product Analysts | by Mariya Mansurova | Could, 2024

Sensible Pc Simulations for Product Analysts | by Mariya Mansurova | Could, 2024

0
Sensible Pc Simulations for Product Analysts | by Mariya Mansurova | Could, 2024

[ad_1]

At present, I wish to present you an instance of the discrete-event simulation strategy. We are going to mannequin the Buyer Assist crew and resolve 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. Certainly one of my topics was Queueing principle, and to get a closing 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 principle behind this matter and easy methods to strategy it.

I used to be decided to get a wonderful 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 received my A grade.

At this level (as usually occurs with college students), I had a sense that this data would not be useful for my future work. Nonetheless, later, I realised that many analytical duties may be solved with this strategy. So, I wish to share it with you.

One of the crucial obvious use instances for agent-based simulations is Operational analytics. Most merchandise have buyer assist the place shoppers can get assist. A CS crew usually seems 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 now have in a backlog proper now.

With out a correct mannequin, it could be tough to grasp how our modifications (i.e. introducing night time 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 will likely be modelling the system. The system is a set of entities (for instance, individuals, servers and even mechanical instruments) that work together with one another to attain some logical purpose (i.e. answering a buyer query or passing border management in an airport).

You possibly can outline the system with the wanted granularity degree, relying in your analysis purpose. For instance, in our case, we wish to examine how the modifications to brokers’ effectivity and schedules might have an effect on common CS ticket decision time. So, the system will likely be only a set of brokers. Nonetheless, if we wish to mannequin the potential of outsourcing some tickets to totally different outsourcing firms, 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 meanwhile in time. These variables outline the system state.

There are two kinds of methods:

  • discretewhen 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 consistently evolving. One such instance is a flying airplane, wherein coordinates, velocity, top, and different parameters change on a regular basis throughout flight.

For our job, 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 deadlines. These time factors are the place occasions happen and immediately change the system state.

So, the entire strategy is predicated on occasions. We are going to generate and course of occasions one after the other to simulate how the system works. We will use the idea of a timeline to construction occasions.

Since this course of is dynamic, we have to maintain monitor of the present worth of simulated time and have the ability to advance it from one worth to a different. The variable in a simulation mannequin that reveals the present time is usually 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 transferring 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 grasp, implement and debug. So, I’ll persist with it for this text.

Let’s overview a easy instance to grasp the way it works. We are going to focus on a simplified case of the CS tickets queue.

We begin the simulation, initialising the simulation clock. Generally, individuals use zero because the preliminary worth. I choose to make use of real-life information and the precise date occasions.

Here is the preliminary state of our system. We’ve got 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 probably the most imminent occasion isn’t the second buyer request however the completion of the primary job.

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 are going to repeat this strategy of transferring from one occasion to a different till the tip of the simulation.

To keep away from unending 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 job.

We have mentioned the idea of discrete occasion simulations and understood the way it works. Now, it is time to observe and implement this strategy in code.

Goal-oriented programming

In my day-to-day job, I often use a procedural programming paradigm. I create features for some repetitive duties, however slightly than that, my code is sort of linear. It is fairly commonplace strategy for data-wrangling duties.

On this instance, we might use Goal-Oriented Programming. So, let’s spend a while revising this matter when 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 might be referred to as attributes) and actions (features or strategies). The entire program describes the interactions between totally different objects. For instance, if we now have an object representing a CS agent, it may have the next properties:

  • attributes: identify, date when an agent began working, common time they spend on duties or present standing ("out of workplace", "engaged on job" or "free").
  • strategies: return the identify, replace the standing or begin processing a buyer request.

To characterize 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, identify, average_handling_time):
# saving parameters talked about throughout object creation
self.identify = identify
self.average_handling_time = average_handling_time
# specifying fixed worth
self.function = 'CS agent'
print('Created %s with identify %s' % (self.function, self.identify))

def get_name(self):
return self.identify

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 identify, common dealing with time, and function. I’ve additionally added a few features that may return inside variables following the incapsulation sample. Additionally, we now have the update_handling_time perform that permits us to replace the agent’s efficiency.

We have created a category (an object that explains any type of CS agent). Let’s make an occasion of the thing — the agent John Doe.

john_agent = CSAgent('John Doe', 12.3)
# Created CS agent with identify John Doe

After we created an occasion of the category, the perform __init__ was executed. We will use __dict__ property to current class fields as a dictionary. It usually may be useful, for instance, if you wish to convert a listing of objects into an information body.

print(john_agent.__dict__)
# {'identify': 'John Doe', 'average_handling_time': 12.3, 'function': 'CS agent'}

We will 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.40

print(john_agent.get_handling_time())
# 5.4

One of many basic ideas of OOP that we are going to use at the moment is inheritance. Inheritance permits us to have a high-level ancestor class and use its options within the descendant courses. Think about we need to haven’t solely CS brokers but in addition KYC brokers. We will create a high-level Agent class with frequent performance and outline it solely as soon as for each KYC and CS brokers.

class Agent:
# initialising class
def __init__(self, identify, average_handling_time, function):
# saving parameters talked about throughout object creation
self.identify = identify
self.average_handling_time = average_handling_time
self.function = function
print('Created %s with identify %s' % (self.function, self.identify))

def get_name(self):
return self.identify

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 totally different __init__ and get_job_description features.

class KYCAgent(Agent):
def __init__(self, identify, average_handling_time):
tremendous().__init__(identify, 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, identify, average_handling_time):
tremendous().__init__(identify, average_handling_time, 'CS agent')

def get_job_description(self):
return 'CS (Buyer Assist) reply buyer questions and assist resolving their issues'

To specify inheritance, we talked about the bottom class in brackets after the present class identify. 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 verify whether or not they work as anticipated.

marie_agent = KYCAgent('Marie', 25)
max_agent = CSAgent('Max', 10)

print(marie_agent.__dict__)
# {'identify': 'Marie', 'average_handling_time': 25, 'function': 'KYC agent'}
print(max_agent.__dict__)
# {'identify': 'Max', 'average_handling_time': 10, 'function': 'CS agent'}

Let’s replace Marie’s dealing with time. Though we haven’t carried out this perform 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 will additionally name the strategies we outlined within the courses.

print(marie_agent.get_job_description())
# KYC (Know Your Buyer) brokers assist to confirm paperwork

print(max_agent.get_job_description())
# CS (Buyer Assist) reply buyer questions and assist resolving their issues

So, we have lined 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 job and the mannequin we want for our simulation.

Structure: courses

For those who haven’t used OOP loads earlier than, switching your mindset from procedures to things could 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 will likely be simpler to learn and assist over time. Nonetheless, there are a variety of debates about the most effective practices, even amongst mature software program engineers, so I wouldn’t trouble attempting to make it good an excessive amount of for analytical ad-hoc analysis.

Let’s take into consideration what objects we want in our simulation:

  • System — probably the most high-level idea we now have in our job. The system will characterize the present state and execute the simulation.
  • As we mentioned earlier than, the system is a set of entities. So, the following object we want 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 will likely be engaged on buyer requests. So, it is a no-brainer— we have to characterize them in our system. Additionally, we’ll retailer a listing 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, it is going to 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 category Occasion for them. Since we may have a bunch of various occasion sorts that we have to course of in a different way, we are able to leverage the OOP inheritance. We are going to focus on 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 exploit 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 isn’t tremendous detailed. For instance, it doesn’t embrace all subject names and strategies. It is intentional. This schema will likely be used as a helicopter view to information the event. So, I do not need to spend an excessive amount of time writing down all the sector and technique names as a result of these particulars would possibly change through the implementation.

Structure: occasion sorts

We have lined this system structure, and now it is time to consider the principle drivers of our simulation — occasions.

Let’s focus on 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 job (if we now have any ready within the queue).
  • We have to know when brokers begin working. These working hours can rely upon an agent and the day of the week. Doubtlessly, we’d even need to change the schedules through the simulation. It is fairly difficult to create all “Agent Prepared” occasions once we initialise the system (particularly since we do not know the way 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 want is a “New Buyer Request” — an occasion that reveals that we received 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 Activity“, which reveals that the agent completed the duty he was engaged on and is doubtlessly prepared to choose up a brand new job.

That is it. These 4 occasions are sufficient to run the entire simulation.

Much like courses, there aren’t any proper or incorrect solutions for system modelling. You would possibly use a barely totally different set of occasions. For instance, you possibly can add a “Begin Activity” occasion to have it explicitly.

Yow will discover the complete implementation on GitHub.

We have outlined the high-level construction of our answer, so we’re prepared to start out 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 wish to specify for the simulation:

  • brokers — set of brokers that will likely be working within the CS crew,
  • queue — the present queue of buyer requests (if we now have 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 wish to print some data for debugging,
  • customer_requests_df — information body with details about the set of buyer requests we wish 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 info after simulation.

It is time to take the mandatory 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.

Here 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_time

self._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 making certain 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.kind(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 aren’t 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_time

def __str__(self):
return f'Buyer Request {self.id}: {self.creation_time.strftime("%Y-%m-%d %H:%M:%S")}'

It is a easy information class that accommodates solely parameters. The one new factor right here is that I’ve overridden the __str__ technique to alter the output of a print perform. It is fairly useful for debugging. You possibly can evaluate 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 carried out 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 = queue

def 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_periods

def 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 now have 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 and identify — 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 unravel the actual job.

We’ve got the next strategies carried out for brokers:

  • understanding whether or not they can tackle a brand new job (whether or not they’re free and nonetheless working),
  • begin and end processing the client request.
class Agent:
def __init__(self, id, identify, schedule, effectiveness = 1):
self.id = id
self.schedule = schedule
self.identify = identify
self.request_in_work = None
self.effectiveness = effectiveness

def 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__ perform (in addition to the occasions processing that we are going to focus on intimately a bit later) is load_customer_request_events perform implementation. It is fairly simple. 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__ features, however doubtlessly, it may assist us present further performance for all occasions.

class Occasion:
def __init__(self, event_type, time):
self.sort = event_type
self.time = time

def __str__(self):
if self.sort == 'agent_ready_for_task':
return '%s (%s) - %s' % (self.sort, self.agent.identify, self.time)
return '%s - %s' % (self.sort, self.time)

Then, I implement a separate subclass for every occasion sort which may have a bit totally 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 = agent

def 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 isn't None:
self.agent.start_task(next_customer_request)
next_customer_request.start_time = system.current_time
next_customer_request.agent_name = self.agent.identify
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.identify, 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 at the moment' % (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.identify, self.customer_request.id))

# decide up the following request if agent proceed working and we now have 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 isn't None:
self.agent.start_task(next_customer_request)
next_customer_request.start_time = system.current_time
next_customer_request.agent_name = self.agent.identify
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.identify, 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):
# verify whether or not we now have 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.identify
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.identify, 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 truly it with the occasions processing enterprise logic. The one bit we have to end is to place the whole lot collectively to run our simulation.

Placing all collectively within the system class

As we mentioned, the System class will likely be in control of working the simulations. So, we’ll put the remaining nuts and bolts there.

Here 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 executes course 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 sort, I verify whether or not there are another occasions within the timeline and if not, I do not course of it since we needn’t schedule brokers anymore.
  • run_simulation is the perform that guidelines our world, however since we now have fairly an honest structure, it is a few strains: we verify 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 now have 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.sort == 'plan_agents_schedule') and self.is_simulation_finished():
if self.logging:
print("FINISH")
else:
self.current_time = occasion.time
occasion.course of(self)

# essential perform
def run_simulation(self):
whereas not self.is_simulation_finished():
self.process_next_event()

It was a protracted journey, however we have accomplished it. Wonderful job! Now, we now have all of the logic we want. Let’s transfer on to the humorous half and use our mannequin for evaluation.

Yow will discover the complete implementation on GitHub.

I’ll use an artificial Buyer Requests dataset to simulate totally different Ops setups.

To start with, let’s run our system and have 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)

# working the simulation
system.run_simulation()

On account of the execution, we received all of the stats in system.processed_requests. Let’s put collectively a few helper features to analyse outcomes simpler.

# convert outcomes to information 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': 'depend', '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['period'] = processed_requests_df.creation_time.map(
lambda x: (x - datetime.timedelta(x.weekday())).strftime('%Y-%m-%d')
)
queue_stats_df['period'] = 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': 'depend', 'handling_time_mins': 'imply',
'waiting_time_mins': 'imply',
'resolution_time_mins': 'imply'})
.be 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 perform to analyse the simulation outcomes. Apparently, 15 brokers should not sufficient for our product since, after three weeks, we now have 4K+ requests in a queue and a mean decision time of round ten days. Clients can be very sad with our service if we had simply 15 brokers.

Let’s learn the way many brokers we want to have the ability to deal with the demand. We will run a bunch of simulations with the totally different variety of brokers and evaluate 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 will 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 isn’t rising week after week.

If we mannequin the state of affairs when we now have 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 typical decision time is 500.67 minutes, and the typical queue size is 259.39.

Let’s attempt to consider the attainable enhancements for our Operations crew:

  • 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 offer protection throughout weekends and late hours.

Since we now have a mannequin, we are able to simply estimate all of the alternatives and decide probably the most possible one.

The primary two approaches are simple. Let’s focus on 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 just a little 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 break 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 now have manner much less demand. It could enhance your metrics even additional. Nonetheless, the extra impact will likely be marginal.

If we run simulations for all these eventualities, surprisingly, we’ll see that KPIs will likely be manner higher if we simply change brokers’ schedules. If we rent 5 extra individuals or enhance brokers’ efficiency by 30%, we can’t obtain such a big enchancment.

Let’s examine how modifications in brokers’ schedules have an effect on our KPIs. Decision time grows just for instances exterior working hours (from 20 to 7), and queue dimension by no means reaches 200 instances.

That is a wonderful end result. Our simulation mannequin has helped us prioritise operational modifications as a substitute of hiring extra individuals or investing in LLM instrument growth.

We have mentioned the fundamentals of this strategy on this article. If you wish to dig deeper and use it in observe, listed here are a pair extra solutions that could be helpful:

  • Earlier than beginning to use such fashions in manufacturing, it’s value testing them. Essentially the most simple manner is to mannequin your present state of affairs and evaluate the principle KPIs. In the event that they differ loads, then your system doesn’t characterize the actual world properly sufficient, and you’ll want 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 selections. In enterprise, we additionally care about prices. So, it is value taking a look at this job from an operational perspective as properly, i.e. measure the proportion of time when brokers do not have duties to work on (which suggests we’re paying them for nothing).
  • In actual life, there could 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 crew 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 sort, day of the week, and so on.) 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 at the moment:

  • We’ve discovered the fundamentals of the discrete-event simulation strategy that helps to mannequin discrete methods 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 job than the frequent procedural code information analysts often use.
  • We’ve constructed the mannequin of the CS crew and have been capable of estimate the impression of various potential enhancements on our KPIs (decision time and queue dimension).

Thank you numerous for studying this text. In case you have any follow-up questions or feedback, please go away them within the feedback part.

All the photographs are produced by the creator except in any other case said.

[ad_2]