Back to Article
View Imported Code, which has had logging steps added at the appropriate points in the ‘model’ class
Download Notebook
In [4]:
from examples.example_2_branching_multistep.ex_2_model_classes import Trial, g
import pandas as pd
import os
In [5]:
g.sim_duration = 3000
g.number_of_runs = 1
import random
import numpy as np
import pandas as pd
import simpy
from sim_tools.distributions import Exponential, Lognormal, Uniform, Normal, Bernoulli
from vidigi.utils import populate_store
from examples.simulation_utility_functions import trace


# Class to store global parameter values.  We don't create an instance of this
# class - we just refer to the class blueprint itself to access the numbers
# inside.
class g:
    '''
    Create a scenario to parameterise the simulation model

    Parameters:
    -----------
    random_number_set: int, optional (default=DEFAULT_RNG_SET)
        Set to control the initial seeds of each stream of pseudo
        random numbers used in the model.

    n_triage: int
        The number of triage cubicles

    n_reg: int
        The number of registration clerks

    n_exam: int
        The number of examination rooms

    n_trauma: int
        The number of trauma bays for stablisation

    n_cubicles_non_trauma_treat: int
        The number of non-trauma treatment cubicles

    n_cubicles_trauma_treat: int
        The number of trauma treatment cubicles

    triage_mean: float
        Mean duration of the triage distribution (Exponential)

    reg_mean: float
        Mean duration of the registration distribution (Lognormal)

    reg_var: float
        Variance of the registration distribution (Lognormal)

    exam_mean: float
        Mean of the examination distribution (Normal)

    exam_var: float
        Variance of the examination distribution (Normal)

    trauma_mean: float
        Mean of the trauma stabilisation distribution (Exponential)

    trauma_treat_mean: float
        Mean of the trauma cubicle treatment distribution (Lognormal)

    trauma_treat_var: float
        Variance of the trauma cubicle treatment distribution (Lognormal)

    non_trauma_treat_mean: float
        Mean of the non trauma treatment distribution

    non_trauma_treat_var: float
        Variance of the non trauma treatment distribution

    non_trauma_treat_p: float
        Probability non trauma patient requires treatment

    prob_trauma: float
        probability that a new arrival is a trauma patient.
    '''
    random_number_set = 42

    n_triage=2
    n_reg=2
    n_exam=3
    n_trauma=4
    n_cubicles_non_trauma_treat=4
    n_cubicles_trauma_treat=5

    triage_mean=6
    reg_mean=8
    reg_var=2
    exam_mean=16
    exam_var=3
    trauma_mean=90
    trauma_treat_mean=30
    trauma_treat_var=4
    non_trauma_treat_mean=13.3
    non_trauma_treat_var=2

    non_trauma_treat_p=0.6
    prob_trauma=0.12

    arrival_df="ed_arrivals.csv"

    sim_duration = 600
    number_of_runs = 100

# Class representing patients coming in to the clinic.
class Patient:
    '''
    Class defining details for a patient entity
    '''
    def __init__(self, p_id):
        '''
        Constructor method

        Params:
        -----
        identifier: int
            a numeric identifier for the patient.
        '''
        self.identifier = p_id

        # Time of arrival in model/at centre
        self.arrival = -np.inf
        # Total time in pathway
        self.total_time = -np.inf

        # Shared waits
        self.wait_triage = -np.inf
        self.wait_reg = -np.inf
        self.wait_treat = -np.inf
        # Non-trauma pathway - examination wait
        self.wait_exam = -np.inf
        # Trauma pathway - stabilisation wait
        self.wait_trauma = -np.inf

        # Shared durations
        self.triage_duration = -np.inf
        self.reg_duration = -np.inf
        self.treat_duration = -np.inf

        # Non-trauma pathway - examination duration
        self.exam_duration = -np.inf
        # Trauma pathway - stabilisation duration
        self.trauma_duration = -np.inf


# Class representing our model of the clinic.
class Model:
    '''
    Simulates the simplest minor treatment process for a patient

    1. Arrive
    2. Examined/treated by nurse when one available
    3. Discharged
    '''
    # Constructor to set up the model for a run.  We pass in a run number when
    # we create a new model.
    def __init__(self, run_number):
        # Create a SimPy environment in which everything will live
        self.env = simpy.Environment()

        self.event_log = []

        # Create a patient counter (which we'll use as a patient ID)
        self.patient_counter = 0

        self.trauma_patients = []
        self.non_trauma_patients = []

        # Create our resources
        self.init_resources()

        # Store the passed in run number
        self.run_number = run_number

        # Create a new Pandas DataFrame that will store some results against
        # the patient ID (which we'll use as the index).
        self.results_df = pd.DataFrame()
        self.results_df["Patient ID"] = [1]
        self.results_df["Queue Time Cubicle"] = [0.0]
        self.results_df["Time with Nurse"] = [0.0]
        self.results_df.set_index("Patient ID", inplace=True)

        # Create an attribute to store the mean queuing times across this run of
        # the model
        self.mean_q_time_cubicle = 0

        # create distributions

        # Triage duration
        self.triage_dist = Exponential(g.triage_mean,
                                       random_seed=self.run_number*g.random_number_set)

        # Registration duration (non-trauma only)
        self.reg_dist = Lognormal(g.reg_mean,
                                  np.sqrt(g.reg_var),
                                  random_seed=self.run_number*g.random_number_set)

        # Evaluation (non-trauma only)
        self.exam_dist = Normal(g.exam_mean,
                                np.sqrt(g.exam_var),
                                random_seed=self.run_number*g.random_number_set)

        # Trauma/stablisation duration (trauma only)
        self.trauma_dist = Exponential(g.trauma_mean,
                                       random_seed=self.run_number*g.random_number_set)

        # Non-trauma treatment
        self.nt_treat_dist = Lognormal(g.non_trauma_treat_mean,
                                       np.sqrt(g.non_trauma_treat_var),
                                       random_seed=self.run_number*g.random_number_set)

        # treatment of trauma patients
        self.treat_dist = Lognormal(g.trauma_treat_mean,
                                    np.sqrt(g.non_trauma_treat_var),
                                    random_seed=self.run_number*g.random_number_set)

        # probability of non-trauma patient requiring treatment
        self.nt_p_treat_dist = Bernoulli(g.non_trauma_treat_p,
                                         random_seed=self.run_number*g.random_number_set)

        # probability of non-trauma versus trauma patient
        self.p_trauma_dist = Bernoulli(g.prob_trauma,
                                       random_seed=self.run_number*g.random_number_set)

        # init sampling for non-stationary poisson process
        self.init_nspp()

    def init_nspp(self):

        # read arrival profile
        self.arrivals = pd.read_csv(g.arrival_df)  # pylint: disable=attribute-defined-outside-init
        self.arrivals['mean_iat'] = 60 / self.arrivals['arrival_rate']

        # maximum arrival rate (smallest time between arrivals)
        self.lambda_max = self.arrivals['arrival_rate'].max()  # pylint: disable=attribute-defined-outside-init

        # thinning exponential
        self.arrival_dist = Exponential(60.0 / self.lambda_max,  # pylint: disable=attribute-defined-outside-init
                                            random_seed=self.run_number*g.random_number_set)

        # thinning uniform rng
        self.thinning_rng = Uniform(low=0.0, high=1.0,  # pylint: disable=attribute-defined-outside-init
                                    random_seed=self.run_number*g.random_number_set)


    def init_resources(self):
        '''
        Init the number of resources
        and store in the arguments container object

        Resource list:
            1. Nurses/treatment bays (same thing in this model)

        '''
        # Shared Resources
        self.triage_cubicles = simpy.Store(self.env)
        populate_store(num_resources=g.n_triage,
                simpy_store=self.triage_cubicles,
                sim_env=self.env)

        self.registration_cubicles = simpy.Store(self.env)
        populate_store(num_resources=g.n_reg,
                       simpy_store=self.registration_cubicles,
                       sim_env=self.env)

        # Non-trauma
        self.exam_cubicles = simpy.Store(self.env)
        populate_store(num_resources=g.n_exam,
                       simpy_store=self.exam_cubicles,
                       sim_env=self.env)

        self.non_trauma_treatment_cubicles = simpy.Store(self.env)
        populate_store(num_resources=g.n_cubicles_non_trauma_treat,
                       simpy_store=self.non_trauma_treatment_cubicles,
                       sim_env=self.env)

        # Trauma
        self.trauma_stabilisation_bays = simpy.Store(self.env)
        populate_store(num_resources=g.n_trauma,
                       simpy_store=self.trauma_stabilisation_bays,
                       sim_env=self.env)

        self.trauma_treatment_cubicles = simpy.Store(self.env)
        populate_store(num_resources=g.n_cubicles_trauma_treat,
                       simpy_store=self.trauma_treatment_cubicles,
                       sim_env=self.env)

    # A generator function that represents the DES generator for patient
    # arrivals
    def generator_patient_arrivals(self):
        # We use an infinite loop here to keep doing this indefinitely whilst
        # the simulation runs
        while True:
            t = int(self.env.now // 60) % self.arrivals.shape[0]
            lambda_t = self.arrivals['arrival_rate'].iloc[t]

            # set to a large number so that at least 1 sample taken!
            u = np.Inf

            interarrival_time = 0.0
            # reject samples if u >= lambda_t / lambda_max
            while u >= (lambda_t / self.lambda_max):
                interarrival_time += self.arrival_dist.sample()
                u = self.thinning_rng.sample()

            # Freeze this instance of this function in place until the
            # inter-arrival time we sampled above has elapsed.  Note - time in
            # SimPy progresses in "Time Units", which can represent anything
            # you like (just make sure you're consistent within the model)
            yield self.env.timeout(interarrival_time)

            # Increment the patient counter by 1 (this means our first patient
            # will have an ID of 1)
            self.patient_counter += 1

            # Create a new patient - an instance of the Patient Class we
            # defined above.  Remember, we pass in the ID when creating a
            # patient - so here we pass the patient counter to use as the ID.
            p = Patient(self.patient_counter)

            trace(f'patient {self.patient_counter} arrives at: {self.env.now:.3f}')
            self.event_log.append(
                {'patient': self.patient_counter,
                 'pathway': 'Shared',
                 'event': 'arrival',
                 'event_type': 'arrival_departure',
                 'time': self.env.now}
            )

            # sample if the patient is trauma or non-trauma
            trauma = self.p_trauma_dist.sample()

            # Tell SimPy to start up the attend_clinic generator function with
            # this patient (the generator function that will model the
            # patient's journey through the system)
            # and store patient in list for later easy access
            if trauma:
                # create and store a trauma patient to update KPIs.
                self.trauma_patients.append(p)
                self.env.process(self.attend_trauma_pathway(p))

            else:
                # create and store a non-trauma patient to update KPIs.
                self.non_trauma_patients.append(p)
                self.env.process(self.attend_non_trauma_pathway(p))

    # A generator function that represents the pathway for a patient going
    # through the clinic.
    # The patient object is passed in to the generator function so we can
    # extract information from / record information to it
    def attend_non_trauma_pathway(self, patient):
        '''
        simulates the non-trauma/minor treatment process for a patient

        1. request and wait for sign-in/triage
        2. patient registration
        3. examination
        4a. percentage discharged
        4b. remaining percentage treatment then discharge
        '''
        # record the time of arrival and entered the triage queue
        patient.arrival = self.env.now
        self.event_log.append(
            {'patient': patient.identifier,
             'pathway': 'Non-Trauma',
             'event_type': 'queue',
             'event': 'triage_wait_begins',
             'time': self.env.now}
        )

        ###################################################
        # request sign-in/triage
        triage_resource = yield self.triage_cubicles.get()

        # record the waiting time for triage
        patient.wait_triage = self.env.now - patient.arrival
        trace(f'patient {patient.identifier} triaged to minors '
                f'{self.env.now:.3f}')
        self.event_log.append(
            {'patient': patient.identifier,
                'pathway': 'Non-Trauma',
                'event_type': 'resource_use',
                'event': 'triage_begins',
                'time': self.env.now,
                'resource_id': triage_resource.id_attribute
                }
        )

        # sample triage duration.
        patient.triage_duration = self.triage_dist.sample()
        yield self.env.timeout(patient.triage_duration)

        trace(f'triage {patient.identifier} complete {self.env.now:.3f}; '
                f'waiting time was {patient.wait_triage:.3f}')
        self.event_log.append(
            {'patient': patient.identifier,
                'pathway': 'Non-Trauma',
                'event_type': 'resource_use_end',
                'event': 'triage_complete',
                'time': self.env.now,
                'resource_id': triage_resource.id_attribute}
        )

        # Resource is no longer in use, so put it back in the store
        self.triage_cubicles.put(triage_resource)
        #########################################################

        # record the time that entered the registration queue
        start_wait = self.env.now
        self.event_log.append(
            {'patient': patient.identifier,
             'pathway': 'Non-Trauma',
             'event_type': 'queue',
             'event': 'MINORS_registration_wait_begins',
             'time': self.env.now}
        )

        #########################################################
        # request registration clerk
        registration_resource = yield self.registration_cubicles.get()

        # record the waiting time for registration
        patient.wait_reg = self.env.now - start_wait
        trace(f'registration of patient {patient.identifier} at '
                f'{self.env.now:.3f}')
        self.event_log.append(
            {'patient': patient.identifier,
                'pathway': 'Non-Trauma',
                'event_type': 'resource_use',
                'event': 'MINORS_registration_begins',
                'time': self.env.now,
                'resource_id': registration_resource.id_attribute
                }
        )

        # sample registration duration.
        patient.reg_duration = self.reg_dist.sample()
        yield self.env.timeout(patient.reg_duration)

        trace(f'patient {patient.identifier} registered at'
                f'{self.env.now:.3f}; '
                f'waiting time was {patient.wait_reg:.3f}')
        self.event_log.append(
            {'patient': patient.identifier,
                'pathway': 'Non-Trauma',
                'event': 'MINORS_registration_complete',
                'event_type': 'resource_use_end',
                'time': self.env.now,
                'resource_id': registration_resource.id_attribute}
        )
        # Resource is no longer in use, so put it back in the store
        self.registration_cubicles.put(registration_resource)
        ########################################################

        # record the time that entered the evaluation queue
        start_wait = self.env.now

        self.event_log.append(
            {'patient': patient.identifier,
             'pathway': 'Non-Trauma',
             'event': 'MINORS_examination_wait_begins',
             'event_type': 'queue',
             'time': self.env.now}
        )

        #########################################################
        # request examination resource
        examination_resource = yield self.exam_cubicles.get()

        # record the waiting time for examination to begin
        patient.wait_exam = self.env.now - start_wait
        trace(f'examination of patient {patient.identifier} begins '
                f'{self.env.now:.3f}')
        self.event_log.append(
            {'patient': patient.identifier,
                'pathway': 'Non-Trauma',
                'event': 'MINORS_examination_begins',
                'event_type': 'resource_use',
                'time': self.env.now,
                'resource_id': examination_resource.id_attribute
                }
        )

        # sample examination duration.
        patient.exam_duration = self.exam_dist.sample()
        yield self.env.timeout(patient.exam_duration)

        trace(f'patient {patient.identifier} examination complete '
                f'at {self.env.now:.3f};'
                f'waiting time was {patient.wait_exam:.3f}')
        self.event_log.append(
            {'patient': patient.identifier,
                'pathway': 'Non-Trauma',
                'event': 'MINORS_examination_complete',
                'event_type': 'resource_use_end',
                'time': self.env.now,
                'resource_id': examination_resource.id_attribute}
        )
        # Resource is no longer in use, so put it back in
        self.exam_cubicles.put(examination_resource)
        ############################################################################

        # sample if patient requires treatment?
        patient.require_treat = self.nt_p_treat_dist.sample()  #pylint: disable=attribute-defined-outside-init

        if patient.require_treat:

            self.event_log.append(
                {'patient': patient.identifier,
                 'pathway': 'Non-Trauma',
                 'event': 'requires_treatment',
                 'event_type': 'attribute_assigned',
                 'time': self.env.now}
            )

            # record the time that entered the treatment queue
            start_wait = self.env.now
            self.event_log.append(
                {'patient': patient.identifier,
                 'pathway': 'Non-Trauma',
                 'event': 'MINORS_treatment_wait_begins',
                 'event_type': 'queue',
                 'time': self.env.now}
            )
            ###################################################
            # request treatment cubicle

            non_trauma_treatment_resource = yield self.non_trauma_treatment_cubicles.get()

            # record the waiting time for treatment
            patient.wait_treat = self.env.now - start_wait
            trace(f'treatment of patient {patient.identifier} begins '
                    f'{self.env.now:.3f}')
            self.event_log.append(
                {'patient': patient.identifier,
                    'pathway': 'Non-Trauma',
                    'event': 'MINORS_treatment_begins',
                    'event_type': 'resource_use',
                    'time': self.env.now,
                    'resource_id': non_trauma_treatment_resource.id_attribute
                }
            )

            # sample treatment duration.
            patient.treat_duration = self.nt_treat_dist.sample()
            yield self.env.timeout(patient.treat_duration)

            trace(f'patient {patient.identifier} treatment complete '
                    f'at {self.env.now:.3f};'
                    f'waiting time was {patient.wait_treat:.3f}')
            self.event_log.append(
                {'patient': patient.identifier,
                    'pathway': 'Non-Trauma',
                    'event': 'MINORS_treatment_complete',
                    'event_type': 'resource_use_end',
                    'time': self.env.now,
                    'resource_id': non_trauma_treatment_resource.id_attribute}
            )

            # Resource is no longer in use, so put it back in the store
            self.non_trauma_treatment_cubicles.put(non_trauma_treatment_resource)
        ##########################################################################

        # Return to what happens to all patients, regardless of whether they were sampled as needing treatment
        self.event_log.append(
            {'patient': patient.identifier,
            'pathway': 'Shared',
            'event': 'depart',
            'event_type': 'arrival_departure',
            'time': self.env.now}
        )

        # total time in system
        patient.total_time = self.env.now - patient.arrival

    def attend_trauma_pathway(self, patient):
        '''
        simulates the major treatment process for a patient

        1. request and wait for sign-in/triage
        2. trauma
        3. treatment
        '''
        # record the time of arrival and entered the triage queue
        patient.arrival = self.env.now
        self.event_log.append(
            {'patient': patient.identifier,
             'pathway': 'Trauma',
             'event_type': 'queue',
             'event': 'triage_wait_begins',
             'time': self.env.now}
        )

        ###################################################
        # request sign-in/triage
        triage_resource = yield self.triage_cubicles.get()

        # record the waiting time for triage
        patient.wait_triage = self.env.now - patient.arrival

        trace(f'patient {patient.identifier} triaged to trauma '
                f'{self.env.now:.3f}')
        self.event_log.append(
            {'patient': patient.identifier,
             'pathway': 'Trauma',
             'event_type': 'resource_use',
             'event': 'triage_begins',
             'time': self.env.now,
             'resource_id': triage_resource.id_attribute
            }
        )

        # sample triage duration.
        patient.triage_duration = self.triage_dist.sample()
        yield self.env.timeout(patient.triage_duration)

        trace(f'triage {patient.identifier} complete {self.env.now:.3f}; '
              f'waiting time was {patient.wait_triage:.3f}')
        self.event_log.append(
            {'patient': patient.identifier,
             'pathway': 'Trauma',
             'event_type': 'resource_use_end',
             'event': 'triage_complete',
             'time': self.env.now,
             'resource_id': triage_resource.id_attribute}
        )

        # Resource is no longer in use, so put it back in the store
        self.triage_cubicles.put(triage_resource)
        ###################################################

        # record the time that entered the trauma queue
        start_wait = self.env.now
        self.event_log.append(
            {'patient': patient.identifier,
             'pathway': 'Trauma',
             'event_type': 'queue',
             'event': 'TRAUMA_stabilisation_wait_begins',
             'time': self.env.now}
        )

        ###################################################
        # request trauma room
        trauma_resource = yield self.trauma_stabilisation_bays.get()

        self.event_log.append(
            {'patient': patient.identifier,
                'pathway': 'Trauma',
                'event_type': 'resource_use',
                'event': 'TRAUMA_stabilisation_begins',
                'time': self.env.now,
                'resource_id': trauma_resource.id_attribute
                }
        )

        # record the waiting time for trauma
        patient.wait_trauma = self.env.now - start_wait

        # sample stablisation duration.
        patient.trauma_duration = self.trauma_dist.sample()
        yield self.env.timeout(patient.trauma_duration)

        trace(f'stabilisation of patient {patient.identifier} at '
              f'{self.env.now:.3f}')
        self.event_log.append(
            {'patient': patient.identifier,
             'pathway': 'Trauma',
             'event_type': 'resource_use_end',
             'event': 'TRAUMA_stabilisation_complete',
             'time': self.env.now,
             'resource_id': trauma_resource.id_attribute
            }
        )
        # Resource is no longer in use, so put it back in the store
        self.trauma_stabilisation_bays.put(trauma_resource)

        #######################################################

        # record the time that patient entered the treatment queue
        start_wait = self.env.now
        self.event_log.append(
            {'patient': patient.identifier,
             'pathway': 'Trauma',
             'event_type': 'queue',
             'event': 'TRAUMA_treatment_wait_begins',
             'time': self.env.now}
        )

        ########################################################
        # request treatment cubicle
        trauma_treatment_resource = yield self.trauma_treatment_cubicles.get()

        # record the waiting time for trauma
        patient.wait_treat = self.env.now - start_wait
        trace(f'treatment of patient {patient.identifier} at '
                f'{self.env.now:.3f}')
        self.event_log.append(
            {'patient': patient.identifier,
                'pathway': 'Trauma',
                'event_type': 'resource_use',
                'event': 'TRAUMA_treatment_begins',
                'time': self.env.now,
                'resource_id': trauma_treatment_resource.id_attribute
                }
        )

        # sample treatment duration.
        patient.treat_duration = self.trauma_dist.sample()
        yield self.env.timeout(patient.treat_duration)

        trace(f'patient {patient.identifier} treatment complete {self.env.now:.3f}; '
              f'waiting time was {patient.wait_treat:.3f}')
        self.event_log.append(
            {'patient': patient.identifier,
             'pathway': 'Trauma',
             'event_type': 'resource_use_end',
             'event': 'TRAUMA_treatment_complete',
             'time': self.env.now,
             'resource_id': trauma_treatment_resource.id_attribute}
        )
        self.event_log.append(
            {'patient': patient.identifier,
            'pathway': 'Shared',
            'event': 'depart',
            'event_type': 'arrival_departure',
            'time': self.env.now}
        )

        # Resource is no longer in use, so put it back in the store
        self.trauma_treatment_cubicles.put(trauma_treatment_resource)

        #########################################################

        # total time in system
        patient.total_time = self.env.now - patient.arrival


    # This method calculates results over a single run.  Here we just calculate
    # a mean, but in real world models you'd probably want to calculate more.
    def calculate_run_results(self):
        # Take the mean of the queuing times across patients in this run of the
        # model.
        self.mean_q_time_cubicle = self.results_df["Queue Time Cubicle"].mean()

    # The run method starts up the DES entity generators, runs the simulation,
    # and in turns calls anything we need to generate results for the run
    def run(self):
        # Start up our DES entity generators that create new patients.  We've
        # only got one in this model, but we'd need to do this for each one if
        # we had multiple generators.
        self.env.process(self.generator_patient_arrivals())

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

        # Now the simulation run has finished, call the method that calculates
        # run results
        self.calculate_run_results()

        self.event_log = pd.DataFrame(self.event_log)

        self.event_log["run"] = self.run_number

        return {'results': self.results_df, 'event_log': self.event_log}

# Class representing a Trial for our simulation - a batch of simulation runs.
class Trial:
    # The constructor sets up a pandas dataframe that will store the key
    # results from each run against run number, with run number as the index.
    def  __init__(self):
        self.df_trial_results = pd.DataFrame()
        self.df_trial_results["Run Number"] = [0]
        self.df_trial_results["Arrivals"] = [0]
        self.df_trial_results["Mean Queue Time Cubicle"] = [0.0]
        self.df_trial_results.set_index("Run Number", inplace=True)

        self.all_event_logs = []

    # Method to run a trial
    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 (just mean queuing time
        # here) and store it against the run number in the trial results
        # dataframe.
        for run in range(g.number_of_runs):
            random.seed(run)

            my_model = Model(run)
            model_outputs = my_model.run()
            patient_level_results = model_outputs["results"]
            event_log = model_outputs["event_log"]

            self.df_trial_results.loc[run] = [
                len(patient_level_results),
                my_model.mean_q_time_cubicle,
            ]

            # print(event_log)

            self.all_event_logs.append(event_log)

        self.all_event_logs = pd.concat(self.all_event_logs)
In [7]:
my_trial = Trial()

my_trial.run_trial()
In [8]:
my_trial.all_event_logs.head(10)
patient pathway event event_type time resource_id run
0 1 Shared arrival arrival_departure 3.285355 NaN 0
1 1 Non-Trauma triage_wait_begins queue 3.285355 NaN 0
2 1 Non-Trauma triage_begins resource_use 3.285355 1.0 0
3 2 Shared arrival arrival_departure 3.289691 NaN 0
4 2 Non-Trauma triage_wait_begins queue 3.289691 NaN 0
5 2 Non-Trauma triage_begins resource_use 3.289691 2.0 0
6 1 Non-Trauma triage_complete resource_use_end 7.364946 1.0 0
7 1 Non-Trauma MINORS_registration_wait_begins queue 7.364946 NaN 0
8 1 Non-Trauma MINORS_registration_begins resource_use 7.364946 1.0 0
9 2 Non-Trauma triage_complete resource_use_end 9.407274 2.0 0
In [9]:
my_trial.all_event_logs.event_type.value_counts()
event_type
queue                 2100
resource_use          2035
resource_use_end      2021
arrival_departure     1167
attribute_assigned     264
Name: count, dtype: int64
In [10]:
my_trial.all_event_logs.event.value_counts()
event
arrival                             623
triage_wait_begins                  623
triage_begins                       622
triage_complete                     620
depart                              544
MINORS_registration_wait_begins     539
MINORS_registration_begins          514
MINORS_registration_complete        512
MINORS_examination_wait_begins      512
MINORS_examination_begins           475
MINORS_examination_complete         472
requires_treatment                  264
MINORS_treatment_wait_begins        264
MINORS_treatment_begins             264
MINORS_treatment_complete           262
TRAUMA_stabilisation_wait_begins     81
TRAUMA_stabilisation_begins          81
TRAUMA_stabilisation_complete        81
TRAUMA_treatment_wait_begins         81
TRAUMA_treatment_begins              79
TRAUMA_treatment_complete            74
Name: count, dtype: int64
In [11]:
# First, identify all patients who have a 'depart' event
# patients_with_depart = my_trial.all_event_logs[my_trial.all_event_logs['event'].str.contains('depart')]['patient'].unique()

# Then filter the original DataFrame to only include those patients
# filtered_df = my_trial.all_event_logs[my_trial.all_event_logs['patient'].isin(patients_with_depart)]

logs_transformed = my_trial.all_event_logs[~my_trial.all_event_logs['event'].str.contains('wait')].copy()
# logs_transformed = filtered_df[~filtered_df['event'].str.contains('wait')].copy()
logs_transformed = logs_transformed[logs_transformed['event_type'].isin(['resource_use', 'resource_use_end'])].copy()
logs_transformed['event_stage'] = logs_transformed['event_type'].apply(lambda x: 'complete' if 'end' in x else 'start')
logs_transformed['event_name'] = logs_transformed['event'].str.replace('_begins|_complete', '', regex=True)
logs_transformed['resource_id_full'] = logs_transformed.apply(lambda x: f"{x['event_name']}_{x['resource_id']:.0f}", axis=1)
logs_transformed = logs_transformed.sort_values(['run', 'time'], ascending=True)
# logs_transformed["activity_id"] = (
#     logs_transformed.groupby(["run", "patient", "event_name"]).ngroup() + 1
# )

# logs_transformed = logs_transformed.sort_values(["run", "patient", "activity_id", "event_stage"], ascending=[True, True, True, False])

# Sort the data by run, patient, time, and event_name to handle tied start times
logs_transformed = logs_transformed.sort_values(["run", "patient", "time", "event_name"])

# Get the first occurrence of each activity (the start event)
first_occurrences = (
    logs_transformed[logs_transformed["event_stage"] == "start"]
    .drop_duplicates(["run", "patient", "event_name"])
    .copy()
)

# Sort by time within each run to determine the proper sequence
first_occurrences = first_occurrences.sort_values(["run", "time", "event_name"])

# Assign sequential activity_id within each run
first_occurrences["activity_id"] = first_occurrences.groupby("run").cumcount() + 1

# Merge the activity_id back to the main DataFrame
logs_transformed = logs_transformed.merge(
    first_occurrences[["run", "patient", "event_name", "activity_id"]],
    on=["run", "patient", "event_name"],
    how="left"
)

# Sort for final ordering
logs_transformed = logs_transformed.sort_values(
    ["run", "patient", "activity_id", "event_stage"],
    ascending=[True, True, True, False]
)
logs_transformed.head(50)
patient pathway event event_type time resource_id run event_stage event_name resource_id_full activity_id
0 1 Non-Trauma triage_begins resource_use 3.285355 1.0 0 start triage triage_1 1
2 1 Non-Trauma triage_complete resource_use_end 7.364946 1.0 0 complete triage triage_1 1
1 1 Non-Trauma MINORS_registration_begins resource_use 7.364946 1.0 0 start MINORS_registration MINORS_registration_1 3
4 1 Non-Trauma MINORS_registration_complete resource_use_end 15.418481 1.0 0 complete MINORS_registration MINORS_registration_1 3
3 1 Non-Trauma MINORS_examination_begins resource_use 15.418481 1.0 0 start MINORS_examination MINORS_examination_1 5
5 1 Non-Trauma MINORS_examination_complete resource_use_end 31.636252 1.0 0 complete MINORS_examination MINORS_examination_1 5
6 2 Non-Trauma triage_begins resource_use 3.289691 2.0 0 start triage triage_2 2
8 2 Non-Trauma triage_complete resource_use_end 9.407274 2.0 0 complete triage triage_2 2
7 2 Non-Trauma MINORS_registration_begins resource_use 9.407274 2.0 0 start MINORS_registration MINORS_registration_2 4
10 2 Non-Trauma MINORS_registration_complete resource_use_end 17.104670 2.0 0 complete MINORS_registration MINORS_registration_2 4
9 2 Non-Trauma MINORS_examination_begins resource_use 17.104670 2.0 0 start MINORS_examination MINORS_examination_2 6
11 2 Non-Trauma MINORS_examination_complete resource_use_end 32.875857 2.0 0 complete MINORS_examination MINORS_examination_2 6
12 2 Non-Trauma MINORS_treatment_begins resource_use 32.875857 1.0 0 start MINORS_treatment MINORS_treatment_1 7
13 2 Non-Trauma MINORS_treatment_complete resource_use_end 46.278797 1.0 0 complete MINORS_treatment MINORS_treatment_1 7
14 3 Non-Trauma triage_begins resource_use 33.426168 1.0 0 start triage triage_1 8
16 3 Non-Trauma triage_complete resource_use_end 33.545008 1.0 0 complete triage triage_1 8
15 3 Non-Trauma MINORS_registration_begins resource_use 33.545008 1.0 0 start MINORS_registration MINORS_registration_1 9
18 3 Non-Trauma MINORS_registration_complete resource_use_end 42.359504 1.0 0 complete MINORS_registration MINORS_registration_1 9
17 3 Non-Trauma MINORS_examination_begins resource_use 42.359504 3.0 0 start MINORS_examination MINORS_examination_3 12
19 3 Non-Trauma MINORS_examination_complete resource_use_end 59.468749 3.0 0 complete MINORS_examination MINORS_examination_3 12
20 3 Non-Trauma MINORS_treatment_begins resource_use 59.468749 2.0 0 start MINORS_treatment MINORS_treatment_2 16
21 3 Non-Trauma MINORS_treatment_complete resource_use_end 72.510229 2.0 0 complete MINORS_treatment MINORS_treatment_2 16
22 4 Non-Trauma triage_begins resource_use 37.900548 2.0 0 start triage triage_2 10
24 4 Non-Trauma triage_complete resource_use_end 37.914164 2.0 0 complete triage triage_2 10
23 4 Non-Trauma MINORS_registration_begins resource_use 37.914164 2.0 0 start MINORS_registration MINORS_registration_2 11
26 4 Non-Trauma MINORS_registration_complete resource_use_end 45.938325 2.0 0 complete MINORS_registration MINORS_registration_2 11
25 4 Non-Trauma MINORS_examination_begins resource_use 45.938325 1.0 0 start MINORS_examination MINORS_examination_1 13
27 4 Non-Trauma MINORS_examination_complete resource_use_end 62.120017 1.0 0 complete MINORS_examination MINORS_examination_1 13
28 4 Non-Trauma MINORS_treatment_begins resource_use 62.120017 3.0 0 start MINORS_treatment MINORS_treatment_3 17
29 4 Non-Trauma MINORS_treatment_complete resource_use_end 76.274743 3.0 0 complete MINORS_treatment MINORS_treatment_3 17
30 5 Non-Trauma triage_begins resource_use 51.770459 1.0 0 start triage triage_1 14
32 5 Non-Trauma triage_complete resource_use_end 55.072516 1.0 0 complete triage triage_1 14
31 5 Non-Trauma MINORS_registration_begins resource_use 55.072516 1.0 0 start MINORS_registration MINORS_registration_1 15
34 5 Non-Trauma MINORS_registration_complete resource_use_end 62.243834 1.0 0 complete MINORS_registration MINORS_registration_1 15
33 5 Non-Trauma MINORS_examination_begins resource_use 62.243834 2.0 0 start MINORS_examination MINORS_examination_2 18
35 5 Non-Trauma MINORS_examination_complete resource_use_end 77.316027 2.0 0 complete MINORS_examination MINORS_examination_2 18
36 6 Trauma triage_begins resource_use 113.767200 2.0 0 start triage triage_2 19
38 6 Trauma triage_complete resource_use_end 123.546842 2.0 0 complete triage triage_2 19
37 6 Trauma TRAUMA_stabilisation_begins resource_use 123.546842 1.0 0 start TRAUMA_stabilisation TRAUMA_stabilisation_1 21
39 6 Trauma TRAUMA_stabilisation_complete resource_use_end 184.740714 1.0 0 complete TRAUMA_stabilisation TRAUMA_stabilisation_1 21
40 6 Trauma TRAUMA_treatment_begins resource_use 184.740714 1.0 0 start TRAUMA_treatment TRAUMA_treatment_1 37
41 6 Trauma TRAUMA_treatment_complete resource_use_end 186.523313 1.0 0 complete TRAUMA_treatment TRAUMA_treatment_1 37
42 7 Non-Trauma triage_begins resource_use 123.044756 1.0 0 start triage triage_1 20
44 7 Non-Trauma triage_complete resource_use_end 127.086253 1.0 0 complete triage triage_1 20
43 7 Non-Trauma MINORS_registration_begins resource_use 127.086253 2.0 0 start MINORS_registration MINORS_registration_2 23
46 7 Non-Trauma MINORS_registration_complete resource_use_end 135.479993 2.0 0 complete MINORS_registration MINORS_registration_2 23
45 7 Non-Trauma MINORS_examination_begins resource_use 135.479993 3.0 0 start MINORS_examination MINORS_examination_3 27
47 7 Non-Trauma MINORS_examination_complete resource_use_end 152.106294 3.0 0 complete MINORS_examination MINORS_examination_3 27
48 8 Non-Trauma triage_begins resource_use 124.735469 2.0 0 start triage triage_2 22
50 8 Non-Trauma triage_complete resource_use_end 129.267277 2.0 0 complete triage triage_2 22
In [12]:
logs_transformed[(logs_transformed["run"]==1) & (logs_transformed["activity_id"]==26)]
patient pathway event event_type time resource_id run event_stage event_name resource_id_full activity_id
In [13]:
logs_transformed[logs_transformed["activity_id"]==26].sort_values('run').head(30)
patient pathway event event_type time resource_id run event_stage event_name resource_id_full activity_id
60 10 Trauma triage_begins resource_use 129.468902 2.0 0 start triage triage_2 26
62 10 Trauma triage_complete resource_use_end 165.815421 2.0 0 complete triage triage_2 26
In [14]:
logs_transformed.sort_values('activity_id').head(20)
patient pathway event event_type time resource_id run event_stage event_name resource_id_full activity_id
0 1 Non-Trauma triage_begins resource_use 3.285355 1.0 0 start triage triage_1 1
2 1 Non-Trauma triage_complete resource_use_end 7.364946 1.0 0 complete triage triage_1 1
6 2 Non-Trauma triage_begins resource_use 3.289691 2.0 0 start triage triage_2 2
8 2 Non-Trauma triage_complete resource_use_end 9.407274 2.0 0 complete triage triage_2 2
1 1 Non-Trauma MINORS_registration_begins resource_use 7.364946 1.0 0 start MINORS_registration MINORS_registration_1 3
4 1 Non-Trauma MINORS_registration_complete resource_use_end 15.418481 1.0 0 complete MINORS_registration MINORS_registration_1 3
7 2 Non-Trauma MINORS_registration_begins resource_use 9.407274 2.0 0 start MINORS_registration MINORS_registration_2 4
10 2 Non-Trauma MINORS_registration_complete resource_use_end 17.104670 2.0 0 complete MINORS_registration MINORS_registration_2 4
3 1 Non-Trauma MINORS_examination_begins resource_use 15.418481 1.0 0 start MINORS_examination MINORS_examination_1 5
5 1 Non-Trauma MINORS_examination_complete resource_use_end 31.636252 1.0 0 complete MINORS_examination MINORS_examination_1 5
11 2 Non-Trauma MINORS_examination_complete resource_use_end 32.875857 2.0 0 complete MINORS_examination MINORS_examination_2 6
9 2 Non-Trauma MINORS_examination_begins resource_use 17.104670 2.0 0 start MINORS_examination MINORS_examination_2 6
12 2 Non-Trauma MINORS_treatment_begins resource_use 32.875857 1.0 0 start MINORS_treatment MINORS_treatment_1 7
13 2 Non-Trauma MINORS_treatment_complete resource_use_end 46.278797 1.0 0 complete MINORS_treatment MINORS_treatment_1 7
14 3 Non-Trauma triage_begins resource_use 33.426168 1.0 0 start triage triage_1 8
16 3 Non-Trauma triage_complete resource_use_end 33.545008 1.0 0 complete triage triage_1 8
15 3 Non-Trauma MINORS_registration_begins resource_use 33.545008 1.0 0 start MINORS_registration MINORS_registration_1 9
18 3 Non-Trauma MINORS_registration_complete resource_use_end 42.359504 1.0 0 complete MINORS_registration MINORS_registration_1 9
24 4 Non-Trauma triage_complete resource_use_end 37.914164 2.0 0 complete triage triage_2 10
22 4 Non-Trauma triage_begins resource_use 37.900548 2.0 0 start triage triage_2 10
In [15]:
logs_transformed[["event_name", "event_stage", "event_type"]].value_counts()
event_name            event_stage  event_type      
triage                start        resource_use        622
                      complete     resource_use_end    620
MINORS_registration   start        resource_use        514
                      complete     resource_use_end    512
MINORS_examination    start        resource_use        475
                      complete     resource_use_end    472
MINORS_treatment      start        resource_use        264
                      complete     resource_use_end    262
TRAUMA_stabilisation  complete     resource_use_end     81
                      start        resource_use         81
TRAUMA_treatment      start        resource_use         79
                      complete     resource_use_end     74
Name: count, dtype: int64
In [16]:
logs_transformed.event.value_counts()
event
triage_begins                    622
triage_complete                  620
MINORS_registration_begins       514
MINORS_registration_complete     512
MINORS_examination_begins        475
MINORS_examination_complete      472
MINORS_treatment_begins          264
MINORS_treatment_complete        262
TRAUMA_stabilisation_begins       81
TRAUMA_stabilisation_complete     81
TRAUMA_treatment_begins           79
TRAUMA_treatment_complete         74
Name: count, dtype: int64

For ease, now let’s save these results as a file that we can load into R.

We could use a csv for easy interoperability. Alternatively, we could use something like Feather or Parquet, which are usable by both R and Python while retaining data types.

For ease of use and long-term readbility, we will use csv in this case.

In [17]:
logs_transformed.to_csv('simulation_logs_for_bupar.csv', index=False)