import math
import pandas as pd
import plotly.io as pio
from model_classes import Scenario, generate_seed_vector
from simulation_execution_functions import single_run
from vidigi.animation import generate_animation
from vidigi.prep import generate_animation_df, reshape_for_animations
pio.renderers.default = "iframe"Mental Health - Appointment Booking Model
This example has not yet been updated to reflect all of the new features and recommendations from vidigi 2.0.0, which have further simplified the process of adding vidigi to your model and accessing and modifying your animation.
For now, all of the code below will still work, but check out the getting started page for a full guide to the recommended way to use vidigi 2.0.0.
Sometimes you may want to display models without having a clear concept of a ‘resource’ that you track.
In this case, vidigi can cope perfectly fine with just having ‘queue’ steps implemented.
In this instance, this model looks at a simple mental health pathway. Here, we are only concerned with the booking of an initial appointment. Each clinician has a certain number of slots available per day, but the number of available slots varies significantly across the course of the week for each individual clinician. Changing the available resources over the course of a simulation is not currently supported - though you could demonstrate resources being ‘unavailable’ by blocking them for a certain duration with an icon indicating unavailability. In this case, where the focus is on the buildup of queues,
This example also shows the automatic generation of the appropriate number of event positioning entries, which would support the use of the animation in a front-end where the number of clinicains could be varied.
By default, the model uses an appointment book with some slots held back for high-priority patients. Each patient in the default scenario can only go to their ‘home’/most local clinic.
However, it is possible to switch to other scenarios - a ‘pooling’ system where patients can choose between one of several linked clinics in their local area (with the assumption that they will choose the clinic of the group with the soonest available appointment) - the pooling system described above, but with no slots held back for high-priority patients (i.e. no ‘carve-out’)
shifts = pd.read_csv("data/shifts.csv")
# if scenario_choice == "As-is" or scenario_choice == "With Pooling":
# prop_carve_out = [0.0, 0.9, 0.15, 0.01]
prop_carve_out = 0.15
# depending on settings and CPU this model takes around 15-20 seconds to run
RESULTS_COLLECTION = 90 * 1
# We use a warm-up period
# because the model starts up empty which doesn't reflect reality
WARM_UP = 60 * 1
RUN_LENGTH = RESULTS_COLLECTION + WARM_UP
# Set up the scenario for the model to run.
scenarios = {}
scenarios["as-is"] = Scenario(
RUN_LENGTH,
WARM_UP,
prop_carve_out=prop_carve_out,
seeds=generate_seed_vector(),
slots_file=shifts,
)
scenarios["pooled"] = Scenario(
RUN_LENGTH,
WARM_UP,
prop_carve_out=prop_carve_out,
pooling=True,
seeds=generate_seed_vector(),
slots_file=shifts,
)
scenarios["no_carve_out"] = Scenario(
RUN_LENGTH,
WARM_UP,
pooling=True,
prop_carve_out=0.0,
seeds=generate_seed_vector(),
slots_file=shifts,
)
clinic_lkup_df = pd.DataFrame(
[
{"clinic": 0, "icon": "🟠"},
{"clinic": 1, "icon": "🟡"},
{"clinic": 2, "icon": "🟢"},
{"clinic": 3, "icon": "🔵"},
{"clinic": 4, "icon": "🟣"},
{"clinic": 5, "icon": "🟤"},
{"clinic": 6, "icon": "⚫"},
{"clinic": 7, "icon": "⚪"},
{"clinic": 8, "icon": "🔶"},
{"clinic": 9, "icon": "🔷"},
{"clinic": 10, "icon": "🟩"},
]
)
def show_home_clinic(row):
if "more" not in row["icon"]:
if row["home_clinic"] == 0:
return "🟠"
if row["home_clinic"] == 1:
return "🟡"
if row["home_clinic"] == 2:
return "🟢"
if row["home_clinic"] == 3:
return "🔵"
if row["home_clinic"] == 4:
return "🟣"
if row["home_clinic"] == 5:
return "🟤"
if row["home_clinic"] == 6:
return "⚫"
if row["home_clinic"] == 7:
return "⚪"
if row["home_clinic"] == 8:
return "🔶"
if row["home_clinic"] == 9:
return "🔷"
if row["home_clinic"] == 10:
return "🟩"
else:
return row["icon"]
else:
return row["icon"]
def show_priority_icon(row):
if "more" not in row["icon"]:
if row["pathway"] == 2:
return "🚨"
else:
return row["icon"]
else:
return row["icon"]
def add_los_to_icon(row):
if row["event_original"] == "have_appointment":
return f"{row['icon']}<br>{int(row['wait'])}"
else:
return row["icon"]def generate_scenario_results(scenario):
results_all, results_low, results_high, event_log = single_run(scenarios[scenario])
event_log_df = pd.DataFrame(event_log)
event_log_df["event_original"] = event_log_df["event"]
event_log_df["event"] = event_log_df.apply(
lambda x: (
f"{x['event']}{f'_{int(x.booked_clinic)}' if pd.notna(x['booked_clinic']) else ''}"
),
axis=1,
)
event_log_df.to_csv(f"{scenario}.csv")
full_patient_df = reshape_for_animations(
event_log_df,
entity_col_name="patient",
event_col_name="event",
event_type_col_name="event_type",
limit_duration=RUN_LENGTH,
every_x_time_units=1,
step_snapshot_max=50,
warm_up=WARM_UP,
)
# Auto-generate event positions for every possible clinic and event combination
clinics = [
x
for x in event_log_df["booked_clinic"].sort_values().unique().tolist()
if not math.isnan(x)
]
clinic_waits = [
{
"event": f"appointment_booked_waiting_{int(clinic)}",
"y": 950 - (clinic + 1) * 80,
"x": 625,
"label": f"Booked into<br>clinic {int(clinic)}",
"clinic": int(clinic),
}
for clinic in clinics
]
clinic_attends = [
{
"event": f"have_appointment_{int(clinic)}",
"y": 950 - (clinic + 1) * 80,
"x": 850,
"label": f"Attending appointment<br>at clinic {int(clinic)}",
}
for clinic in clinics
]
event_position_df = pd.concat(
[pd.DataFrame(clinic_waits), (pd.DataFrame(clinic_attends))]
)
referred_out = [
{
"event": f"referred_out_{int(clinic)}",
"y": 950 - (clinic + 1) * 80,
"x": 125,
"label": f"Referred Out From <br>clinic {int(clinic)}",
}
for clinic in clinics
]
event_position_df = pd.concat([event_position_df, (pd.DataFrame(referred_out))])
if scenario in ["pooled", "no_carve_out"]:
event_position_df = event_position_df.merge(clinic_lkup_df, how="left")
event_position_df["label"] = event_position_df.apply(
lambda x: (
f"{x['label']} {x['icon']}" if pd.notna(x["icon"]) else x["label"]
),
axis=1,
)
event_position_df = event_position_df.drop(columns="icon")
event_position_df = event_position_df.drop(columns="clinic")
# Generate the final animation
full_patient_df_plus_pos = generate_animation_df(
full_entity_df=full_patient_df,
entity_col_name="patient",
event_col_name="event",
event_type_col_name="event_type",
event_position_df=event_position_df,
wrap_queues_at=25,
step_snapshot_max=50,
gap_between_entities=15,
gap_between_queue_rows=15,
debug_mode=True,
step_snapshot_limit_gauges=True,
)
return event_log_df, full_patient_df, full_patient_df_plus_pos, event_position_dfScenario 1 - As-is
event_log_df, full_patient_df, full_patient_df_plus_pos, event_position_df = (
generate_scenario_results("as-is")
)Placement dataframe started construction at 13:43:45
Placement dataframe finished construction at 13:43:45
/tmp/ipykernel_4131/2772590182.py:85: UserWarning: 8467 row(s) across 1 event(s) with no matching `event_position_df` row were rendered anyway: 'depart' (4667 entities).
An event with no position gets no coordinates, and a point with no coordinates can't be drawn - the entity's icon disappears for that frame, then flies in from the top-left corner once a positioned event takes over again.
Add a row to `event_position_df` for each event listed above.
full_patient_df_plus_pos = generate_animation_df(
full_patient_df_plus_pos| index | patient | pathway | event_type | event | home_clinic | time | booked_clinic | wait | event_original | ... | snapshot_time | hidden_run_before | y_final | x | label | x_final | row | icon | _phantom | opacity | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 56222 | 16860 | 9084207 | 1 | queue | appointment_booked_waiting_7 | 7 | 82 | 7.0 | NaN | appointment_booked_waiting | ... | 82 | 0 | 340.0 | 625.0 | Booked into<br>clinic 7 | 437.5 | 2.0 | [░░░░░░░░░░] + 0 more | False | 1.0 |
| 56223 | 16860 | 9084207 | 1 | queue | appointment_booked_waiting_7 | 7 | 82 | 7.0 | NaN | appointment_booked_waiting | ... | 83 | 0 | 340.0 | 625.0 | Booked into<br>clinic 7 | 437.5 | 2.0 | [░░░░░░░░░░] + 1 more | False | 1.0 |
| 56224 | 17205 | 9084207 | 1 | queue | appointment_booked_waiting_7 | 7 | 84 | 7.0 | NaN | appointment_booked_waiting | ... | 84 | 0 | 340.0 | 625.0 | Booked into<br>clinic 7 | 437.5 | 2.0 | [░░░░░░░░░░] + 0 more | False | 1.0 |
| 56225 | 17900 | 9084207 | 1 | queue | appointment_booked_waiting_7 | 7 | 87 | 7.0 | NaN | appointment_booked_waiting | ... | 87 | 0 | 340.0 | 625.0 | Booked into<br>clinic 7 | 437.5 | 2.0 | [░░░░░░░░░░] + 1 more | False | 1.0 |
| 56226 | 18110 | 9084207 | 1 | queue | appointment_booked_waiting_7 | 7 | 88 | 7.0 | NaN | appointment_booked_waiting | ... | 88 | 0 | 340.0 | 625.0 | Booked into<br>clinic 7 | 437.5 | 2.0 | [░░░░░░░░░░] + 1 more | False | 1.0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 4311 | 20266 | 99_9 | 1 | queue | appointment_booked_waiting_0 | 0 | 99 | 0.0 | NaN | appointment_booked_waiting | ... | 146 | 0 | 870.0 | 625.0 | Booked into<br>clinic 0 | 460.0 | 0.0 | 🤰 | False | 1.0 |
| 4358 | 20266 | 99_9 | 1 | queue | appointment_booked_waiting_0 | 0 | 99 | 0.0 | NaN | appointment_booked_waiting | ... | 147 | 0 | 870.0 | 625.0 | Booked into<br>clinic 0 | 505.0 | 0.0 | 🤰 | False | 1.0 |
| 4405 | 20266 | 99_9 | 1 | queue | appointment_booked_waiting_0 | 0 | 99 | 0.0 | NaN | appointment_booked_waiting | ... | 148 | 0 | 870.0 | 625.0 | Booked into<br>clinic 0 | 550.0 | 0.0 | 🤰 | False | 1.0 |
| 4452 | 20266 | 99_9 | 1 | queue | appointment_booked_waiting_0 | 0 | 99 | 0.0 | NaN | appointment_booked_waiting | ... | 149 | 0 | 870.0 | 625.0 | Booked into<br>clinic 0 | 595.0 | 0.0 | 🤰 | False | 1.0 |
| 4502 | 20266 | 99_9 | 1 | queue | appointment_booked_waiting_0 | 0 | 99 | 0.0 | NaN | appointment_booked_waiting | ... | 150 | 0 | 870.0 | 625.0 | Booked into<br>clinic 0 | 595.0 | 0.0 | 🤰 | False | 1.0 |
56596 rows × 22 columns
event_position_df.head()| event | y | x | label | |
|---|---|---|---|---|
| 0 | appointment_booked_waiting_0 | 870.0 | 625 | Booked into<br>clinic 0 |
| 1 | appointment_booked_waiting_1 | 790.0 | 625 | Booked into<br>clinic 1 |
| 2 | appointment_booked_waiting_2 | 710.0 | 625 | Booked into<br>clinic 2 |
| 3 | appointment_booked_waiting_3 | 630.0 | 625 | Booked into<br>clinic 3 |
| 4 | appointment_booked_waiting_4 | 550.0 | 625 | Booked into<br>clinic 4 |
def generate_clinic_animation(final_df):
fig = generate_animation(
full_entity_df_plus_pos=final_df,
event_position_df=event_position_df,
scenario=None,
entity_col_name="patient",
plotly_height=900,
plotly_width=1000,
override_x_max=1000,
override_y_max=1000,
entity_icon_size=10,
text_size=10,
include_play_button=True,
add_background_image=None,
display_stage_labels=True,
time_display_units="d",
simulation_time_unit="days",
start_date="2022-06-27",
setup_mode=False,
frame_duration=1500, # milliseconds
frame_transition_duration=1000, # milliseconds
debug_mode=False,
)
return fig
# TODO
# Add in additional trace that shows the number of available slots per day
# using the slot df
# TODO
# Pooled booking version where being in non-home clinic makes you one colour
# and home clinic makes you another
# TODO
# Investigate adding a priority attribute to event log
# that can be considered when ranking queues if presentgenerate_clinic_animation(full_patient_df_plus_pos).show()With Pooling
event_log_df, full_patient_df, full_patient_df_plus_pos, event_position_df = (
generate_scenario_results("pooled")
)Placement dataframe started construction at 13:43:52
Placement dataframe finished construction at 13:43:52
/tmp/ipykernel_4131/2772590182.py:85: UserWarning: 8499 row(s) across 1 event(s) with no matching `event_position_df` row were rendered anyway: 'depart' (4697 entities).
An event with no position gets no coordinates, and a point with no coordinates can't be drawn - the entity's icon disappears for that frame, then flies in from the top-left corner once a positioned event takes over again.
Add a row to `event_position_df` for each event listed above.
full_patient_df_plus_pos = generate_animation_df(
full_patient_df_plus_pos| index | patient | pathway | event_type | event | home_clinic | time | booked_clinic | wait | event_original | ... | snapshot_time | hidden_run_before | y_final | x | label | x_final | row | icon | _phantom | opacity | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 58066 | 12411 | 9084207 | 1 | queue | appointment_booked_waiting_7 | 7 | 60 | 7.0 | NaN | appointment_booked_waiting | ... | 60 | 0 | 340.0 | 625.0 | Booked into<br>clinic 7 ⚪ | 437.5 | 2.0 | [░░░░░░░░░░] + 0 more | False | 1.0 |
| 58067 | 12411 | 9084207 | 1 | queue | appointment_booked_waiting_7 | 7 | 60 | 7.0 | NaN | appointment_booked_waiting | ... | 61 | 0 | 340.0 | 625.0 | Booked into<br>clinic 7 ⚪ | 437.5 | 2.0 | [░░░░░░░░░░] + 3 more | False | 1.0 |
| 58068 | 12411 | 9084207 | 1 | queue | appointment_booked_waiting_7 | 7 | 60 | 7.0 | NaN | appointment_booked_waiting | ... | 62 | 0 | 340.0 | 625.0 | Booked into<br>clinic 7 ⚪ | 437.5 | 2.0 | [░░░░░░░░░░] + 5 more | False | 1.0 |
| 58069 | 12544 | 9084207 | 1 | queue | appointment_booked_waiting_7 | 8 | 61 | 7.0 | NaN | appointment_booked_waiting | ... | 63 | 0 | 340.0 | 625.0 | Booked into<br>clinic 7 ⚪ | 437.5 | 2.0 | [░░░░░░░░░░] + 4 more | False | 1.0 |
| 58070 | 12887 | 9084207 | 1 | queue | appointment_booked_waiting_7 | 8 | 63 | 7.0 | NaN | appointment_booked_waiting | ... | 64 | 0 | 340.0 | 625.0 | Booked into<br>clinic 7 ⚪ | 437.5 | 2.0 | [░░░░░░░░░░] + 4 more | False | 1.0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 13081 | 20364 | 99_9 | 1 | queue | appointment_booked_waiting_2 | 0 | 99 | 2.0 | NaN | appointment_booked_waiting | ... | 139 | 0 | 710.0 | 625.0 | Booked into<br>clinic 2 🟢 | 475.0 | 0.0 | 👱🏻♀️ | False | 1.0 |
| 13125 | 20364 | 99_9 | 1 | queue | appointment_booked_waiting_2 | 0 | 99 | 2.0 | NaN | appointment_booked_waiting | ... | 140 | 0 | 710.0 | 625.0 | Booked into<br>clinic 2 🟢 | 565.0 | 0.0 | 👱🏻♀️ | False | 1.0 |
| 45301 | 29007 | 99_9 | 1 | queue | have_appointment_2 | 0 | 141 | 2.0 | 42.0 | have_appointment | ... | 141 | 0 | 710.0 | 850.0 | Attending appointment<br>at clinic 2 | 790.0 | 0.0 | 👱🏻♀️ | False | 1.0 |
| 57217 | 29008 | 99_9 | 1 | arrival_departure | depart | 0 | 142 | NaN | NaN | depart | ... | 142 | 0 | NaN | NaN | NaN | NaN | NaN | 👱🏻♀️ | False | 1.0 |
| 53450 | 29008 | 99_9 | 1 | exit | depart | 0 | 142 | NaN | NaN | depart | ... | 143 | 0 | NaN | NaN | NaN | NaN | NaN | 👱🏻♀️ | False | 1.0 |
58466 rows × 22 columns
full_patient_df_plus_pos = full_patient_df_plus_pos.assign(
icon=full_patient_df_plus_pos.apply(show_home_clinic, axis=1)
)
full_patient_df_plus_pos = full_patient_df_plus_pos.assign(
icon=full_patient_df_plus_pos.apply(show_priority_icon, axis=1)
)
full_patient_df_plus_pos = full_patient_df_plus_pos.assign(
icon=full_patient_df_plus_pos.apply(add_los_to_icon, axis=1)
)generate_clinic_animation(full_patient_df_plus_pos).show()With Pooling - No Carve-out
event_log_df, full_patient_df, full_patient_df_plus_pos, event_position_df = (
generate_scenario_results("no_carve_out")
)Placement dataframe started construction at 13:44:00
Placement dataframe finished construction at 13:44:01
/tmp/ipykernel_4131/2772590182.py:85: UserWarning: 8499 row(s) across 1 event(s) with no matching `event_position_df` row were rendered anyway: 'depart' (4697 entities).
An event with no position gets no coordinates, and a point with no coordinates can't be drawn - the entity's icon disappears for that frame, then flies in from the top-left corner once a positioned event takes over again.
Add a row to `event_position_df` for each event listed above.
full_patient_df_plus_pos = generate_animation_df(
full_patient_df_plus_pos = full_patient_df_plus_pos.assign(
icon=full_patient_df_plus_pos.apply(show_home_clinic, axis=1)
)
full_patient_df_plus_pos = full_patient_df_plus_pos.assign(
icon=full_patient_df_plus_pos.apply(show_priority_icon, axis=1)
)
full_patient_df_plus_pos = full_patient_df_plus_pos.assign(
icon=full_patient_df_plus_pos.apply(add_los_to_icon, axis=1)
)generate_clinic_animation(full_patient_df_plus_pos).show()