10  Priority-Based Queueing

So far, we’ve assumed that the queues in our models follow a FIFO (First in First Out) policy. This means that whoever has been queuing the longest is seen next.

But in healthcare systems, very often there is an element of prioritisation in a real world queue. Typically this represents the severity of the patient’s condition.

We can build in priority-based queuing in our SimPy models in a few different ways - but one of the easiest is using something known as a PriorityResource.

A PriorityResource is a class in SimPy that’s like the standard Resource class we’ve used so far, but also has functionality that allows it to select which entity to pull out of a queue next based on a priority value we specify.

The way this works in SimPy is :

  1. We set up resources that will be dealing with priority-based queues as PriorityResources rather than Resources

  2. We have an attribute stored against the entity that specifies that entity’s priority (with lower values indicating higher priority)

  3. When we request a PriorityResource, we tell it the attribute to use to determine priority in that queue (this also means we could have multiple attributes for priority and use different ones for different queues).

10.1 Implementing priority-based queueing

10.1.1 g class

The g class is unchanged

10.1.2 Patient class

Here we add an attribute of the patient that determines their priority :::{.callout-tip} When using a priority resource

Lower value = higher priority :::

In this example, we just randomly pick a value between 1 and 5, but you can use whatever logic you like.

In reality, you’d likely have probabilities to determine what priority a patient is based on your data - maybe there’s a 20% chance they are a high priority and an 80% chance they are a low priority.

class Patient:
    def __init__(self, p_id):
        self.id = p_id
        self.q_time_nurse = 0
        ##NEW
        self.priority = random.randint(1,5)

10.1.3 Model class

10.1.3.1 _init

Here we set up the nurse as an instance of PriorityResource rather than Resource

def __init__(self, run_number):
        # Set up SimPy environment
        self.env = simpy.Environment()

        # Set up counters to use as entity IDs
        self.patient_counter = 0

        # Set up resources
        ##NEW
        self.nurse = simpy.PriorityResource(self.env,
                                            capacity=g.number_of_nurses)

        # Set run number from value passed in
        self.run_number = run_number


        # Set up DataFrame to store patient-level results
        self.results_df = pd.DataFrame()
        self.results_df["Patient ID"] = [1]
        self.results_df["Q Time Nurse"] = [0.0]
        self.results_df["Priority"] = [1] ##NEW
        self.results_df.set_index("Patient ID", inplace=True)

        # Set up attributes that will store mean queuing times across the run
        self.mean_q_time_nurse = 0

10.1.3.2 attend_clinic

Near the beginning of the attend_clinic() method, we have added a print message so we can see how priority works.

Tip

Logging in this way can help you check that your model is behaving as expected.

Now that the nurse is set up as a PriorityResource, we can pass in the value that we want it to look at to determine who’s seen next when we request the resource (here, that’s the priority attribute of the patient we set up in the Patient class).

We have also added a step that records the patient priority to our dataframe of individual patient results.

# Generator function representing pathway for patients attending the
    # clinic.
    def attend_clinic(self, patient):
        # Nurse consultation activity
        start_q_nurse = self.env.now

        ##NEW
        print (f"Patient {patient.id} with priority {patient.priority} is",
               "queuing for the nurse.")

        ##NEW
        with self.nurse.request(priority=patient.priority) as req:
            yield req

            end_q_nurse = self.env.now

            ##NEW
            print (f"Patient {patient.id} with priority {patient.priority} is",
                   f"being seen at minute {self.env.now}.")

            patient.q_time_nurse = end_q_nurse - start_q_nurse

            if self.env.now > g.warm_up_period:
                self.results_df.at[patient.id, "Q Time Nurse"] = (
                    patient.q_time_nurse
                )

                ##NEW
                self.results_df.at[patient.id, "Priority"] = (
                    patient.priority
                )

            sampled_nurse_act_time = random.expovariate(1.0 /
                                                        g.mean_n_consult_time)

            yield self.env.timeout(sampled_nurse_act_time)

10.1.4 Trial class

The trial class is unchanged.

10.2 The full code

The full code is given below.

Click here to view the full code
import simpy
import random
import pandas as pd

# Class to store global parameter values.
class g:
    # Inter-arrival times
    patient_inter = 5

    # Activity times
    mean_n_consult_time = 6

    # Resource numbers
    number_of_nurses = 1

    ##NEW - We've changed the parameters to have no warm-up
    # Simulation meta parameters
    sim_duration = 5000
    number_of_runs = 1
    warm_up_period = 0

# Class representing patients coming in to the clinic.
class Patient:
    def __init__(self, p_id):
        self.id = p_id
        self.q_time_nurse = 0
        ##NEW - here we add an attribute of the patient that determines their
        # priority (lower value = higher priority).  In this example, we just
        # randomly pick a value between 1 and 5, but you can use whatever logic
        # you like (in reality, you'd likely have probabilities to determine
        # what priority a patient is based on your data)
        self.priority = random.randint(1,5)

# Class representing our model of the clinic.
class Model:
    # Constructor
    def __init__(self, run_number):
        # Set up SimPy environment
        self.env = simpy.Environment()

        # Set up counters to use as entity IDs
        self.patient_counter = 0

        # Set up resources
        ##NEW - here we set up the nurse as an instance of PriorityResource
        # rather than Resource
        self.nurse = simpy.PriorityResource(self.env,
                                            capacity=g.number_of_nurses)

        # Set run number from value passed in
        self.run_number = run_number

        # Set up DataFrame to store patient-level results
        self.results_df = pd.DataFrame()
        self.results_df["Patient ID"] = [1]
        self.results_df["Q Time Nurse"] = [0.0]
        self.results_df["Priority"] = [1] ##NEW
        self.results_df.set_index("Patient ID", inplace=True)

        # Set up attributes that will store mean queuing times across the run
        self.mean_q_time_nurse = 0

    # Generator function that represents the DES generator for patient arrivals
    def generator_patient_arrivals(self):
        while True:
            self.patient_counter += 1

            p = Patient(self.patient_counter)

            self.env.process(self.attend_clinic(p))

            sampled_inter = random.expovariate(1.0 / g.patient_inter)

            yield self.env.timeout(sampled_inter)

    # Generator function representing pathway for patients attending the
    # clinic.
    def attend_clinic(self, patient):
        # Nurse consultation activity
        start_q_nurse = self.env.now

        ##NEW - added a print message so we can see how priority works
        # I'm limiting it to the first 10 patients so we're not swamped by outputs!
        if patient.id <= 10:
            print (f"Patient {patient.id} with priority {patient.priority} is",
                "queuing for the nurse.")

        ##NEW - now that the nurse is set up as a PriorityResource, we can pass
        # in the value that we want it to look at to determine who's seen next
        # when we request the resource (here, that's the priority attribute of
        # the patient we set up in the Patient class)
        with self.nurse.request(priority=patient.priority) as req:
            yield req

            end_q_nurse = self.env.now

            ##NEW - added a print message so we can see how priority works
            # I'm limiting it to the first 10 patients so we're not swamped by outputs!
            if patient.id <= 10:
                print (f"Patient {patient.id} with priority {patient.priority} is",
                    f"being seen at minute {self.env.now}")

            patient.q_time_nurse = end_q_nurse - start_q_nurse

            if self.env.now > g.warm_up_period:
                self.results_df.at[patient.id, "Q Time Nurse"] = (
                    patient.q_time_nurse
                )

                self.results_df.at[patient.id, "Priority"] = (
                    patient.priority
                )

            sampled_nurse_act_time = random.expovariate(1.0 /
                                                        g.mean_n_consult_time)

            yield self.env.timeout(sampled_nurse_act_time)

    # Method to calculate and store results over the run
    def calculate_run_results(self):
        self.mean_q_time_nurse = self.results_df["Q Time Nurse"].mean()

    # Method to run a single run of the simulation
    def run(self):
        # Start up DES generators
        self.env.process(self.generator_patient_arrivals())

        # Run for the duration specified in g class
        self.env.run(until=(g.sim_duration + g.warm_up_period))

        # Calculate results over the run
        self.calculate_run_results()

        return self.results_df

# Class representing a Trial for our simulation
class Trial:
    # Constructor
    def  __init__(self):
        self.df_trial_results = pd.DataFrame()
        self.df_trial_results["Run Number"] = [0]
        self.df_trial_results["Mean Q Time Nurse"] = [0.0]
        self.df_trial_results.set_index("Run Number", inplace=True)

    # Method to calculate and store means across runs in the trial
    def calculate_means_over_trial(self):
        self.mean_q_time_nurse_trial = (
            self.df_trial_results["Mean Q Time Nurse"].mean()
        )

    # Method to calculate and store means across runs in the trial
    def calculate_means_over_trial(self):
        self.mean_q_time_nurse_trial = (
            self.df_trial_results["Mean Q Time Nurse"].mean()
        )

    def run_trial(self):
        # Run the simulation for the number of runs specified in g class.
        # For each run, we create a new instance of the Model class and call its
        # run method, which sets everything else in motion.  Once the run has
        # completed, we grab out the stored run results and store it against
        # the run number in the trial results dataframe. We also return the
        # full patient-level dataframes.

        # First, create an empty list for storing our patient-level dataframes.
        results_dfs = []

        for run in range(g.number_of_runs):
            my_model = Model(run)
            patient_level_results = my_model.run()

            print( self.df_trial_results)
            # First let's record our mean wait time for this run
            self.df_trial_results.loc[run] = [my_model.mean_q_time_nurse]

            # Next let's work on our patient-level results dataframes
            # We start by rounding everything to 2 decimal places
            patient_level_results = patient_level_results.round(2)
            # Add a new column recording the run
            patient_level_results['run'] = run
            # Now we're just going to add this to our empty list (or, after the first
            # time we loop through, as an extra dataframe in our list)
            results_dfs.append(patient_level_results)

        all_results_patient_level = pd.concat(results_dfs)

        # This calculates the attribute self.mean_q_time_nurse_trial
        self.calculate_means_over_trial()

        # Once the trial (ie all runs) has completed, return the results
        return self.df_trial_results, all_results_patient_level, self.mean_q_time_nurse_trial

    # Method to print trial results, including averages across runs
    def print_trial_results(self):
        print ("Trial Results")
        print (self.df_trial_results)

        print (f"Mean Q Nurse : {self.mean_q_time_nurse_trial:.1f} minutes")

# Create new instance of Trial and run it
my_trial = Trial()
df_trial_results, all_results_patient_level, means_over_trial  = my_trial.run_trial()
Patient 1 with priority 3 is queuing for the nurse.
Patient 1 with priority 3 is being seen at minute 0
Patient 2 with priority 3 is queuing for the nurse.
Patient 2 with priority 3 is being seen at minute 1.7444855927356335
Patient 3 with priority 3 is queuing for the nurse.
Patient 3 with priority 3 is being seen at minute 25.07633504139986
Patient 4 with priority 2 is queuing for the nurse.
Patient 4 with priority 2 is being seen at minute 29.18878561595557
Patient 5 with priority 1 is queuing for the nurse.
Patient 5 with priority 1 is being seen at minute 32.09348545656953
Patient 6 with priority 5 is queuing for the nurse.
Patient 6 with priority 5 is being seen at minute 35.642288982269086
Patient 7 with priority 2 is queuing for the nurse.
Patient 8 with priority 4 is queuing for the nurse.
Patient 9 with priority 3 is queuing for the nurse.
Patient 7 with priority 2 is being seen at minute 47.64251549714372
Patient 10 with priority 5 is queuing for the nurse.
Patient 9 with priority 3 is being seen at minute 53.74077636812673
Patient 8 with priority 4 is being seen at minute 54.38743141971299
Patient 10 with priority 5 is being seen at minute 56.55048366668566
            Mean Q Time Nurse
Run Number                   
0                         0.0

10.3 Evaluating the outputs

First let’s look at some sample patients.

all_results_patient_level.head()
Q Time Nurse Priority run
Patient ID
1 0.0 1.0 0
2 0.0 3.0 0
3 0.0 3.0 0
4 0.0 2.0 0
5 0.0 1.0 0

Let’s calculate the mean queue time by priority.

(all_results_patient_level
    .groupby('Priority')
    .agg({'Priority':'size', 'Q Time Nurse':'mean'}) \
    .rename(columns={'Priority':'count','Q Time Nurse':'mean queue time'})
    .round(2)
    )
count mean queue time
Priority
1.0 212 7.02
2.0 205 14.10
3.0 202 35.49
4.0 172 303.78
5.0 19 2343.31

We can see that the queueing time is shorter for the clients with a lower priority value (and therefore a higher actual priority in terms of the model - i.e. they will be seen first)

Remember that we are only recording the queue time at the point at which someone exits the queue to be seen by a nurse.

This means that there may be lots of people - particularly those with a higher priority number (and therefore the least important to see as far as the model is concerned) who are still sitting waiting to be seen when our model stops running.

Think about ways you might try to account for that.

import plotly.express as px

fig = px.box(all_results_patient_level.reset_index(), x="Priority", y="Q Time Nurse", points="all")
fig.show()