Skip to content

Commit 345f216

Browse files
authored
Merge pull request #3 from pythonhealthdatascience/dev
Dev
2 parents 8c10985 + a49788c commit 345f216

18 files changed

+2383
-230
lines changed

DESCRIPTION

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ Encoding: UTF-8
1818
LazyData: true
1919
RoxygenNote: 7.3.2
2020
Imports:
21+
dplyr,
22+
future,
23+
future.apply,
2124
simmer
2225
Suggests:
23-
devtools
26+
devtools,
27+
ggplot2,
28+
scales
2429
Config/testthat/edition: 3

NAMESPACE

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,29 @@ export(create_rehab_arrivals)
1010
export(create_rehab_los)
1111
export(create_rehab_routing)
1212
export(create_rehab_trajectory)
13+
export(get_occupancy_stats)
1314
export(model)
15+
export(runner)
1416
export(sample_routing)
1517
export(transform_to_lnorm)
18+
importFrom(dplyr,bind_rows)
19+
importFrom(dplyr,filter)
20+
importFrom(dplyr,mutate)
21+
importFrom(dplyr,rowwise)
22+
importFrom(dplyr,ungroup)
23+
importFrom(future,multisession)
24+
importFrom(future,plan)
25+
importFrom(future,sequential)
26+
importFrom(future.apply,future_lapply)
1627
importFrom(simmer,add_generator)
28+
importFrom(simmer,add_resource)
1729
importFrom(simmer,branch)
1830
importFrom(simmer,get_attribute)
1931
importFrom(simmer,get_mon_arrivals)
2032
importFrom(simmer,get_mon_resources)
2133
importFrom(simmer,log_)
34+
importFrom(simmer,release)
35+
importFrom(simmer,seize)
2236
importFrom(simmer,set_attribute)
2337
importFrom(simmer,simmer)
2438
importFrom(simmer,timeout)

R/create_asu_trajectory.R

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
#' of routing to each destination (e.g.
1414
#' \code{param$asu_routing$stroke$rehab = 0.24}).
1515
#'
16-
#' @importFrom simmer branch get_attribute log_ set_attribute timeout trajectory
16+
#' @importFrom simmer branch get_attribute log_ release seize set_attribute
17+
#' @importFrom simmer timeout trajectory
1718
#' @importFrom stats rlnorm
1819
#'
1920
#' @return Simmer trajectory object. Defines patient journey logic through the
@@ -25,7 +26,9 @@ create_asu_trajectory <- function(env, patient_type, param) {
2526
# Set up simmer trajectory object...
2627
trajectory(paste0("ASU_", patient_type, "_path")) |>
2728

28-
log_("🚶 Arrived at ASU") |>
29+
log_("🚶 Arrived at ASU", level = 1L) |>
30+
31+
seize("asu_bed", 1L) |>
2932

3033
# Sample destination after ASU (as destination influences length of stay)
3134
set_attribute("post_asu_destination", function() {
@@ -39,7 +42,7 @@ create_asu_trajectory <- function(env, patient_type, param) {
3942
dest <- dest_names[dest_index]
4043
# Create log message
4144
paste0("🎯 Planned ASU -> ", dest_index, " (", dest, ")")
42-
}) |>
45+
}, level = 1L) |>
4346

4447
set_attribute("asu_los", function() {
4548
# Retrieve attribute, and use to get post-ASU destination as a string
@@ -72,11 +75,13 @@ create_asu_trajectory <- function(env, patient_type, param) {
7275
log_(function() {
7376
paste0("⏳ ASU length of stay: ",
7477
round(get_attribute(env, "asu_los"), 3L))
75-
}) |>
78+
}, level = 1L) |>
7679

7780
timeout(function() get_attribute(env, "asu_los")) |>
7881

79-
log_("🏁 ASU stay completed") |>
82+
log_("🏁 ASU stay completed", level = 1L) |>
83+
84+
release("asu_bed", 1L) |>
8085

8186
# If that patient's destination is rehab, then start on that trajectory
8287
branch(

R/create_rehab_trajectory.R

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
#' probability of routing to each destination (e.g.
1414
#' \code{param$rehab_routing$stroke$esd = 0.40}).
1515
#'
16-
#' @importFrom simmer get_attribute log_ set_attribute timeout trajectory
16+
#' @importFrom simmer get_attribute log_ release seize set_attribute timeout
17+
#' @importFrom simmer trajectory
1718
#' @importFrom stats rlnorm
1819
#'
1920
#' @return Simmer trajectory object. Defines patient journey logic through the
@@ -25,7 +26,9 @@ create_rehab_trajectory <- function(env, patient_type, param) {
2526
# Set up simmer trajectory object...
2627
trajectory(paste0("rehab_", patient_type, "_path")) |>
2728

28-
log_("🚶 Arrived at rehab") |>
29+
log_("🚶 Arrived at rehab", level = 1L) |>
30+
31+
seize("rehab_bed", 1L) |>
2932

3033
# Sample destination after rehab (as destination influences length of stay)
3134
set_attribute("post_rehab_destination", function() {
@@ -39,7 +42,7 @@ create_rehab_trajectory <- function(env, patient_type, param) {
3942
dest <- dest_names[dest_index]
4043
# Create log message
4144
paste0("🎯 Planned rehab -> ", dest_index, " (", dest, ")")
42-
}) |>
45+
}, level = 1L) |>
4346

4447
set_attribute("rehab_los", function() {
4548
# Retrieve attribute, and use to get post-rehab destination as a string
@@ -71,9 +74,11 @@ create_rehab_trajectory <- function(env, patient_type, param) {
7174
log_(function() {
7275
paste0("⏳ Rehab length of stay: ",
7376
round(get_attribute(env, "rehab_los"), 3L))
74-
}) |>
77+
}, level = 1L) |>
7578

7679
timeout(function() get_attribute(env, "rehab_los")) |>
7780

78-
log_("🏁 Rehab stay completed")
81+
log_("🏁 Rehab stay completed", level = 1L) |>
82+
83+
release("rehab_bed", 1L)
7984
}

R/get_occupancy_stats.R

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#' Calculate occupancy statistics
2+
#'
3+
#' For each resource in the data, this function computes how often each
4+
#' occupancy level was observed. It then calculates:
5+
#' \itemize{
6+
#' \item The frequency of each occupancy level
7+
#' \item The proportion (percentage) of time at each occupancy
8+
#' \item The cumulative proportion (cumulative percentage) up to each
9+
#' occupancy
10+
#' \item The probability of delay (probability that occupancy is at or above
11+
#' a given level)
12+
#' \item The "1 in every n" patients delayed (inverse of probability of delay)
13+
#' }
14+
#'
15+
#' @param occupancy DataFrame with three columns: \code{resource}, \code{time},
16+
#' and \code{occupancy}.
17+
#'
18+
#' @return A list of data frames, one per resource, each containing occupancy
19+
#' statistics.
20+
#' @export
21+
22+
get_occupancy_stats <- function(occupancy) {
23+
24+
results <- list()
25+
26+
# Split by resource
27+
for (resource_name in unique(occupancy[["resource"]])) {
28+
occ <- filter(occupancy, .data[["resource"]] == resource_name)
29+
30+
# Get frequency of each occupancy value
31+
freq_table <- table(occ[["occupancy"]])
32+
33+
# Get the full range of occupancy values (fill in gaps)
34+
min_occ <- min(occ[["occupancy"]])
35+
max_occ <- max(occ[["occupancy"]])
36+
all_occupancy <- min_occ:max_occ
37+
38+
# Create a complete frequency vector (fill missing with 0)
39+
complete_freq <- vapply(
40+
all_occupancy,
41+
function(x) {
42+
if (x %in% names(freq_table)) freq_table[as.character(x)] else 0L
43+
},
44+
FUN.VALUE = integer(1L)
45+
)
46+
47+
# Build the summary dataframe
48+
occ_stats <- data.frame(
49+
beds = all_occupancy,
50+
freq = as.integer(complete_freq)
51+
)
52+
53+
# Calculate proportion and cumulative proportion (percentage if *100)
54+
occ_stats[["pct"]] <- occ_stats[["freq"]] / sum(occ_stats[["freq"]])
55+
occ_stats[["c_pct"]] <- cumsum(occ_stats[["pct"]])
56+
57+
# Calculate probability of delay using the Erlang loss formula
58+
occ_stats[["prob_delay"]] <- occ_stats[["pct"]] / occ_stats[["c_pct"]]
59+
60+
# Calculate 1 in every n patients delayed
61+
occ_stats[["1_in_n_delay"]] <- round(1L / occ_stats[["prob_delay"]])
62+
63+
results[[resource_name]] <- occ_stats
64+
}
65+
results
66+
}

R/model.R

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
#' may not wish to do if being set elsewhere - such as done in \code{runner()}).
77
#' Default is TRUE.
88
#'
9-
#' @importFrom simmer get_mon_arrivals get_mon_resources simmer wrap
9+
#' @importFrom dplyr filter mutate rowwise ungroup
10+
#' @importFrom simmer add_resource get_mon_arrivals get_mon_resources simmer
11+
#' @importFrom simmer wrap
1012
#' @importFrom utils capture.output
1113
#'
1214
#' @return TBC
@@ -20,17 +22,27 @@ model <- function(run_number, param, set_seed = TRUE) {
2022
}
2123

2224
# Determine whether to get verbose activity logs
23-
verbose <- any(c(param[["log_to_console"]], param[["log_to_file"]]))
25+
param[["verbose"]] <- any(c(param[["log_to_console"]],
26+
param[["log_to_file"]]))
2427

2528
# Transform LOS parameters to lognormal scale
2629
param[["asu_los_lnorm"]] <- transform_to_lnorm(param[["asu_los"]])
2730
param[["rehab_los_lnorm"]] <- transform_to_lnorm(param[["rehab_los"]])
2831

2932
# Create simmer environment - set verbose to FALSE as using custom logs
30-
env <- simmer("simulation", verbose = FALSE)
33+
# (but can change to TRUE if want to see default simmer logs as well)
34+
env <- simmer("simulation", verbose = FALSE,
35+
log_level = if (param[["verbose"]]) 1L else 0L)
3136

3237
# Add ASU and rehab direct admission patient generators
3338
for (unit in c("asu", "rehab")) {
39+
40+
# Add beds resource with inifinite capacity (required so we can get metrics
41+
# on occupancy etc. based on count of patients with each resource)
42+
env <- add_resource(
43+
.env = env, name = paste0(unit, "_bed"), capacity = Inf
44+
)
45+
3446
for (patient_type in names(param[[paste0(unit, "_arrivals")]])) {
3547

3648
# Create patient trajectory
@@ -41,27 +53,26 @@ model <- function(run_number, param, set_seed = TRUE) {
4153
}
4254

4355
# Add patient generator using the created trajectory
44-
sim_log <- capture.output(
45-
env <- add_patient_generator( # nolint
46-
env = env,
47-
trajectory = traj,
48-
unit = unit,
49-
patient_type = patient_type,
50-
param = param
51-
)
56+
env <- add_patient_generator(
57+
env = env,
58+
trajectory = traj,
59+
unit = unit,
60+
patient_type = patient_type,
61+
param = param
5262
)
5363
}
5464
}
5565

5666
# Run the model
67+
sim_length <- param[["data_collection_period"]] + param[["warm_up_period"]]
5768
sim_log <- capture.output(
5869
env <- env |> # nolint
59-
simmer::run(20L) |>
70+
simmer::run(sim_length) |>
6071
wrap()
6172
)
6273

6374
# Save and/or display the log
64-
if (isTRUE(verbose)) {
75+
if (isTRUE(param[["verbose"]])) {
6576
# Create full log message by adding parameters
6677
param_string <- paste(names(param), param, sep = "=", collapse = ";\n ")
6778
full_log <- append(c("Parameters:", param_string, "Log:"), sim_log)
@@ -75,12 +86,39 @@ model <- function(run_number, param, set_seed = TRUE) {
7586
}
7687
}
7788

78-
# Extract the monitored arrivals and resources information from the simmer
79-
# environment object
80-
result <- list(
81-
arrivals = get_mon_arrivals(env, per_resource = TRUE, ongoing = TRUE),
82-
resources = get_mon_resources(env)
83-
)
89+
# Extract the monitored arrivals info from the simmer environment object.
90+
# Remove patients with start time of -1, as they are patients whose arrival
91+
# was sampled but falls after the end of the simulation.
92+
arrivals <- get_mon_arrivals(env, per_resource = TRUE, ongoing = TRUE) |>
93+
filter(.data[["start_time"]] != -1L)
94+
95+
# Create sequence of days from 0 to end of simulation
96+
days <- seq(0L, ceiling(sim_length))
97+
98+
# Calculate occupancy at end of each day (i.e. at time 1, 2, 3, 4...)
99+
# Make dataframe with one row per resource per day to count patients
100+
occupancy <- expand.grid(
101+
resource = unique(arrivals[["resource"]]),
102+
time = days
103+
) |>
104+
rowwise() |>
105+
mutate(
106+
# For each resource and day, count patients who:
107+
# - Arrived on or before this day (start_time <= time)
108+
# - Have not yet left by this day (end_time > time), or have NA end_time
109+
# (still present at simulation end)
110+
occupancy = sum(
111+
arrivals[["resource"]] == .data[["resource"]] &
112+
arrivals[["start_time"]] <= .data[["time"]] &
113+
(is.na(arrivals[["end_time"]]) |
114+
arrivals[["end_time"]] > .data[["time"]])
115+
)
116+
) |>
117+
ungroup()
118+
119+
# Set replication
120+
arrivals <- mutate(arrivals, replication = run_number)
121+
occupancy <- mutate(occupancy, replication = run_number)
84122

85-
return(result)
123+
return(list(arrivals = arrivals, occupancy = occupancy))
86124
}

0 commit comments

Comments
 (0)