from examples.example_2_branching_multistep.ex_2_model_classes import Trial, g
import pandas as pd
import os
Vidigi vs BupaR
Switch this page to the light mode using the toggle at the top of the navigation sidebar on the left of this page.
I have not yet worked out how to change the colour of the text on the resulting process maps, so using light mode is currently the only way to see it.
The title of this section is perhaps misleading! As the author of the package, I think the visuals produced by the two packages occupy slightly different niches, and the use of both can benefit your project.
As an additional bonus, the process of creating the logs you require for a vidigi project give you the perfect dataset for your bupaR visuals too!
bupaR outputs could form part of a verification and validation strategy. They can also perform part of your communications strategy, helping to provide a talking point for meetings with stakeholders in much the same way as a screenshot of your Simul8 or Anylogic model would. In the absence of a graphical interface for building a model, the bupar outputs can help you - and your stakeholders - to ensure that linkages between different model steps are sensible and appropriate.
We will begin in Python, working to add a couple of columns to our vidigi event log to prepare it for use in bupaR.
Now, it’s time to move to R (as bupaR and the bupaverse is only implemented in R).
pm4py exists as a process analytics package for Python, but the visuals of bupaR are of a high quality.
Importing the required R functions and our data
library(dplyr)
Attaching package: 'dplyr'
The following objects are masked from 'package:stats':
filter, lag
The following objects are masked from 'package:base':
intersect, setdiff, setequal, union
library(readr)
library(bupaverse)
Warning: package 'bupaverse' was built under R version 4.3.3
.______ __ __ .______ ___ ____ ____ _______ .______ _______. _______
| _ \ | | | | | _ \ / \ \ \ / / | ____|| _ \ / || ____|
| |_) | | | | | | |_) | / ^ \ \ \/ / | |__ | |_) | | (----`| |__
| _ < | | | | | ___/ / /_\ \ \ / | __| | / \ \ | __|
| |_) | | `--' | | | / _____ \ \ / | |____ | |\ \----.----) | | |____
|______/ \______/ | _| /__/ \__\ \__/ |_______|| _| `._____|_______/ |_______|
── Attaching packages ─────────────────────────────────────── bupaverse 0.1.0 ──
✔ bupaR 0.5.4 ✔ processcheckR 0.1.4
✔ edeaR 0.9.4 ✔ processmapR 0.5.6
✔ eventdataR 0.3.1
Warning: package 'bupaR' was built under R version 4.3.3
Warning: package 'processcheckR' was built under R version 4.3.3
── Conflicts ────────────────────────────────────────── bupaverse_conflicts() ──
✖ processcheckR::contains() masks dplyr::contains()
✖ bupaR::filter() masks dplyr::filter(), stats::filter()
✖ processmapR::frequency() masks stats::frequency()
✖ edeaR::setdiff() masks dplyr::setdiff(), base::setdiff()
✖ bupaR::timestamp() masks utils::timestamp()
✖ processcheckR::xor() masks base::xor()
library(processanimateR)
Warning: package 'processanimateR' was built under R version 4.3.3
library(lubridate)
Attaching package: 'lubridate'
The following objects are masked from 'package:base':
date, intersect, setdiff, union
library(DT)
Warning: package 'DT' was built under R version 4.3.3
library(psmineR)
Warning: package 'psmineR' was built under R version 4.3.3
<- readr::read_csv("simulation_logs_for_bupar.csv") data
Rows: 4056 Columns: 11
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (6): pathway, event, event_type, event_stage, event_name, resource_id_full
dbl (5): patient, time, resource_id, run, activity_id
ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
%>% head() data
# A tibble: 6 × 11
patient pathway event event_type time resource_id run event_stage
<dbl> <chr> <chr> <chr> <dbl> <dbl> <dbl> <chr>
1 1 Non-Trauma triage_begi… resource_… 3.29 1 0 start
2 1 Non-Trauma triage_comp… resource_… 7.36 1 0 complete
3 1 Non-Trauma MINORS_regi… resource_… 7.36 1 0 start
4 1 Non-Trauma MINORS_regi… resource_… 15.4 1 0 complete
5 1 Non-Trauma MINORS_exam… resource_… 15.4 1 0 start
6 1 Non-Trauma MINORS_exam… resource_… 31.6 1 0 complete
# ℹ 3 more variables: event_name <chr>, resource_id_full <chr>,
# activity_id <dbl>
Ensuring our data has the required time columns and is only for a single run
<- data %>%
data_processed ::filter(run == 0) %>%
dplyr::rename(minutes_after_origin=time) %>%
dplyr# We provide a theoretical date to act as a starting point - the date does not have to be
# a true representation of the actual simulation, though you may wish it to be if there
# are date elements in your simulation (e.g. within-week or within-year seasonality)
::mutate(time = as.POSIXct("2024-01-01 00:00:00", tz = "UTC") + lubridate::dminutes(minutes_after_origin)) %>%
dplyr::convert_timestamps("time", ymd_hms) %>%
bupaR::mutate(patient = as.factor(patient))
dplyr
::datatable(data_processed) DT
Converting to the activitylog format
<- data_processed %>%
activity_log ::eventlog(
bupaRcase_id = "patient",
activity_id = "event_name",
activity_instance_id = "activity_id",
lifecycle_id = "event_stage",
timestamp = "time",
resource_id = "resource_id_full"
)
## !!!! Note that the bupaR documentation recommmends using the
## to_activitylog() function at the end of this set of steps.
## This caused significant errors in testing of this code, so
## I would not recommend following this recommendation, and instead
## you can mimic the above
activity_log
# Log of 4056 events consisting of:
6 traces
622 cases
2035 instances of 6 activities
20 resources
Events occurred from 2024-01-01 00:03:17 until 2024-01-03 01:58:40
# Variables were mapped as follows:
Case identifier: patient
Activity identifier: event_name
Resource identifier: resource_id_full
Activity instance identifier: activity_id
Timestamp: time
Lifecycle transition: event_stage
# A tibble: 4,056 × 13
patient pathway event event_type minutes_after_origin resource_id run
<fct> <chr> <chr> <chr> <dbl> <dbl> <dbl>
1 1 Non-Trauma triage_… resource_… 3.29 1 0
2 1 Non-Trauma triage_… resource_… 7.36 1 0
3 1 Non-Trauma MINORS_… resource_… 7.36 1 0
4 1 Non-Trauma MINORS_… resource_… 15.4 1 0
5 1 Non-Trauma MINORS_… resource_… 15.4 1 0
6 1 Non-Trauma MINORS_… resource_… 31.6 1 0
7 2 Non-Trauma triage_… resource_… 3.29 2 0
8 2 Non-Trauma triage_… resource_… 9.41 2 0
9 2 Non-Trauma MINORS_… resource_… 9.41 2 0
10 2 Non-Trauma MINORS_… resource_… 17.1 2 0
# ℹ 4,046 more rows
# ℹ 6 more variables: event_stage <chr>, event_name <chr>,
# resource_id_full <chr>, activity_id <dbl>, time <dttm>, .order <int>
Creating outputs
Process Maps
Absolute frequencies
%>%
activity_log process_map(frequency("absolute"))
%>%
activity_log process_map(frequency("absolute-case"))
Relative frequencies
%>%
activity_log process_map(frequency("relative"))
%>%
activity_log process_map(frequency("relative-case"),
render_options = list(edge_label_color = "white"))
%>%
activity_log process_map(frequency("relative-consequent"),
render_options = list(edge_label_color = "white"))
Performance Maps
Mean Waits
%>%
activity_log process_map(performance())
Max Waits
%>%
activity_log process_map(performance(FUN = max))
Warning: There was 1 warning in `summarize()`.
ℹ In argument: `label = do.call(...)`.
ℹ In group 9: `ACTIVITY_CLASSIFIER_ = NA` and `from_id = NA`.
Caused by warning in `type()`:
! no non-missing arguments to max; returning -Inf
Warning: There were 2 warnings in `summarize()`.
The first warning was:
ℹ In argument: `value = do.call(...)`.
ℹ In group 1: `ACTIVITY_CLASSIFIER_ = "ARTIFICIAL_END"`, `next_act = NA`,
`from_id = 1`, `to_id = NA`.
Caused by warning in `type()`:
! no non-missing arguments to max; returning -Inf
ℹ Run `dplyr::last_dplyr_warnings()` to see the 1 remaining warning.
90th percentile
<- function(x, ...) {
p90 quantile(x, probs = 0.9, ...)
}
%>%
activity_log process_map(performance(FUN = p90))
Analytics
Take a look at this page in the bupaR docs details of each of these plots.
Idle Time
%>%
activity_log idle_time("resource", units = "mins")
# A tibble: 20 × 2
resource_id_full idle_time
<chr> <drtn>
1 MINORS_treatment_1 2101.6612 mins
2 MINORS_treatment_2 2065.5869 mins
3 MINORS_treatment_3 2043.3483 mins
4 MINORS_treatment_4 1945.5681 mins
5 TRAUMA_treatment_1 1415.3914 mins
6 TRAUMA_treatment_2 1275.9829 mins
7 TRAUMA_treatment_3 1208.0795 mins
8 TRAUMA_treatment_5 1098.2404 mins
9 triage_2 1043.9654 mins
10 triage_1 999.9198 mins
11 TRAUMA_treatment_4 959.1452 mins
12 MINORS_registration_2 951.0949 mins
13 MINORS_registration_1 943.0261 mins
14 TRAUMA_stabilisation_1 881.6663 mins
15 TRAUMA_stabilisation_2 714.7071 mins
16 TRAUMA_stabilisation_3 691.9842 mins
17 TRAUMA_stabilisation_4 564.1900 mins
18 MINORS_examination_2 469.1167 mins
19 MINORS_examination_1 463.9669 mins
20 MINORS_examination_3 441.1148 mins
%>%
activity_log idle_time("resource", units = "mins") %>%
plot()
Processing Time
%>%
activity_log processing_time("log", units = "mins") %>%
plot()
%>%
activity_log processing_time("case", units = "mins") %>%
plot()
%>%
activity_log processing_time("activity", units = "mins") %>%
plot()
%>%
activity_log processing_time("resource-activity", units = "mins") %>%
plot()
Throughput time
%>%
activity_log throughput_time("log", units = "mins") %>%
plot()
Activity Presence
%>%
activity_log activity_presence() %>%
plot()
Resource visualisations
Handover-of-work network
%>%
activity_log resource_map()
Resource precedence matrix
%>%
activity_log resource_matrix() %>%
plot()
Process matrix
%>%
activity_log process_matrix(frequency("absolute")) %>%
plot()
Trace Explorer
This plot helps us to unerstand how often different combinations of activities occur, and whether there are any unexpected paths in our data.
%>%
activity_log trace_explorer(n_traces = 10)
Warning: Fewer traces (6) found than specified `n_traces` (10).
Animated process map
%>%
activity_log animate_process()
Let’s compare directly with our vidigi output.
The key difference between what is produced via bupaverse’s animate_process
and what can be created via vidigi is the ability of vidigi to more clearly show the scale of queues, and the number of resources available at any given point.
Vidigi can also more clearly highlight the impact of priority on resources through the use of distinct icons, though this is not demonstrated in this example.
from examples.example_2_branching_multistep.ex_2_model_classes import Trial, g
from vidigi.animation import animate_activity_log
import pandas as pd
import plotly.io as pio
#pio.renderers.default = "notebook"
= "iframe"
pio.renderers.default
= 3000
g.sim_duration = 3
g.number_of_runs
= Trial()
my_trial
my_trial.run_trial()
= pd.DataFrame([
event_position_df # {'event': 'arrival', 'x': 10, 'y': 250, 'label': "Arrival" },
# Triage - minor and trauma
'event': 'triage_wait_begins',
{'x': 160, 'y': 375, 'label': "Waiting for<br>Triage" },
'event': 'triage_begins',
{'x': 160, 'y': 315, 'resource':'n_triage', 'label': "Being Triaged" },
# Minors (non-trauma) pathway
'event': 'MINORS_registration_wait_begins',
{'x': 300, 'y': 145, 'label': "Waiting for<br>Registration" },
'event': 'MINORS_registration_begins',
{'x': 300, 'y': 85, 'resource':'n_reg', 'label':'Being<br>Registered' },
'event': 'MINORS_examination_wait_begins',
{'x': 465, 'y': 145, 'label': "Waiting for<br>Examination" },
'event': 'MINORS_examination_begins',
{'x': 465, 'y': 85, 'resource':'n_exam', 'label': "Being<br>Examined" },
'event': 'MINORS_treatment_wait_begins',
{'x': 630, 'y': 145, 'label': "Waiting for<br>Treatment" },
'event': 'MINORS_treatment_begins',
{'x': 630, 'y': 85, 'resource':'n_cubicles_non_trauma_treat', 'label': "Being<br>Treated" },
# Trauma pathway
'event': 'TRAUMA_stabilisation_wait_begins',
{'x': 300, 'y': 560, 'label': "Waiting for<br>Stabilisation" },
'event': 'TRAUMA_stabilisation_begins',
{'x': 300, 'y': 490, 'resource':'n_trauma', 'label': "Being<br>Stabilised" },
'event': 'TRAUMA_treatment_wait_begins',
{'x': 630, 'y': 560, 'label': "Waiting for<br>Treatment" },
'event': 'TRAUMA_treatment_begins',
{'x': 630, 'y': 490, 'resource':'n_cubicles_trauma_treat', 'label': "Being<br>Treated" },
'event': 'exit',
{'x': 670, 'y': 330, 'label': "Exit"}
])
animate_activity_log(=my_trial.all_event_logs[my_trial.all_event_logs['run']==0],
event_log=event_position_df,
event_position_df=g(),
scenario=False,
debug_mode=False,
setup_mode=5,
every_x_time_units=True,
include_play_button=10,
gap_between_entities=20,
gap_between_rows=900,
plotly_height=1600,
plotly_width=700,
override_x_max=675,
override_y_max=20,
icon_and_text_size=10,
wrap_queues_at=50,
step_snapshot_max=3000,
limit_duration="dhm",
time_display_units=False,
display_stage_labels="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_2_branching_multistep/Full%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
add_background_image )
Other chart types
Dotted chart
We can see the impact of the pattern of daily arrivals across the course of the model, with the waits clearing out overnight when arrivals slow down.
%>%
activity_log dotted_chart(x = "absolute")
%>%
activity_log dotted_chart(x = "relative", sort="start")
Breaking down dotted charts by route
Minors
%>%
activity_log filter(event_name %in% c('MINORS_examination', 'MINORS_registration', 'MINORS_treatment', 'triage')) %>%
dotted_chart(x = "absolute")
%>%
activity_log filter(event_name %in% c('MINORS_examination', 'MINORS_registration', 'MINORS_treatment', 'triage')) %>%
dotted_chart(x = "relative", sort="start")
Trauma
%>%
activity_log filter(event_name %in% c('TRAUMA_stabilisation', 'TRAUMA_treatment', 'triage')) %>%
dotted_chart(x = "absolute")
%>%
activity_log filter(event_name %in% c('TRAUMA_stabilisation', 'TRAUMA_treatment', 'triage')) %>%
dotted_chart(x = "relative", sort="start")
Conclusion
vidigi and bupaR are complementary packages to use when visualising, verifying and validating your simulation models - or working with real-world process data.