In this chapter and beyond, we will be using the term ‘event logging’ to describe the process of generating a step-by-step log of what happens to each entity as they pass through our system. The resulting file will be an ‘event log’.
But why bother with event logging? In previous chapters, we have managed to record a range of useful metrics from our simulation, and used these to display the performance of our model with different sets of parameters. In the debugging chapters, we have explored how to track down issues in our simulation with a combination of print statements and the logging module. So what else is there left to do?
Event logging can be a way to tackle both of the above in a robust, efficient and reusable way - while also opening up the use of a wide range of reusable boilerplate code for visualising, animating and formally testing our simulation outputs.
Tip
While this chapter - and the rest of the book - will make use of a particular structure for our event log, this isn’t the only way you can set up an event log. However, this structure has been tested to work well across a range of different models, and interfaces well with the vidigi package for animated simulation outputs.
30.1 A sample event log
First, it may be helpful to see what an event log looks like.
A sample log is displayed below:
import pandas as pdevent_log = pd.read_csv("resources/sample_event_log.csv")event_log
entity_id
pathway
event_type
event
time
resource_id
run
0
1
Simplest
arrival_departure
arrival
0.000000
NaN
0
1
1
Simplest
queue
treatment_wait_begins
0.000000
NaN
0
2
1
Simplest
resource_use
treatment_begins
0.000000
1.0
0
3
2
Simplest
arrival_departure
arrival
3.399660
NaN
0
4
2
Simplest
queue
treatment_wait_begins
3.399660
NaN
0
5
2
Simplest
resource_use
treatment_begins
3.399660
2.0
0
6
3
Simplest
arrival_departure
arrival
8.497645
NaN
0
7
3
Simplest
queue
treatment_wait_begins
8.497645
NaN
0
8
3
Simplest
resource_use
treatment_begins
8.497645
3.0
0
9
4
Simplest
arrival_departure
arrival
8.596678
NaN
0
10
4
Simplest
queue
treatment_wait_begins
8.596678
NaN
0
11
4
Simplest
resource_use
treatment_begins
8.596678
4.0
0
12
5
Simplest
arrival_departure
arrival
8.608025
NaN
0
13
5
Simplest
queue
treatment_wait_begins
8.608025
NaN
0
14
6
Simplest
arrival_departure
arrival
11.359739
NaN
0
15
6
Simplest
queue
treatment_wait_begins
11.359739
NaN
0
16
7
Simplest
arrival_departure
arrival
19.509442
NaN
0
17
7
Simplest
queue
treatment_wait_begins
19.509442
NaN
0
18
8
Simplest
arrival_departure
arrival
22.877356
NaN
0
19
8
Simplest
queue
treatment_wait_begins
22.877356
NaN
0
20
9
Simplest
arrival_departure
arrival
26.653863
NaN
0
21
9
Simplest
queue
treatment_wait_begins
26.653863
NaN
0
22
1
Simplest
resource_use_end
treatment_complete
40.317385
1.0
0
23
1
Simplest
arrival_departure
depart
40.317385
NaN
0
24
5
Simplest
resource_use
treatment_begins
40.317385
1.0
0
25
10
Simplest
arrival_departure
arrival
40.737793
NaN
0
26
10
Simplest
queue
treatment_wait_begins
40.737793
NaN
0
27
2
Simplest
resource_use_end
treatment_complete
42.443230
2.0
0
28
2
Simplest
arrival_departure
depart
42.443230
NaN
0
29
6
Simplest
resource_use
treatment_begins
42.443230
2.0
0
30
4
Simplest
resource_use_end
treatment_complete
48.809628
4.0
0
31
4
Simplest
arrival_departure
depart
48.809628
NaN
0
32
7
Simplest
resource_use
treatment_begins
48.809628
4.0
0
33
3
Simplest
resource_use_end
treatment_complete
51.483457
3.0
0
34
3
Simplest
arrival_departure
depart
51.483457
NaN
0
35
8
Simplest
resource_use
treatment_begins
51.483457
3.0
0
36
11
Simplest
arrival_departure
arrival
71.026558
NaN
0
37
11
Simplest
queue
treatment_wait_begins
71.026558
NaN
0
38
5
Simplest
resource_use_end
treatment_complete
77.447488
1.0
0
39
5
Simplest
arrival_departure
depart
77.447488
NaN
0
40
9
Simplest
resource_use
treatment_begins
77.447488
1.0
0
41
6
Simplest
resource_use_end
treatment_complete
83.962251
2.0
0
42
6
Simplest
arrival_departure
depart
83.962251
NaN
0
43
10
Simplest
resource_use
treatment_begins
83.962251
2.0
0
44
12
Simplest
arrival_departure
arrival
87.458700
NaN
0
45
12
Simplest
queue
treatment_wait_begins
87.458700
NaN
0
46
13
Simplest
arrival_departure
arrival
87.465138
NaN
0
47
13
Simplest
queue
treatment_wait_begins
87.465138
NaN
0
48
7
Simplest
resource_use_end
treatment_complete
95.498040
4.0
0
49
7
Simplest
arrival_departure
depart
95.498040
NaN
0
Let’s break down the key components.
30.1.1 Mandatory Columns
We have several mandatory columns:
entity_id
event_type
event
time
entity_id: a unique identifider to allow us to follow a given entity through their journey
event_type: this column is used to distinguish between three key kinds of events:
arrival_departure: an entity first entering the system, or the entity leaving the system
queue: an entity beginning to queue for a resource
this can also be used to just generally record the movement of someone through points of a system that don’t necessarily have a traditional ‘queue’
resource_use: this relates to anything where someone starts or ends their time with a resource in the system
event: this column further breaks down what is happening during each event type, such as what stage of the system people are waiting to interact with
time: this can be an absolute timestamp in the form of a datetime (e.g. 2027-01-01 23:01:47), or a relative timestamp in time units from the start of the simulation.
30.1.2 Recommended Columns
In addition, the following columns are highly recommended to include:
run: this separates out results across multiple simulations, allowing for easy grouping of results by run
30.1.3 Optional Columns
There are a few more recommended but optional columns:
resource_id
pathway
resource_id: While a traditional simpy resource does not have a concept of a resource ID, there are various ways we can tackle adding one into our system. Let’s assume we have 5 nurses - it’s very helpful to know which of these 5 nurses are in use at any given time for visualisation and auditing purposes. A discussion of approaches to this will be found in a later chapter (coming soon), or you can take a look at the populate_store function from vidigi for a discussion on one approach to this: click here
30.2 Exploring our log
Let’s explore what we can find out even from this small sample of logs.
30.2.1 Tracking the journey of a single entity/patient
We can easily filter down by entity ID to see how the journey of an individual looks
event_log[event_log["entity_id"] ==1]
entity_id
pathway
event_type
event
time
resource_id
run
0
1
Simplest
arrival_departure
arrival
0.000000
NaN
0
1
1
Simplest
queue
treatment_wait_begins
0.000000
NaN
0
2
1
Simplest
resource_use
treatment_begins
0.000000
1.0
0
22
1
Simplest
resource_use_end
treatment_complete
40.317385
1.0
0
23
1
Simplest
arrival_departure
depart
40.317385
NaN
0
We can see that this person arrived and was seen immediately.
30.2.2 Calculating waits
In this event log, patients who wait for treatment (an event of ‘treatment_wait_begins’) will have the next event of ‘treatment_wait’. We’ll start by just pulling out those instances.
Finally, let’s work out the average wait for only those patients who were seen by someone before our simulation finished. We first need to calculate the difference between the two stamps, which in this simulation, represents a number of minutes
This would be a lot more powerful if we weren’t just working with a small sample of 50 rows of an event log. Let’s instead load in the full event log from this model, which runs for a lot longer and includes multiple runs. All our existing code can be reused.
event_log = pd.read_csv("resources/sample_event_log_10_day_10_run.csv")# We'll limit this to the first 24 hours of our clinicevent_log = event_log[event_log["time"] <=60*24]event_log.sample(10)
entity_id
pathway
event_type
event
time
resource_id
run
30944
135
Simplest
resource_use_end
treatment_complete
1364.238468
4.0
3
10340
114
Simplest
arrival_departure
arrival
520.944947
NaN
1
20788
219
Simplest
arrival_departure
arrival
1159.087158
NaN
2
10497
75
Simplest
resource_use
treatment_begins
739.629092
2.0
1
61025
251
Simplest
queue
treatment_wait_begins
1245.354799
NaN
6
40725
174
Simplest
queue
treatment_wait_begins
832.687462
NaN
4
10649
93
Simplest
resource_use_end
treatment_complete
957.492104
1.0
1
60712
161
Simplest
arrival_departure
arrival
806.687920
NaN
6
40438
91
Simplest
arrival_departure
arrival
440.216636
NaN
4
70631
141
Simplest
queue
treatment_wait_begins
687.885873
NaN
7
Let’s recalculate our waits. In fact - let’s write a function that could be used to make a wait table for any pair of events!
We’ll also make sure it takes account of the ‘run’ column.
# Create a suitable class to pass in the resource numbers to the animation functionclass model_params():def__init__(self):self.n_cubicles =4# In this case, I know that there were four resources available.params = model_params()print(f"Number of nurses: {params.n_cubicles}")
Number of nurses: 4
from vidigi.animation import animate_activity_loganimate_activity_log(# We need to ensure we only pass in a single run of the model# We also need to ensure that the column 'entity_id' is renamed to 'patient' to match# vidigi's expectations - though this will be made more flexible in a future version# of the package event_log=event_log[event_log['run']==1], event_position_df=event_position_df, scenario=params, debug_mode=True, setup_mode=False, every_x_time_units=5, include_play_button=True, entity_icon_size=20, text_size=20, gap_between_entities=6, gap_between_queue_rows=25, plotly_height=700, frame_duration=200, plotly_width=1200, override_x_max=300, override_y_max=500, limit_duration=event_log["time"].max().astype('int'), wrap_queues_at=25, step_snapshot_max=125, time_display_units="dhm", display_stage_labels=False, add_background_image="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_1_simplest_case/Simplest%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png", )
Animation function called at 11:44:12
C:\simviz\vidigi\vidigi\animation.py:1169: UserWarning:
`limit_duration` was provided as int32 (1439); rounding to nearest integer (1439).
Iteration through time-unit-by-time-unit logs complete 11:44:13
Snapshot df concatenation complete at 11:44:14
Reshaped animation dataframe finished construction at 11:44:14
Placement dataframe finished construction at 11:44:14
Output animation generation complete at 11:44:16
Total Time Elapsed: 4.17 seconds