This workshop builds a discrete event simulation model using a primary care example. The model worksheet and supporting material (slides, videos etc) can be found here.
It outlines the scenarios, giving a flow chart of the flow of patients through the activities. It then gives prompts on how to construct the model, as well as asking questions after each section.
Modifying the colours used
# simplified version of StrategyUnitTheme::su_theme_pal()
# see: https://github.com/The-Strategy-Unit/StrategyUnitTheme
su_theme_pal <- function(palette = "main", ...) {
pal <- c(orange = "#f9bf07",
red = "#ec6555",
blue = "#5881c1")
grDevices::colorRampPalette(pal, ...)
}
If you haven’t already got these packages installed you will need the following to complete this workshop:
install.packages(c("tidyverse", "simmer", "simmer.plot", "gridExtra", "TruncatedNormal", "writexlsx", "ggplot2"))
suppressPackageStartupMessages({
library(tidyverse)
library(simmer)
library(simmer.plot)
library(TruncatedNormal)
library(writexl)
library(gridExtra)
})
Create a simulation environment for “env_part1”.
env_part1 <- simmer("part1")
env_part1
## simmer environment: part1 | now: 0 | next:
## { Monitor: in memory }
There are 4 time related distributions to create: - the patients interarrival rate, - the time taken for asking the screening questions - the time for the receptionists to make the decision on what appointment to book - and the booking of the appointment (if needed).
# generic exponential distribution
rexp(n = 1, rate = 1)
## [1] 3.708146
# generic truncated normal distributions looks like:
rtnorm(n = 1, mu = 1, sd = 0.25, lb = 0, ub = Inf)
## [1] 1.283495
You can use helper functions, one option:
dist_helper <- function(rfn, ...) {
function() rfn(1, ...)
}
dist_patient_interarrival <- dist_helper(rexp, 40 / 60)
dist_screening <- dist_helper(rtnorm, 4, 1, 0, Inf)
dist_decision <- dist_helper(rtnorm, 1, 0.25, 0, Inf)
dist_booking <- dist_helper(rtnorm, 1, 0.25, 0, Inf)
Alternatively:
dist_patient_interarrival <- function() rexp(1, 40 / 60)
dist_screening <- function() rtnorm(n = 1, mu = 4, sd = 1, lb = 0, ub = Inf)
dist_decision <- function() rtnorm(n = 1, mu = 1, sd = 0.25, lb = 0, ub = Inf)
dist_booking <- function() rtnorm(n = 1, mu = 1, sd = 0.25, lb = 0, ub = Inf)
Create functions to sample from discrete distributions for routing the patient entities at the branches in the model. - Which mode of contact the patients will use, electronic vs phone - For whether no appointment, phone appointment or face to face appointment is given
dist_contact_mode <- function() sample(1:2, 1, FALSE, c(0.15, 0.85))
dist_booking_type <- function() sample(1:3, 1, FALSE, c(0.3, 0.6, 0.1))
Think about the flow diagram of the process to create the trajectories for the model, let’s break it down into manageable sections.
Create a trajectory for this branch called branch_ecr
. Within the trajectory use set_attribute via_ecr
to label the patient with a value of 1, so we will know what route they have come to analyse later. Then seize a receptionist ready for the next stage of the process.
branch_ecr <- trajectory("branch ecr") %>%
set_attribute("via_ecr", 1) %>%
seize("receptionist")
Create a renege trajectory, the patients renege (hang up) after 5 minutes so log this and set the attribute "reneged"
to 1.
Create a trajectory for this branch called branch_phone
. Within the trajectory use set_attribute via_ecr
to label the patient with a value of 0 (as opposed to 1 which are ECR route patients). Set the renege trajectory. Then seize a receptionist ready for the next stage. Once the patient has a receptionist abort the renege and add timeout of distribution screening
renege_hungup <- trajectory("hung up") %>%
set_attribute("reneged", 1) %>%
log_("lost my patience. Hanging up...")
branch_phone <- trajectory("branch phone") %>%
set_attribute("via_ecr", 0) %>%
renege_in(t = 5, out = renege_hungup) %>%
seize("receptionist") %>%
renege_abort() %>% # now have a receptionist the patient won't renege
timeout(dist_screening)
We’ll look at an alternative way, Option B, to set this branch up later on.
The branches of the trajectory after the receptionist decides if a patient needs a face to face appointment, phone appointment or does not need any appointment. Using set_attribute to record the outcome and the time taken to make the booking where appropriate.
branch_booking_none <- trajectory("no appointment needed") %>%
release("receptionist") %>%
set_attribute("booked_appt", 0)
branch_booking_phone <- trajectory("booking a phone") %>%
timeout(dist_booking) %>%
release("receptionist") %>%
set_attribute("booked_appt", 1)
branch_booking_f2f <- trajectory("booking a f2f") %>%
timeout(dist_booking) %>%
release("receptionist") %>%
set_attribute("booked_appt", 2)
Create the overall patient flow trajectory by including the branch for the contact modes, then the time for whether to book an appointment, followed by the branch for the outcomes of that decision (none/phone/F2F).
patient_flow <- trajectory("patient flow") %>%
branch(dist_contact_mode, TRUE,
branch_ecr,
branch_phone) %>%
timeout(dist_decision) %>%
branch(dist_booking_type, TRUE,
branch_booking_none,
branch_booking_phone,
branch_booking_f2f)
patient_flow
## trajectory: patient flow, 20 activities
## { Activity: Branch | option: function() }
## Fork 1, continue, trajectory: branch ecr, 2 activities
## { Activity: SetAttribute | keys: [via_ecr], values: [1], global: 0, mod: N, init: 0 }
## { Activity: Seize | resource: receptionist, amount: 1 }
## Fork 2, continue, trajectory: branch phone, 7 activities
## { Activity: SetAttribute | keys: [via_ecr], values: [0], global: 0, mod: N, init: 0 }
## { Activity: RenegeIn | t: 5, keep_seized: 0 }
## Fork 1, stop, trajectory: hung up, 2 activities
## { Activity: SetAttribute | keys: [reneged], values: [1], global: 0, mod: N, init: 0 }
## { Activity: Log | message: lost my pa..., level: 0 }
## { Activity: Seize | resource: receptionist, amount: 1 }
## { Activity: RenegeAbort | }
## { Activity: Timeout | delay: function() }
## { Activity: Timeout | delay: function() }
## { Activity: Branch | option: function() }
## Fork 1, continue, trajectory: no appointment needed, 2 activities
## { Activity: Release | resource: receptionist, amount: 1 }
## { Activity: SetAttribute | keys: [booked_appt], values: [0], global: 0, mod: N, init: 0 }
## Fork 2, continue, trajectory: booking a phone, 3 activities
## { Activity: Timeout | delay: function() }
## { Activity: Release | resource: receptionist, amount: 1 }
## { Activity: SetAttribute | keys: [booked_appt], values: [1], global: 0, mod: N, init: 0 }
## Fork 3, continue, trajectory: booking a f2f, 3 activities
## { Activity: Timeout | delay: function() }
## { Activity: Release | resource: receptionist, amount: 1 }
## { Activity: SetAttribute | keys: [booked_appt], values: [2], global: 0, mod: N, init: 0 }
plot(patient_flow, fill = su_theme_pal("main"))
# you can change the verbose argument to TRUE to get more information
plot(patient_flow, fill = su_theme_pal("main"), verbose = TRUE)
Add the 2 receptionists as a resource. Add a generator to control the arrival of patient contacts to your trajectory using to(stop_time, dist), where stop time is 600 (10 hours =10 * 60 minutes=600 minutes) and set mon=2 to include additional attribute monitoring. Use to() with caution as the stop time is that of the generator and not the simulation.
env_part1 <- simmer("part 1") %>%
add_resource("receptionist", 2) %>%
add_generator("patient", patient_flow, to(10 * 60, dist_patient_interarrival), mon = 2)
The simulation will run until all arrivals are finished, as we haven’t specified a time in the run command. We are assuming that staff stay overtime to finish. Setting a seed value makes it so we always get the same starting point for random numbers, making our results repeatable, the choice of seed is not important here.
set.seed(1)
env_part1 %>%
run()
This helps us check how the model has run:
env_part1 %>%
get_mon_arrivals() %>%
arrange(start_time) %>%
head(10)
name | start_time | end_time | activity_time | finished | replication |
---|---|---|---|---|---|
patient0 | 1.132773 | 6.908897 | 5.776124 | TRUE | 1 |
patient1 | 1.351333 | 3.088203 | 1.736870 | TRUE | 1 |
patient2 | 2.005436 | 9.805289 | 6.717086 | TRUE | 1 |
patient3 | 3.440287 | 12.685677 | 5.776780 | TRUE | 1 |
patient4 | 5.526390 | 15.822586 | 6.017297 | TRUE | 1 |
patient5 | 7.382795 | 12.382795 | 0.000000 | TRUE | 1 |
patient6 | 8.616130 | 18.480375 | 5.794698 | TRUE | 1 |
patient7 | 10.623497 | 15.623497 | 0.000000 | TRUE | 1 |
patient8 | 16.561897 | 22.403788 | 5.841891 | TRUE | 1 |
patient9 | 17.047912 | 23.154664 | 4.674289 | TRUE | 1 |
env_part1 %>%
get_mon_arrivals() %>%
arrange(start_time) %>%
tail(10)
name | start_time | end_time | activity_time | finished | replication | |
---|---|---|---|---|---|---|
391 | patient390 | 584.1847 | 589.1847 | 0.000000 | TRUE | 1 |
392 | patient391 | 585.5690 | 590.5690 | 0.000000 | TRUE | 1 |
393 | patient392 | 588.2739 | 596.2042 | 4.619542 | TRUE | 1 |
394 | patient393 | 588.7914 | 593.7914 | 0.000000 | TRUE | 1 |
395 | patient394 | 591.1883 | 596.5531 | 2.389544 | TRUE | 1 |
396 | patient395 | 592.3754 | 604.9451 | 8.740904 | TRUE | 1 |
397 | patient396 | 594.9240 | 605.2079 | 8.654825 | TRUE | 1 |
398 | patient397 | 595.6792 | 600.6792 | 0.000000 | TRUE | 1 |
399 | patient398 | 597.8085 | 602.8085 | 0.000000 | TRUE | 1 |
400 | patient399 | 599.7585 | 604.7585 | 0.000000 | TRUE | 1 |
If you are new to R and struggling with data manipulation, you can export the results as xlsx or csv. Or email us and we can help you!
write_xlsx(get_mon_arrivals(env_part1), "arrivals_part1.xlsx")
write_xlsx(get_mon_resources(env_part1), "resources_part1.xlsx")
write_xlsx(get_mon_attributes(env_part1), "attributes_part1.xlsx")
This can be done via get_mon_attributes() and filtering appropriate attribute
env_part1 %>%
get_mon_attributes() %>%
filter(key == "reneged") %>%
summarise(countreneged = n())
countreneged |
---|
151 |
countreneged=151
This can be done via get_mon_attributes() and selecting appropriate attribute and value
env_part1 %>%
get_mon_attributes() %>%
filter(key == "booked_appt", value == 2) %>%
summarise(countF2F = n())
countF2F |
---|
28 |
countF2F=28
Firstly, we will use the get_mon_attributes() function to get attributes such as reneged and ECR of finished patients. It is helpful to use: pivot_wider(names_from = “key”, values_from = “value”, values_fill = 0) as part of this.
part1_attributes <- env_part1 %>%
get_mon_attributes() %>%
dplyr::select(-time) %>%
pivot_wider(names_from = "key", values_from = "value", values_fill = 0)
We will then join it with arrivals obtained from get_mon_arrivals(). Filter those who came through phone branch and calculate call wait time as a difference between total time in the system (end time - start time) and activity time. We will also set waiting time as 5 min for those who reneged. The current model has no unfinished and so we do not consider them here. Due to rounding issues, you might get very low negative values for waiting times, e.g. 9*10-16. To solve it, we will substitute negative values with 0 via pmax(end_time - start_time - activity_time, 0).
part1_callwait_results <- env_part1 %>%
get_mon_arrivals(ongoing = TRUE) %>% # not needed as there aren't any due to the generator and run()
inner_join(part1_attributes, by = c("name", "replication")) %>%
filter(via_ecr == 0) %>%
mutate(wait_time_call = case_when(
# the caller reneged
reneged == 1 ~ 5,
# everything else, (assume has been answered)
TRUE ~ pmax(end_time - start_time - activity_time, 0)
))
Once done, we can calculate desired statistics such as minimum, maximum, mean and quantiles.
part1_callwait_summary <- part1_callwait_results %>%
summarise(median_wait = median(wait_time_call),
mean_wait = mean(wait_time_call),
var_wait = var(wait_time_call),
min_wait = min(wait_time_call),
max_wait = max(wait_time_call),
Qtile25th_wait = quantile(wait_time_call, probs = 0.25),
Qtile75th_wait = quantile(wait_time_call, probs = 0.75),
countunfinished = sum(!finished),
countfinished = sum(finished))
part1_callwait_summary
median_wait | mean_wait | var_wait | min_wait | max_wait | Qtile25th_wait | Qtile75th_wait | countunfinished | countfinished |
---|---|---|---|---|---|---|---|---|
4.839153 | 3.811972 | 2.657451 | 0 | 5 | 3.010095 | 5 | 0 | 334 |
Glimpse the end tail and also check whether ECR backlog is cleared at the end of the days. Overall backlog - let’s look at 1 replication
attributes <- env_part1 %>%
get_mon_attributes() %>%
filter(replication == 1)
test <- env_part1 %>%
get_mon_arrivals() %>%
filter(finished == FALSE, replication == 1) %>%
inner_join(attributes, by = "name") %>%
arrange(start_time)
test
name | start_time | end_time | activity_time | finished | replication.x | time | key | value | replication.y |
---|
env_part1 %>%
get_mon_attributes() %>%
filter(replication == 1, key == "via_ecr", value == 1) %>%
count()
n |
---|
66 |
You can see that everyone finished and we had total of 66 ECR.
An alternative way to do this, uses the way described above but with the addition of setting an attribute for the time of events. However, this way will make code more complex when we get to part 3 - multiple replications but might be useful to see for application in other models with more comlex trajectories.
env_part1B <- simmer("part 1B")
env_part1B
## simmer environment: part 1B | now: 0 | next:
## { Monitor: in memory }
Phone branch: As Option A with the addition of setting an attribute for the time of events e.g. the time a patients call is answered using ‘now(env_part1)’.
renege_hungupB <- trajectory("hung up") %>%
set_attribute("reneged", 1) %>%
set_attribute("reneged_at", function() now(env_part1B)) %>% #Opt.B addition
log_("lost my patience. Hanging up...")
branch_phoneB <- trajectory("branch phone") %>%
set_attribute("via_ecr", 0) %>%
set_attribute("arrival", function() now(env_part1B)) %>% #Opt.B addition
renege_in(t = 5, out = renege_hungup) %>%
seize("receptionist") %>%
set_attribute("call_taken", function() now(env_part1B)) %>% #Opt.B addition
renege_abort() %>% # now have a receptionist the patient won't renege
timeout(dist_screening)
Outcome branches: As option A but with additional set attributes
#No appointment needed:
branch_booking_noneB <- trajectory("no appointment needed") %>%
release("receptionist") %>%
set_attribute("booked_appt", 0) %>%
set_attribute("call_end", function() now(env_part1B)) #Opt.B addition
#Phone appointment needed:
branch_booking_phoneB <- trajectory("booking a f2f") %>%
timeout(dist_booking) %>%
release("receptionist") %>%
set_attribute("booked_appt", 1) %>%
set_attribute("call_end", function() now(env_part1B)) #Opt.B addition
#Face to Face appointment needed:
branch_booking_f2fB <- trajectory("booking a f2f") %>%
timeout(dist_booking) %>%
release("receptionist") %>%
set_attribute("booked_appt", 2) %>%
set_attribute("call_end", function() now(env_part1B)) #Opt.B addition
Create the overall patient flow trajectory as OptionA but using the Option B branches
patient_flowB <- trajectory("patient flowB") %>%
branch(dist_contact_mode, TRUE,
branch_ecr,
branch_phoneB) %>%
timeout(dist_decision) %>%
branch(dist_booking_type, TRUE,
branch_booking_noneB,
branch_booking_phoneB,
branch_booking_f2fB)
Look at option B’s trajectory, note the additional set attributes to the plot produced by option A
plot(patient_flowB, fill = su_theme_pal("main"))
As before but using the option B trajectory
env_part1B <- simmer("part 1B") %>%
add_resource("receptionist", 2) %>%
add_generator("patient", patient_flowB, to(10 * 60, dist_patient_interarrival), mon = 2)
Run Part 1B model
set.seed(1)
env_part1B %>% run()
Q1. How many patients fail to speak to the receptionist (renege)?
env_part1B %>%
get_mon_attributes() %>%
filter(key == "reneged") %>%
summarise(countreneged = n())
countreneged |
---|
151 |
n=151, same as before
Q2. How many people get a F2F booking?
env_part1B %>%
get_mon_attributes() %>%
filter(key == "booked_appt", value == 2) %>%
summarise(countF2F = n())
countF2F |
---|
28 |
n=28, same as before
Q3. What can you say about the wait for call to be answered?
The attribute data can be used to calculate call waiting time from the recorded times in the attributes, remembering to include 5 min for those who reneged. We don’t have any unfinished calls in our model but we could use the duration of the run and the time they entered the queue to get this. This approach will cause extra complexity in part 3 of the workshop, but it might work better in other models when you have different resources and servers.
part1B_callwait_results <- env_part1B %>%
get_mon_attributes() %>%
pivot_wider(-c(time, replication), names_from = key, values_from = value) %>%
filter(via_ecr == 0, arrival >= 0) %>%
mutate(wait_time_call = ifelse(is.na(reneged),
call_taken - arrival, #has been answered
5)) # has reneged
Then calculate desired summary statistics as before.
part1B_callwait_summary <- part1B_callwait_results %>%
summarise(median_wait = median(wait_time_call),
mean_wait = mean(wait_time_call),
var_wait = var(wait_time_call),
min_wait = min(wait_time_call),
max_wait = max(wait_time_call),
Qtile25th_wait = quantile(wait_time_call, probs = 0.25),
Qtile75th_wait = quantile(wait_time_call, probs = 0.75),
countunfinished = sum(is.na(call_end) & is.na(reneged)),
countfinished = sum(!is.na(call_end) | !is.na(reneged)))
part1B_callwait_summary
median_wait | mean_wait | var_wait | min_wait | max_wait | Qtile25th_wait | Qtile75th_wait | countunfinished | countfinished |
---|---|---|---|---|---|---|---|---|
4.839153 | 3.811972 | 2.657451 | 0 | 5 | 3.010095 | 5 | 0 | 334 |
The worksheet outlines some additional features to include in the model: - Time-dependent interarrival times - Time-dependent resources (staffing) - Prioritisation of phone calls over electronic requests
Create a new environment
env_part2 <- simmer("part2")
env_part2
## simmer environment: part2 | now: 0 | next:
## { Monitor: in memory }
It’s helpful to use these helper functions to switch between minutes, hours and day.
t_minute <- function(t) t
t_hour <- function(t) t_minute(t) * 60
t_day <- function(t) t_hour(t) * 24
To include time dependent interarrivals we create a different distribution, one for each time period (3 in total).
dist_patient_interarrival1 <- function() rexp(1, 120 / t_hour(2))
dist_patient_interarrival2 <- function() rexp(1, 240 / t_hour(6))
dist_patient_interarrival3 <- function() rexp(1, 40 / t_hour(2))
We are also going prioritise the patients contacting the surgery via the phone.
In the ECR branch, no changes are needed. The default priority for all arrivals is set up to 0.
In the phone branch, we want to set a higher priority using set_prioritization(), that receptionists prioritise answering calls over reviewing electronic requests but without interrupting current tasks (prioirity = 1
, preemptive = 1
, restart = FALSE
). If you didn’t use now(env_part1) to set attributes you can use join(branch_phone) instead to link after the prioritisation back to your original phone branch.
branch_phone2 <- trajectory("branch phone prioritised") %>%
set_prioritization(c(1, 1, FALSE)) %>%
join(branch_phone)
plot(branch_phone2, fill = su_theme_pal("main"))
from the help page for add_generator()
: - priority
: the priority of each arrival (a higher integer equals higher priority; defaults to the minimum priority, which is 0. - preemptible
: if a seize occurs in a preemptive resource, this parameter establishes the minimum incoming priority that can preempt these arrivals (an arrival with a priority greater than preemptible
gains the resource). In any case, preemptible
must be equal or greater than priority, and thus only higher priority arrivals can trigger preemption. - restart
: whether the activity must be restarted after being preempted.
Create the overall patient trajectory again, the same as Part 1 but with the new prioritised phone branch
patient_flow2 <- trajectory("patient flow2") %>%
branch(dist_contact_mode, TRUE,
branch_ecr,
branch_phone2) %>% #<-- changed the phone branch
timeout(dist_decision) %>%
branch(dist_booking_type, TRUE,
branch_booking_none,
branch_booking_phone,
branch_booking_f2f)
plot(patient_flow2, fill = su_theme_pal())
Use the schedule(timetable, values, period) function and set it to repeat the schedule every 24 hour period. Work out from the numbers on each shift what times capacity changes and how many are working at that point: e.g. 0-8am = 0, 8-9am = 1, 9-9.30 am = 2 etc….
sch_receptionists <- schedule(
timetable = t_hour(c(0.0, 8.0, 9.0, 9.5, 12.0, 15.5, 18.0)),
values = c(0, 1, 2, 4, 3, 1, 0),
period = t_hour(24)
)
Add the receptionists as a resource using the schedule you created for the capacity. Either add three different generators to the model environment setting the times they operate with from_to or use functional programming to control this. You can look back at the training slides and examples to help you.
env_part2 %>%
add_resource("receptionist", capacity = sch_receptionists) %>%
add_generator("patient[8am-10am]", patient_flow2,
from_to(t_hour(8), t_hour(10), dist_patient_interarrival1, every = t_day(1)),
mon = 2) %>%
add_generator("patient[10am-4pm]", patient_flow2,
from_to(t_hour(10), t_hour(16), dist_patient_interarrival2, every = t_day(1)),
mon = 2) %>%
add_generator("patient[4pm-6pm]", patient_flow2,
from_to(t_hour(16), t_hour(18), dist_patient_interarrival3, every = t_day(1)),
mon = 2)
## simmer environment: part2 | now: 0 | next: 0
## { Monitor: in memory }
## { Resource: receptionist | monitored: TRUE | server status: 0(0) | queue status: 0(Inf) }
## { Source: patient[8am-10am] | monitored: 2 | n_generated: 0 }
## { Source: patient[10am-4pm] | monitored: 2 | n_generated: 0 }
## { Source: patient[4pm-6pm] | monitored: 2 | n_generated: 0 }
Set the random seed to 1 to give us the same results each time. Run the model for 5 days (52460) or use one of the time helper functions.
set.seed(1)
env_part2 %>%
run(t_day(5))
Repeat the analysis of call waits, reneging and F2F appointments as for part 1 using the environment for part 2.
env_part2 %>%
get_mon_attributes() %>%
filter(key == "reneged") %>%
summarise(countreneged = n())
countreneged |
---|
582 |
n = 582
env_part2 %>%
get_mon_attributes() %>%
filter(key == "booked_appt", value == 2) %>%
summarise(countF2F = n())
countF2F |
---|
160 |
n = 160
part2_attributes <- env_part2 %>%
get_mon_attributes() %>%
dplyr::select(-time) %>%
pivot_wider(names_from = "key", values_from = "value", values_fill = 0)
part2_callwait_results <- env_part2 %>%
get_mon_arrivals(ongoing = TRUE) %>%
full_join(part2_attributes, by = c("name", "replication")) %>% # or can use inner_join
filter(via_ecr == 0) %>%
mutate(wait_time_call = case_when(
reneged == 1 ~ 5,
TRUE ~ pmax(end_time - start_time - activity_time, 0)
))
part2_callwait_summary <- part2_callwait_results %>%
summarise(median_wait = median(wait_time_call),
mean_wait = mean(wait_time_call),
var_wait = var(wait_time_call),
min_wait = min(wait_time_call),
max_wait = max(wait_time_call),
Qtile25th_wait = quantile(wait_time_call, probs = 0.25),
Qtile75th_wait = quantile(wait_time_call, probs = 0.75),
countunfinished = sum(!finished),
countfinished = sum(finished))
part2_callwait_summary
median_wait | mean_wait | var_wait | min_wait | max_wait | Qtile25th_wait | Qtile75th_wait | countunfinished | countfinished |
---|---|---|---|---|---|---|---|---|
3.825883 | 3.183036 | 3.479728 | 0 | 5 | 1.464796 | 5 | 0 | 1726 |
Some ideas: * prevent staff staying to finish all just the one they are on, * multiple runs, * staff breaks, availability, are they also doing other tasks?
Below are some ideas on additional analysis you might want to do.
plot(get_mon_arrivals(env_part2), metric = "waiting_time")
## `geom_smooth()` using method = 'gam' and formula 'y ~ s(x, bs = "cs")'
g1 <- ggplot() +
geom_boxplot(data = part2_callwait_results,
aes(x = "rep=1", y = wait_time_call),
outlier.shape = 19,
width = 0.3,
fill = "white",
colour = "orange") +
geom_point(data = part2_callwait_summary,
aes(x = "rep=1", y = mean_wait),
shape = 19,
size = 2,
colour = "red") +
labs(x = "Part2",
y = "call wait (mins)",
title = "Boxplot of call wait from single replication") +
theme(axis.text = element_text(size = 11))
g1
Glimpse the end tail and also check whether ECR backlog is cleared at the end of the days. Overall backlog:
attributes <- env_part2 %>%
get_mon_attributes() %>%
filter(replication == 1)
test <- env_part2 %>%
get_mon_arrivals() %>%
filter(finished == FALSE, replication == 1) %>%
inner_join(attributes, by = "name") %>%
arrange(start_time)
test %>%
filter(key == "via_ecr", value != 1)
name | start_time | end_time | activity_time | finished | replication.x | time | key | value | replication.y |
---|
env_part2 %>%
get_mon_attributes() %>%
filter(replication == 1, key == "via_ecr", value == 1) %>%
count()
n |
---|
357 |
test %>%
head(10) %>%
as_tibble()
name | start_time | end_time | activity_time | finished | replication.x | time | key | value | replication.y |
---|---|---|---|---|---|---|---|---|---|
patient[4pm-6pm]191 | 6774.405 | NA | NA | FALSE | 1 | 6774.405 | via_ecr | 1 | 1 |
patient[4pm-6pm]192 | 6775.585 | NA | NA | FALSE | 1 | 6775.585 | via_ecr | 1 | 1 |
patient[4pm-6pm]201 | 6790.286 | NA | NA | FALSE | 1 | 6790.286 | via_ecr | 1 | 1 |
All unfinished are ECR, because calls were prioritised ahead of them. 357 total ECR.
Look at resources (reception staff in this example)
resources2 <- get_mon_resources(env_part2)
plot(resources2, metric = "usage", "receptionist", items = "server", steps = TRUE)
plot(resources2, metric = "utilization", "receptionist")
# show the top 10 results
head(resources2, n = 10)
resource | time | server | queue | capacity | queue_size | system | limit | replication |
---|---|---|---|---|---|---|---|---|
receptionist | 480.0000 | 0 | 0 | 1 | Inf | 0 | Inf | 1 |
receptionist | 480.0000 | 1 | 0 | 1 | Inf | 1 | Inf | 1 |
receptionist | 481.1816 | 1 | 1 | 1 | Inf | 2 | Inf | 1 |
receptionist | 481.6397 | 1 | 2 | 1 | Inf | 3 | Inf | 1 |
receptionist | 484.5346 | 1 | 3 | 1 | Inf | 4 | Inf | 1 |
receptionist | 484.9699 | 1 | 4 | 1 | Inf | 5 | Inf | 1 |
receptionist | 485.1100 | 1 | 3 | 1 | Inf | 4 | Inf | 1 |
receptionist | 486.1832 | 1 | 4 | 1 | Inf | 5 | Inf | 1 |
receptionist | 486.6397 | 1 | 3 | 1 | Inf | 4 | Inf | 1 |
receptionist | 487.5739 | 1 | 4 | 1 | Inf | 5 | Inf | 1 |
# list of hours
HrList <- data.frame(
resource = "receptionist",
time = seq(from = 0, to = t_day(5), by = 60),
server = NA,
queue = NA,
capacity = NA,
queue_size = Inf,
system = NA,
limit = Inf,
replication = 1
)
# which aren't in the data
AddSnaps <- filter(HrList, !(time %in% resources2$time))
resources2_hourly <- bind_rows(resources2, AddSnaps) %>%
arrange(time) %>%
mutate(server = ifelse(is.na(server), lag(server), server),
queue = ifelse(is.na(queue), lag(queue), queue),
capacity = ifelse(is.na(capacity), lag(capacity), capacity),
system = ifelse(is.na(system), lag(system), system))
resourceutilisation_byhour <- resources2_hourly %>%
arrange(time) %>%
mutate(slot_hour = floor(time / 60)) %>%
mutate(slot_day = ceiling(slot_hour / 24)) %>%
mutate(slot = slot_hour - ((slot_day - 1) * 24)) %>%
group_by(slot) %>%
mutate(dt = time - lag(time),
capacity = ifelse(capacity < server, server, capacity),
in_use = dt * lag(server / capacity)) %>%
summarise(utilised_percent = sum(in_use, na.rm = TRUE) / sum(dt, na.rm = TRUE) * 100)
resourceutilisation_byhour
slot | utilised_percent |
---|---|
1 | 0.0000000 |
2 | 0.0000000 |
3 | 0.0000000 |
4 | 0.0000000 |
5 | 0.0000000 |
6 | 0.0000000 |
7 | 0.0000000 |
8 | 100.0000000 |
9 | 100.0000000 |
10 | 99.9259080 |
11 | 63.7850307 |
12 | 99.6780740 |
13 | 91.7546945 |
14 | 75.7575154 |
15 | 99.8493045 |
16 | 99.8748793 |
17 | 100.0000000 |
18 | 0.1985734 |
19 | 0.0000000 |
20 | 0.0000000 |
21 | 0.0000000 |
22 | 0.0000000 |
23 | 0.0000000 |
24 | 0.0000000 |
Alternative way using Option B, if we used now(env_part1) to set attributes then code needs to change environment for all parts of the trajectory in which it was used.c The overall trajectory is then created again using the new versions.
Firstly, create a new environment.
env_part2B <- simmer("part 2B")
env_part2B
## simmer environment: part 2B | now: 0 | next:
## { Monitor: in memory }
Change references to part env_part2B in all trajectories using now(env_part2B.
renege_hungup2B <- trajectory("hung up") %>%
set_attribute("reneged", 1) %>%
set_attribute("reneged_at", function() now(env_part2B)) %>% # changed to env_part2B
log_("lost my patience. Hanging up...")
branch_phone2B <- trajectory("branch phone") %>%
set_prioritization(c(1, 1, FALSE)) %>% # set to increase priority from default of 0 to 1, no preempt and no restart
set_attribute("via_ecr", 0) %>%
set_attribute("arrival", function() now(env_part2B)) %>% # changed to env_part2B
renege_in(t = 5, out = renege_hungup) %>%
seize("receptionist") %>%
set_attribute("call_taken", function() now(env_part2B)) %>% # changed to env_part2B
renege_abort() %>%
timeout(dist_screening)
branch_booking_none2B <- trajectory("no appointment needed") %>%
release("receptionist") %>%
set_attribute("booked_appt", 0) %>%
set_attribute("call_end", function() now(env_part2B)) # changed to env_part2B
branch_booking_phone2B <- trajectory("booking a f2f") %>%
timeout(dist_booking) %>%
release("receptionist") %>%
set_attribute("booked_appt", 1) %>%
set_attribute("call_end", function() now(env_part2B)) # changed to env_part2B
branch_booking_f2f2B <- trajectory("booking a f2f") %>%
timeout(dist_booking) %>%
release("receptionist") %>%
set_attribute("booked_appt", 2) %>%
set_attribute("call_end", function() now(env_part2B)) # changed to env_part2B
patient_flow2B <- trajectory("patient flow2B") %>%
branch(dist_contact_mode, TRUE,
branch_ecr,
branch_phone2B) %>%
timeout(dist_decision) %>%
branch(dist_booking_type, TRUE,
branch_booking_none2B,
branch_booking_phone2B,
branch_booking_f2f2B)
Populate environment using the trajectory for patient flow 2B
env_part2B %>%
add_resource("receptionist", capacity = sch_receptionists) %>%
add_generator("patient[8am-10am]", patient_flow2B,
from_to(t_hour(8), t_hour(10), dist_patient_interarrival1, every = t_day(1)),
mon = 2) %>%
add_generator("patient[10am-4pm]", patient_flow2B,
from_to(t_hour(10), t_hour(16), dist_patient_interarrival2, every = t_day(1)),
mon = 2) %>%
add_generator("patient[4pm-6pm]", patient_flow2B,
from_to(t_hour(16), t_hour(18), dist_patient_interarrival3, every = t_day(1)),
mon = 2)
## simmer environment: part 2B | now: 0 | next: 0
## { Monitor: in memory }
## { Resource: receptionist | monitored: TRUE | server status: 0(0) | queue status: 0(Inf) }
## { Source: patient[8am-10am] | monitored: 2 | n_generated: 0 }
## { Source: patient[10am-4pm] | monitored: 2 | n_generated: 0 }
## { Source: patient[4pm-6pm] | monitored: 2 | n_generated: 0 }
Run the model for 5 days
set.seed(1)
env_part2B %>%
run(t_day(5))
Q1. What is the average wait for calls to be answered, number abandoning their calls (hanging up) and the number booked for a face to face appointment in this simulated week?
env_part2B %>%
get_mon_attributes() %>%
filter(key == "reneged") %>%
summarise(countreneged = n())
countreneged |
---|
582 |
n=582
env_part2B %>%
get_mon_attributes() %>%
filter(key == "booked_appt", value == 2) %>%
summarise(countF2F = n())
countF2F |
---|
160 |
n=160
part2B_callwait_results <- env_part2B %>%
get_mon_attributes() %>%
pivot_wider(-c(time, replication), names_from = key, values_from = value) %>%
filter(via_ecr == 0, arrival >= 0) %>%
mutate(wait_time_call = case_when(
!is.na(reneged) ~ 5,
is.na(call_taken) ~ t_day(5) - arrival, # in queue/progress still, model run for 5 days
TRUE ~ call_taken - arrival # has been answered
))
part2B_callwait_summary <- part2B_callwait_results %>%
summarise(median_wait = median(wait_time_call),
mean_wait = mean(wait_time_call),
var_wait = var(wait_time_call),
min_wait = min(wait_time_call),
max_wait = max(wait_time_call),
Qtile25th_wait = quantile(wait_time_call, probs = 0.25),
Qtile75th_wait = quantile(wait_time_call, probs = 0.75),
countunfinished = sum(is.na(call_end) & is.na(reneged)),
countfinished = sum(!is.na(call_end) | !is.na(reneged)))
part2B_callwait_summary
median_wait | mean_wait | var_wait | min_wait | max_wait | Qtile25th_wait | Qtile75th_wait | countunfinished | countfinished |
---|---|---|---|---|---|---|---|---|
3.825883 | 3.183036 | 3.479728 | 0 | 5 | 1.464796 | 5 | 0 | 1726 |
Now we are going to run our model 50 times and analyse different scenarios.
Use lapply to loop through and replicate the environment you created in Part 2 50 times.
envs_baseline <- lapply(1:50, function(i) {
set.seed(i)
simmer("baseline") %>%
add_resource("receptionist", capacity = sch_receptionists) %>%
add_generator("patient[8am-10am]", patient_flow2,
from_to(t_hour(8), t_hour(10), dist_patient_interarrival1, every = t_day(1)),
mon = 2) %>%
add_generator("patient[10am-4pm]", patient_flow2,
from_to(t_hour(10), t_hour(16), dist_patient_interarrival2, every = t_day(1)),
mon = 2) %>%
add_generator("patient[4pm-6pm]", patient_flow2,
from_to(t_hour(16), t_hour(18), dist_patient_interarrival3, every = t_day(1)),
mon = 2) %>%
run(t_day(5))
})
(Note mclapply function from the ‘parallel’ package could also be used to run each simulation in parallel, using the available cores in the computer’s processor. See here for an example: https://r-simmer.org/articles/simmer-04-bank-1.html)
Update some of the code used to look at results in parts 1 and 2. This time group_by(replication) before we summarise. E.g count of the reneged, then ungroup(). This will give the result for each replication. It is these that are then summarised again to give the final results for i.e. the mean of the mean number reneged in each replication.
Count reneged
envs_baseline %>%
get_mon_attributes() %>%
filter(key == "reneged") %>%
group_by(replication) %>%
summarise(countreneged = n()) %>%
ungroup() %>%
summarise(countreneged_mean = mean(countreneged))
countreneged_mean |
---|
579.86 |
n=580
Count F2F
envs_baseline %>%
get_mon_attributes() %>%
filter(key == "booked_appt", value == 2) %>%
group_by(replication) %>%
summarise(countF2F = n()) %>%
ungroup() %>%
summarise(countF2F_mean = mean(countF2F))
countF2F_mean |
---|
143.04 |
n=143
Before we look at call waits let’s understand a little more about unfinished. Using the staff schedule means that the final staff member finishes at 6pm on the 5th day so at (42460)+(18*60)= 6840, there are 4 in the system, 1 patient in progress and 3 waiting the staff member finishes that one at 6845 and then leaves with 3 remaining unfinished a similar thing occurs at the end of day 3.
get_mon_resources(envs_baseline) %>%
filter(replication == 1) %>%
tail(n = 10) # show last 10 rows
resource | time | server | queue | capacity | queue_size | system | limit | replication | |
---|---|---|---|---|---|---|---|---|---|
4184 | receptionist | 6824.155 | 1 | 4 | 1 | Inf | 5 | Inf | 1 |
4185 | receptionist | 6827.207 | 1 | 5 | 1 | Inf | 6 | Inf | 1 |
4186 | receptionist | 6829.155 | 1 | 4 | 1 | Inf | 5 | Inf | 1 |
4187 | receptionist | 6829.959 | 1 | 3 | 1 | Inf | 4 | Inf | 1 |
4188 | receptionist | 6832.803 | 1 | 4 | 1 | Inf | 5 | Inf | 1 |
4189 | receptionist | 6835.244 | 1 | 3 | 1 | Inf | 4 | Inf | 1 |
4190 | receptionist | 6835.407 | 1 | 4 | 1 | Inf | 5 | Inf | 1 |
4191 | receptionist | 6839.038 | 1 | 3 | 1 | Inf | 4 | Inf | 1 |
4192 | receptionist | 6840.000 | 1 | 3 | 0 | Inf | 4 | Inf | 1 |
4193 | receptionist | 6845.235 | 0 | 3 | 0 | Inf | 3 | Inf | 1 |
get_mon_resources(envs_baseline) %>%
filter(replication == 1, time < 6240) %>%
tail(n = 10)
resource | time | server | queue | capacity | queue_size | system | limit | replication | |
---|---|---|---|---|---|---|---|---|---|
3326 | receptionist | 5387.154 | 1 | 4 | 1 | Inf | 5 | Inf | 1 |
3327 | receptionist | 5387.827 | 1 | 3 | 1 | Inf | 4 | Inf | 1 |
3328 | receptionist | 5391.761 | 1 | 4 | 1 | Inf | 5 | Inf | 1 |
3329 | receptionist | 5392.154 | 1 | 3 | 1 | Inf | 4 | Inf | 1 |
3330 | receptionist | 5392.374 | 1 | 4 | 1 | Inf | 5 | Inf | 1 |
3331 | receptionist | 5392.431 | 1 | 5 | 1 | Inf | 6 | Inf | 1 |
3332 | receptionist | 5393.999 | 1 | 4 | 1 | Inf | 5 | Inf | 1 |
3333 | receptionist | 5397.431 | 1 | 3 | 1 | Inf | 4 | Inf | 1 |
3334 | receptionist | 5400.000 | 1 | 3 | 0 | Inf | 4 | Inf | 1 |
3335 | receptionist | 5401.615 | 0 | 3 | 0 | Inf | 3 | Inf | 1 |
We also need to consider duplicates in our results.
envs_baseline %>%
get_mon_arrivals(ongoing = TRUE) %>% # 101972 rows
filter(replication == 1, is.na(end_time)) %>%
head(n = 10)
name | start_time | end_time | activity_time | finished | replication |
---|---|---|---|---|---|
patient[4pm-6pm]212 | -1.000 | NA | NA | FALSE | 1 |
patient[4pm-6pm]201 | 6790.286 | NA | NA | FALSE | 1 |
patient[4pm-6pm]192 | 6775.585 | NA | NA | FALSE | 1 |
patient[4pm-6pm]191 | 6774.405 | NA | NA | FALSE | 1 |
patient[10am-4pm]1218 | -1.000 | NA | NA | FALSE | 1 |
patient[8am-10am]653 | -1.000 | NA | NA | FALSE | 1 |
Now considering this let’s calculate the call waits:
baseline_attributes <- envs_baseline %>%
get_mon_attributes() %>% #202732 obs
dplyr::select(-time) %>%
pivot_wider(names_from = "key", values_from = "value", values_fill = 0)
#101468 obs
baseline_callwait_results <- envs_baseline %>%
get_mon_arrivals(ongoing = TRUE) %>%
distinct() %>% # goes down to 101618, removes duplicates of unfinished
# still more in arrivals than attributes as attributes only has finished
full_join(baseline_attributes, by = c("name", "replication")) %>%
filter(via_ecr == 0) %>% # phone only also removes those yet to start e.g. start_time -1
mutate(wait_time_call = case_when(
# the caller reneged
reneged == 1 ~ 5,
# unfinished still in the system,
!finished ~ t_day(5) - start_time,
# everything else, (assume has been answered)
TRUE ~ pmax(end_time - start_time - activity_time, 0)
))
## calculate summary statistics
baseline_callwait_replication_summary <- baseline_callwait_results %>%
group_by(replication) %>%
summarise(median_wait = median(wait_time_call),
mean_wait = mean(wait_time_call),
var_wait = var(wait_time_call),
min_wait = min(wait_time_call),
max_wait = max(wait_time_call),
Qtile25th_wait = quantile(wait_time_call, probs = 0.25),
Qtile75th_wait = quantile(wait_time_call, probs = 0.75),
countunfinished = sum(!finished),
countfinished = sum(finished)) %>%
mutate(scenario = "baseline")
# show top 10 rows
head(baseline_callwait_replication_summary, n = 10)
replication | median_wait | mean_wait | var_wait | min_wait | max_wait | Qtile25th_wait | Qtile75th_wait | countunfinished | countfinished | scenario |
---|---|---|---|---|---|---|---|---|---|---|
1 | 3.825883 | 3.183036 | 3.479728 | 0 | 5 | 1.464796 | 5 | 0 | 1726 | baseline |
2 | 3.744117 | 3.221769 | 3.258414 | 0 | 5 | 1.555424 | 5 | 0 | 1770 | baseline |
3 | 3.775021 | 3.116034 | 3.682359 | 0 | 5 | 1.206570 | 5 | 0 | 1772 | baseline |
4 | 3.952666 | 3.235613 | 3.446565 | 0 | 5 | 1.535425 | 5 | 0 | 1722 | baseline |
5 | 3.810451 | 3.201863 | 3.433046 | 0 | 5 | 1.446567 | 5 | 0 | 1736 | baseline |
6 | 3.550199 | 3.053321 | 3.688564 | 0 | 5 | 1.141632 | 5 | 0 | 1699 | baseline |
7 | 3.525437 | 3.005393 | 3.874786 | 0 | 5 | 1.005038 | 5 | 0 | 1652 | baseline |
8 | 3.808467 | 3.141373 | 3.616938 | 0 | 5 | 1.237069 | 5 | 0 | 1733 | baseline |
9 | 3.892609 | 3.168985 | 3.635974 | 0 | 5 | 1.225592 | 5 | 0 | 1750 | baseline |
10 | 3.623343 | 3.068928 | 3.714392 | 0 | 5 | 1.107349 | 5 | 0 | 1662 | baseline |
baseline_callwait_summary <- baseline_callwait_replication_summary %>%
summarise(median_wait = mean(median_wait),
mean_wait = mean(mean_wait),
var_wait = mean(var_wait),
min_wait = mean(min_wait),
max_wait = mean(max_wait),
Qtile25th_wait = mean(Qtile25th_wait),
Qtile75th_wait = mean(Qtile75th_wait),
countunfinished = mean(countunfinished),
countfinished = mean(countfinished)) %>%
mutate(scenario = "baseline") # to identify it later
baseline_callwait_summary
median_wait | mean_wait | var_wait | min_wait | max_wait | Qtile25th_wait | Qtile75th_wait | countunfinished | countfinished | scenario |
---|---|---|---|---|---|---|---|---|---|
3.765179 | 3.147497 | 3.580269 | 0 | 5 | 1.334405 | 5 | 0 | 1722.8 | baseline |
If a neighbouring practice closed leading to an increase in demand of 45%. Whilst the practice were able to better advertise the ECR route to patients such that 30% of all the patients (old and new) were able to use this route. What impact would this have on the call wait and reneging of patients?
Create a new distribution for the contact mode with 30% ECR and use this in a new version of the trajectory for this scenario.
dist_contact_mode_S <- function() sample(1:2, 1, FALSE, c(0.3, 0.6))
patient_flow3 <- trajectory("patient flow3") %>%
branch(dist_contact_mode_S, TRUE, # <- changed to the scenario distribution
branch_ecr,
branch_phone2) %>%
timeout(dist_decision) %>%
branch(dist_booking_type, TRUE,
branch_booking_none,
branch_booking_phone,
branch_booking_f2f)
Create scenario interarrival rates 1.45x higher than original by multiplying the number expected upward
dist_patient_interarrival1_S <- function() rexp(1, (120 * 1.45) / t_hour(2))
dist_patient_interarrival2_S <- function() rexp(1, (240 * 1.45) / t_hour(6))
dist_patient_interarrival3_S <- function() rexp(1, (40 * 1.45) / t_hour(2))
###Scenario replications
Similar to the code used for the replications of the baseline, run the new scenario trajectory 50 times. We lapply as before but using the trajectory for the scenario (patient_flow3) and increasing frequency of arrivals using the scenarios interarrival distributions.
envs_scenario <- lapply(1:50, function(i) {
set.seed(i)
simmer("scenario") %>%
add_resource("receptionist", capacity = sch_receptionists) %>%
add_generator("patient[8am-10am]", patient_flow3,
from_to(t_hour(8), t_hour(10), dist_patient_interarrival1_S, every = t_day(1)),
mon = 2) %>%
add_generator("patient[10am-4pm]", patient_flow3,
from_to(t_hour(10), t_hour(16), dist_patient_interarrival2_S, every = t_day(1)),
mon = 2) %>%
add_generator("patient[4pm-6pm]", patient_flow3,
from_to(t_hour(16), t_hour(18), dist_patient_interarrival3_S, every = t_day(1)),
mon = 2) %>%
run(t_day(5))
})
Repurpose some of the code used to look at results in parts 1 and 2 of the workshop. Remembering to store the results with a different name to the baseline results.
envs_scenario %>%
get_mon_attributes() %>%
filter(key == "reneged") %>%
group_by(replication) %>%
summarise(countreneged = n()) %>%
ungroup() %>%
summarise(countreneged_mean = mean(countreneged))
countreneged_mean |
---|
752.14 |
n=752
envs_scenario %>%
get_mon_attributes() %>%
filter(key == "booked_appt", value == 2) %>%
group_by(replication) %>%
summarise(countF2F = n()) %>%
ungroup() %>%
summarise(countF2F_mean = mean(countF2F))
countF2F_mean |
---|
164.7 |
n=143
scenario_attributes <- envs_scenario %>%
get_mon_attributes() %>%
dplyr::select(-time) %>%
pivot_wider(names_from = "key", values_from = "value", values_fill = 0)
scenario_callwait_results <- envs_scenario %>%
get_mon_arrivals(ongoing = TRUE) %>%
distinct() %>%
full_join(scenario_attributes, by = c("name", "replication")) %>%
filter(via_ecr == 0) %>%
mutate(wait_time_call = case_when(
# the caller reneged
reneged == 1 ~ 5,
# unfinished still in the system,
!finished ~ t_day(5) - start_time,
# everything else, (assume has been answered)
TRUE ~ pmax(end_time - start_time - activity_time, 0)
))
## calculate summary statistics
scenario_callwait_replication_summary <- scenario_callwait_results %>%
group_by(replication) %>%
summarise(median_wait = median(wait_time_call),
mean_wait = mean(wait_time_call),
var_wait = var(wait_time_call),
min_wait = min(wait_time_call),
max_wait = max(wait_time_call),
Qtile25th_wait = quantile(wait_time_call, probs = 0.25),
Qtile75th_wait = quantile(wait_time_call, probs = 0.75),
countunfinished = sum(!finished),
countfinished = sum(finished)) %>%
mutate(scenario = "scenario")
scenario_callwait_summary <- scenario_callwait_replication_summary %>%
summarise(median_wait = mean(median_wait),
mean_wait = mean(mean_wait),
var_wait = mean(var_wait),
min_wait = mean(min_wait),
max_wait = mean(max_wait),
Qtile25th_wait = mean(Qtile25th_wait),
Qtile75th_wait = mean(Qtile75th_wait),
countunfinished = mean(countunfinished),
countfinished = mean(countfinished)) %>%
mutate(scenario = "scenario")
# show top 10 results
head(scenario_callwait_summary, n = 10)
median_wait | mean_wait | var_wait | min_wait | max_wait | Qtile25th_wait | Qtile75th_wait | countunfinished | countfinished | scenario |
---|---|---|---|---|---|---|---|---|---|
4.308835 | 3.540098 | 2.798809 | 0.0006983 | 5 | 2.146385 | 5 | 0 | 1946.42 | scenario |
Use t.test()
function to compare waiting times between baseline and scenario and see if there is a significant difference in the results.
Version taught in slidepack:
t.test(scenario_callwait_results$wait_time_call, baseline_callwait_results$wait_time_call)
##
## Welch Two Sample t-test
##
## data: scenario_callwait_results$wait_time_call and baseline_callwait_results$wait_time_call
## t = 46.783, df = 173168, p-value < 2.2e-16
## alternative hypothesis: true difference in means is not equal to 0
## 95 percent confidence interval:
## 0.3760273 0.4089121
## sample estimates:
## mean of x mean of y
## 3.541733 3.149264
This could actually be a paired t-test because they use the same common random number set and have the same number of replications therefore replication 1 of baseline and scenario are correlated (unless the scenario itself changes the andom number sampling).
We should check the variances of the differences is less than the individual variances before using paired t test. To do this, we need to:
part3_summary_byreplication <- bind_rows(
baseline_callwait_replication_summary,
scenario_callwait_replication_summary
)
d <- with(
part3_summary_byreplication,
mean_wait[scenario == "baseline"] - mean_wait[scenario == "scenario"]
)
shapiro.test(d) # => p-value = 0.6375
##
## Shapiro-Wilk normality test
##
## data: d
## W = 0.98197, p-value = 0.6375
The p-value (0.6375) is greater than the significance level 0.05 implying that the distribution of the differences (d) are not significantly different from normal distribution. therefore we do a paired T-Test.
res <- t.test(mean_wait ~ scenario, data = part3_summary_byreplication, paired = TRUE)
res
##
## Paired t-test
##
## data: mean_wait by scenario
## t = -26.092, df = 49, p-value < 2.2e-16
## alternative hypothesis: true difference in means is not equal to 0
## 95 percent confidence interval:
## -0.4228384 -0.3623634
## sample estimates:
## mean of the differences
## -0.3926009
The p-value of the test is < 2.2e-16, which is less than the significance level, alpha = 0.05. We can reject the null hypothesis and conclude that the average wait in the baseline is significantly different from the average wait in the scenario.
Using the ggplot2 package and geom_boxplot(), create boxplots of the means of the replications of the call waiting time in the baseline compared to the scenario. Set theme and title if you wish. To compare boxplots, you can either
a <- ggplot(baseline_callwait_replication_summary) +
geom_boxplot(aes(x = scenario, y = mean_wait),
outlier.shape = 19,
width = 0.3,
fill = "white",
colour = "orange") +
labs(y = "mean call wait (mins)") +
ylim(0, 4) +
theme(axis.title.x = element_blank(),
axis.text = element_text(size = 11))
b <- ggplot(scenario_callwait_replication_summary) +
geom_boxplot(aes(x = scenario, y = mean_wait),
outlier.shape = 19,
width = 0.3,
fill = "white",
colour = "orange") +
labs(y = "mean call wait (mins)") +
ylim(0, 4) +
theme(axis.title.x = element_blank(),
axis.text = element_text(size = 11))
grid.arrange(a, b, ncol = 2, nrow = 1, top = "Call wait mean results of baseline(left) and scenario(right)")
part3_summary <- bind_rows(baseline_callwait_summary, scenario_callwait_summary)
# plot
g <- ggplot() +
geom_boxplot(data = part3_summary_byreplication,
aes(x = scenario, y = mean_wait),
outlier.shape = 19,
width = 0.3,
fill = "white",
colour = "orange") +
geom_point(data = part3_summary,
aes(x = scenario, y = mean_wait),
shape = 19,
size = 2,
colour = "red") +
labs(x = "scenario", y = "mean call wait (mins)", title = "Boxplot of mean call wait from 50 replications") +
theme(axis.text = element_text(size = 11))
g
Q1. Looking at the results of baseline of 50 replications, what can you say about the results?
Q2. What do you interpret from the results of the scenario in comparison to the baseline?
Example if you want to compare ECR to Phones waits you might do it this way:
baseline_wait_results <- envs_baseline %>%
get_mon_arrivals(ongoing = TRUE) %>%
distinct() %>%
full_join(baseline_attributes, by = c("name", "replication")) %>%
filter(start_time >= 0) %>% # so use this to remove the not started e.g. start_time -1
mutate(wait_time_call = case_when(
reneged == 1 ~ 5,
!finished ~ t_day(5) - start_time,
TRUE ~ pmax(end_time - start_time - activity_time, 0)
))
baseline_wait_replication_summary <- baseline_wait_results %>%
group_by(replication, via_ecr) %>% #group by both of these to get results for phone and ECR
summarise(median_wait = median(wait_time_call),
mean_wait = mean(wait_time_call),
var_wait = var(wait_time_call),
min_wait = min(wait_time_call),
max_wait = max(wait_time_call),
Qtile25th_wait = quantile(wait_time_call, probs = 0.25),
Qtile75th_wait = quantile(wait_time_call, probs = 0.75),
countunfinished = sum(!finished),
countfinished = sum(finished)) %>%
mutate(scenario = "baseline")
## `summarise()` has grouped output by 'replication'. You can override using the `.groups` argument.
# show top 10 results
head(baseline_wait_replication_summary, n = 10)
replication | via_ecr | median_wait | mean_wait | var_wait | min_wait | max_wait | Qtile25th_wait | Qtile75th_wait | countunfinished | countfinished | scenario |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | 0 | 3.825883 | 3.183036 | 3.479728 | 0 | 5.000 | 1.464796 | 5.00000 | 0 | 1726 | baseline |
1 | 1 | 56.571437 | 121.432726 | 60680.123013 | 0 | 1129.364 | 14.716072 | 99.92010 | 3 | 354 | baseline |
2 | 0 | 3.744117 | 3.221769 | 3.258414 | 0 | 5.000 | 1.555424 | 5.00000 | 0 | 1770 | baseline |
2 | 1 | 65.218754 | 149.913667 | 69329.798636 | 0 | 1080.604 | 15.965540 | 149.12634 | 1 | 291 | baseline |
3 | 0 | 3.775021 | 3.116034 | 3.682359 | 0 | 5.000 | 1.206570 | 5.00000 | 0 | 1772 | baseline |
3 | 1 | 32.526169 | 64.839190 | 22473.421606 | 0 | 1075.412 | 5.002446 | 64.15030 | 6 | 314 | baseline |
4 | 0 | 3.952666 | 3.235613 | 3.446565 | 0 | 5.000 | 1.535425 | 5.00000 | 0 | 1722 | baseline |
4 | 1 | 41.584256 | 83.401127 | 35084.985697 | 0 | 1099.012 | 12.369562 | 73.55387 | 1 | 303 | baseline |
5 | 0 | 3.810451 | 3.201863 | 3.433046 | 0 | 5.000 | 1.446567 | 5.00000 | 0 | 1736 | baseline |
5 | 1 | 40.110651 | 76.963237 | 27211.750428 | 0 | 1008.028 | 9.223911 | 84.78485 | 0 | 299 | baseline |
baseline_wait_summary <- baseline_wait_replication_summary %>%
group_by(via_ecr) %>%
summarise(median_wait = mean(median_wait),
mean_wait = mean(mean_wait),
var_wait = mean(var_wait),
min_wait = mean(min_wait),
max_wait = mean(max_wait),
Qtile25th_wait = mean(Qtile25th_wait),
Qtile75th_wait = mean(Qtile75th_wait),
countunfinished = mean(countunfinished),
countfinished = mean(countfinished))
baseline_wait_summary
via_ecr | median_wait | mean_wait | var_wait | min_wait | max_wait | Qtile25th_wait | Qtile75th_wait | countunfinished | countfinished |
---|---|---|---|---|---|---|---|---|---|
0 | 3.765179 | 3.147497 | 3.580269 | 0 | 5.000 | 1.334405 | 5.00000 | 0.00 | 1722.80 |
1 | 42.015194 | 94.007905 | 41412.497885 | 0 | 1052.733 | 10.070932 | 85.24734 | 4.08 | 302.48 |
For example, plot results from all replications
plot(get_mon_arrivals(envs_baseline), metric = "waiting_time")
## `geom_smooth()` using method = 'gam' and formula 'y ~ s(x, bs = "cs")'
## Warning: Removed 1062 rows containing non-finite values (stat_smooth).
## Warning: Removed 1062 row(s) containing missing values (geom_path).
plot(get_mon_arrivals(envs_scenario), metric = "waiting_time")
## `geom_smooth()` using method = 'gam' and formula 'y ~ s(x, bs = "cs")'
## Warning: Removed 27318 rows containing non-finite values (stat_smooth).
## Warning: Removed 27318 row(s) containing missing values (geom_path).
We could produce a chart showing the mean call wait from each of the 50 replications.
ggplot(part3_summary_byreplication, aes(mean_wait)) +
geom_histogram(fill = "orange") +
labs(title = "Distribution of average call wait in 50 replications") +
facet_wrap(vars(scenario), ncol = 2)
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
To include the set_attribute using now() we reference the correct environment for each replication
envs_baselineB <- lapply(1:50, function(i) {
assign(paste("env", i[1], sep = "_"), simmer("baselineB"))
get(paste0("env_", i))
# change references to env
renege_hungup3B <- trajectory("hung up") %>%
set_attribute("reneged", 1) %>%
set_attribute("reneged_at", function() now(get(paste0("env_", i)))) %>% # changed to env_part2B
log_("lost my patience. Hanging up...")
branch_phone3B <- trajectory("branch phone") %>%
set_prioritization(c(1, 1, FALSE)) %>% # set to increase priority from default of 0 to 1, no preempt and no restart
set_attribute("via_ecr", 0) %>%
set_attribute("arrival", function() now(get(paste0("env_", i)))) %>% # changed to env_part2B
renege_in(t = 5, out = renege_hungup) %>%
seize("receptionist") %>%
set_attribute("call_taken", function() now(get(paste0("env_", i)))) %>% # changed to env_part2B
renege_abort() %>%
timeout(dist_screening)
branch_booking_none3B <- trajectory("no appointment needed") %>%
release("receptionist") %>%
set_attribute("booked_appt", 0) %>%
set_attribute("call_end", function() now(get(paste0("env_", i)))) # changed to env_part2B
branch_booking_phone3B <- trajectory("booking a f2f") %>%
timeout(dist_booking) %>%
release("receptionist") %>%
set_attribute("booked_appt", 1) %>%
set_attribute("call_end", function() now(get(paste0("env_", i)))) # changed to env_part2B
branch_booking_f2f3B <- trajectory("booking a f2f") %>%
timeout(dist_booking) %>%
release("receptionist") %>%
set_attribute("booked_appt", 2) %>%
set_attribute("call_end", function() now(get(paste0("env_", i)))) # changed to env_part2B
# Overall trajectory:
patient_flow3B <- trajectory("patient flow3B") %>%
branch(dist_contact_mode, TRUE,
branch_ecr,
branch_phone3B) %>%
timeout(dist_decision) %>%
branch(dist_booking_type, TRUE,
branch_booking_none3B,
branch_booking_phone3B,
branch_booking_f2f3B)
set.seed(i)
get(paste0("env_", i)) %>%
add_resource("receptionist", capacity = sch_receptionists) %>%
add_generator("patient[8am-10am]", patient_flow3B,
from_to(t_hour(8), t_hour(10), dist_patient_interarrival1, every = t_day(1)),
mon = 2) %>%
add_generator("patient[10am-4pm]", patient_flow3B,
from_to(t_hour(10), t_hour(16), dist_patient_interarrival2, every = t_day(1)),
mon = 2) %>%
add_generator("patient[4pm-6pm]", patient_flow3B,
from_to(t_hour(16), t_hour(18), dist_patient_interarrival3, every = t_day(1)),
mon = 2) %>%
run(t_day(5))
})
We can now analyse results
envs_baselineB %>%
get_mon_attributes() %>%
filter(key == "reneged") %>%
group_by(replication) %>%
summarise(countreneged = n()) %>%
ungroup() %>%
summarise(countreneged_mean = mean(countreneged))
countreneged_mean |
---|
579.86 |
n=580
envs_baselineB %>%
get_mon_attributes() %>%
filter(key == "booked_appt", value == 2) %>%
group_by(replication) %>%
summarise(countF2F = n()) %>%
ungroup() %>%
summarise(countF2F_mean = mean(countF2F))
countF2F_mean |
---|
143.04 |
n=143
baselineB_callwait_results <- envs_baselineB %>%
get_mon_attributes() %>%
pivot_wider(-c(time), names_from = key, values_from = value) %>%
filter(via_ecr == 0, arrival >= 0) %>%
mutate(wait_time_call = case_when(
!is.na(reneged) ~ 5,
is.na(call_taken) ~ t_day(5) - arrival,
TRUE ~ call_taken - arrival
))
baselineB_callwait_replication_summary <- baselineB_callwait_results %>%
group_by(replication) %>%
summarise(median_wait = median(wait_time_call),
mean_wait = mean(wait_time_call),
var_wait = var(wait_time_call),
min_wait = min(wait_time_call),
max_wait = max(wait_time_call),
Qtile25th_wait = quantile(wait_time_call, probs = 0.25),
Qtile75th_wait = quantile(wait_time_call, probs = 0.75),
countunfinished = sum(is.na(call_end) & is.na(reneged)),
countfinished = sum(!is.na(call_end) | !is.na(reneged)))
# show top 10 results
head(baselineB_callwait_replication_summary, n = 10)
replication | median_wait | mean_wait | var_wait | min_wait | max_wait | Qtile25th_wait | Qtile75th_wait | countunfinished | countfinished |
---|---|---|---|---|---|---|---|---|---|
1 | 3.825883 | 3.183036 | 3.479728 | 0 | 5 | 1.464796 | 5 | 0 | 1726 |
2 | 3.744117 | 3.221769 | 3.258414 | 0 | 5 | 1.555424 | 5 | 0 | 1770 |
3 | 3.775021 | 3.116034 | 3.682359 | 0 | 5 | 1.206570 | 5 | 0 | 1772 |
4 | 3.952666 | 3.235613 | 3.446565 | 0 | 5 | 1.535425 | 5 | 0 | 1722 |
5 | 3.810451 | 3.201863 | 3.433046 | 0 | 5 | 1.446567 | 5 | 0 | 1736 |
6 | 3.550199 | 3.053321 | 3.688564 | 0 | 5 | 1.141632 | 5 | 0 | 1699 |
7 | 3.525437 | 3.005393 | 3.874786 | 0 | 5 | 1.005038 | 5 | 0 | 1652 |
8 | 3.808467 | 3.141373 | 3.616938 | 0 | 5 | 1.237069 | 5 | 0 | 1733 |
9 | 3.892609 | 3.168985 | 3.635974 | 0 | 5 | 1.225592 | 5 | 0 | 1750 |
10 | 3.623343 | 3.068928 | 3.714392 | 0 | 5 | 1.107349 | 5 | 0 | 1662 |
baselineB_callwait_summary <- baselineB_callwait_replication_summary %>%
summarise(median_wait = mean(median_wait),
mean_wait = mean(mean_wait),
var_wait = mean(var_wait),
min_wait = mean(min_wait),
max_wait = mean(max_wait),
Qtile25th_wait = mean(Qtile25th_wait),
Qtile75th_wait = mean(Qtile75th_wait),
countunfinished = mean(countunfinished),
countfinished = mean(countfinished)) %>%
mutate(scenario = "baseline") # to identify it later
baselineB_callwait_summary
median_wait | mean_wait | var_wait | min_wait | max_wait | Qtile25th_wait | Qtile75th_wait | countunfinished | countfinished | scenario |
---|---|---|---|---|---|---|---|---|---|
3.765179 | 3.147497 | 3.580269 | 0 | 5 | 1.334405 | 5 | 0 | 1722.8 | baseline |
baselineB_callwait_summary$mean_wait
## [1] 3.147497
baseline_callwait_summary$mean_wait
## [1] 3.147497
Trajectory must be included inside the lapply for replication
envs_scenarioB <- lapply(1:50, function(i) {
assign(paste("env", i[1], sep = "_"), simmer("scenarioB"))
get(paste0("env_", i))
# change references to env
renege_hungup3B <- trajectory("hung up") %>%
set_attribute("reneged", 1) %>%
set_attribute("reneged_at", function() now(get(paste0("env_", i)))) %>% # changed to env_part2B
log_("lost my patience. Hanging up...")
branch_phone3B <- trajectory("branch phone") %>%
set_prioritization(c(1, 1, FALSE)) %>% # set to increase priority from default of 0 to 1, no preempt and no restart
set_attribute("via_ecr", 0) %>%
set_attribute("arrival", function() now(get(paste0("env_", i)))) %>% # changed to env_part2B
renege_in(t = 5, out = renege_hungup) %>%
seize("receptionist") %>%
set_attribute("call_taken", function() now(get(paste0("env_", i)))) %>% # changed to env_part2B
renege_abort() %>%
timeout(dist_screening)
branch_booking_none3B <- trajectory("no appointment needed") %>%
release("receptionist") %>%
set_attribute("booked_appt", 0) %>%
set_attribute("call_end", function() now(get(paste0("env_", i)))) # changed to env_part2B
branch_booking_phone3B <- trajectory("booking a f2f") %>%
timeout(dist_booking) %>%
release("receptionist") %>%
set_attribute("booked_appt", 1) %>%
set_attribute("call_end", function() now(get(paste0("env_", i)))) # changed to env_part2B
branch_booking_f2f3B <- trajectory("booking a f2f") %>%
timeout(dist_booking) %>%
release("receptionist") %>%
set_attribute("booked_appt", 2) %>%
set_attribute("call_end", function() now(get(paste0("env_", i)))) # changed to env_part2B
# Overall trajectory:
# Create a new patient flow trajectory using the new contact mode distribution
patient_flow3BS <- trajectory("patient flow3BS") %>%
branch(dist_contact_mode_S, TRUE, # <-changed to the scenario distribution
branch_ecr,
branch_phone3B) %>%
timeout(dist_decision) %>%
branch(dist_booking_type, TRUE,
branch_booking_none3B,
branch_booking_phone3B,
branch_booking_f2f3B)
set.seed(i)
get(paste0("env_", i)) %>%
add_resource("receptionist", capacity = sch_receptionists) %>%
add_generator("patient[8am-10am]", patient_flow3BS,
from_to(t_hour(8), t_hour(10), dist_patient_interarrival1_S, every = t_day(1)),
mon = 2) %>%
add_generator("patient[10am-4pm]", patient_flow3BS,
from_to(t_hour(10), t_hour(16), dist_patient_interarrival2_S, every = t_day(1)),
mon = 2) %>%
add_generator("patient[4pm-6pm]", patient_flow3BS,
from_to(t_hour(16), t_hour(18), dist_patient_interarrival3_S, every = t_day(1)),
mon = 2) %>%
run(t_day(5))
})
We can now analyse results
envs_scenarioB %>%
get_mon_attributes() %>%
filter(key == "reneged") %>%
group_by(replication) %>%
summarise(countreneged = n()) %>%
ungroup() %>%
summarise(countreneged_mean = mean(countreneged))
countreneged_mean |
---|
752.14 |
n=752
envs_scenarioB %>%
get_mon_attributes() %>%
filter(key == "booked_appt", value == 2) %>%
group_by(replication) %>%
summarise(countF2F = n()) %>%
ungroup() %>%
summarise(countF2F_mean = mean(countF2F))
countF2F_mean |
---|
164.7 |
n=165
scenarioB_callwait_results <- envs_scenarioB %>%
get_mon_attributes() %>%
pivot_wider(-c(time), names_from = key, values_from = value) %>%
filter(via_ecr == 0, arrival >= 0) %>%
mutate(wait_time_call = case_when(
!is.na(reneged) ~ 5,
is.na(call_taken) ~ t_day(5) - arrival,
TRUE ~ call_taken - arrival
))
scenarioB_callwait_replication_summary <- scenarioB_callwait_results %>%
group_by(replication) %>%
summarise(median_wait = median(wait_time_call),
mean_wait = mean(wait_time_call),
var_wait = var(wait_time_call),
min_wait = min(wait_time_call),
max_wait = max(wait_time_call),
Qtile25th_wait = quantile(wait_time_call, probs = 0.25),
Qtile75th_wait = quantile(wait_time_call, probs = 0.75),
countunfinished = sum(is.na(call_end) & is.na(reneged)),
countfinished = sum(!is.na(call_end) | !is.na(reneged)))
# show top 10 results
head(scenarioB_callwait_replication_summary, n = 10)
replication | median_wait | mean_wait | var_wait | min_wait | max_wait | Qtile25th_wait | Qtile75th_wait | countunfinished | countfinished |
---|---|---|---|---|---|---|---|---|---|
1 | 4.238399 | 3.491723 | 2.864142 | 0.0000000 | 5 | 2.083413 | 5 | 0 | 1891 |
2 | 4.180311 | 3.445853 | 2.978597 | 0.0000000 | 5 | 1.915787 | 5 | 0 | 1864 |
3 | 4.096406 | 3.461692 | 2.837873 | 0.0000000 | 5 | 2.014210 | 5 | 0 | 1911 |
4 | 4.272697 | 3.519810 | 2.804108 | 0.0000000 | 5 | 2.053628 | 5 | 0 | 1935 |
5 | 4.377080 | 3.580585 | 2.723323 | 0.0000000 | 5 | 2.319311 | 5 | 0 | 2006 |
6 | 4.466820 | 3.667720 | 2.552275 | 0.0000000 | 5 | 2.493847 | 5 | 0 | 2034 |
7 | 4.254043 | 3.511574 | 2.849837 | 0.0142518 | 5 | 2.089649 | 5 | 0 | 1899 |
8 | 4.343878 | 3.529024 | 2.851664 | 0.0000000 | 5 | 2.139716 | 5 | 0 | 1927 |
9 | 4.467159 | 3.585018 | 2.819673 | 0.0000000 | 5 | 2.217959 | 5 | 0 | 1964 |
10 | 4.356379 | 3.544891 | 2.773248 | 0.0000000 | 5 | 2.170278 | 5 | 0 | 1983 |
scenarioB_callwait_summary <- scenarioB_callwait_replication_summary %>%
summarise(median_wait = mean(median_wait),
mean_wait = mean(mean_wait),
var_wait = mean(var_wait),
min_wait = mean(min_wait),
max_wait = mean(max_wait),
Qtile25th_wait = mean(Qtile25th_wait),
Qtile75th_wait = mean(Qtile75th_wait),
countunfinished = mean(countunfinished),
countfinished = mean(countfinished)) %>%
mutate(scenario = "scenario") # to identify it later
scenarioB_callwait_summary
median_wait | mean_wait | var_wait | min_wait | max_wait | Qtile25th_wait | Qtile75th_wait | countunfinished | countfinished | scenario |
---|---|---|---|---|---|---|---|---|---|
4.308835 | 3.540098 | 2.798809 | 0.0006983 | 5 | 2.146385 | 5 | 0 | 1946.42 | scenario |
scenarioB_callwait_summary$mean_wait
## [1] 3.540098
scenario_callwait_summary$mean_wait
## [1] 3.540098
Q1. What other opportunities are there for improvement of the process?
Q2. What other scenarios might you want to consider with the practice? What would your performance indicators be? How would you incorporate them in the model? Have a go if you have time.
Examples might include staff rota, opening hours, percentage coming via ecr, include preemption and restart as a way of working
Q3. What else would need to be considered before they could implement changes suggested by the model?
E.g. costs, other tasks the staff do, feasibility of suggested rota in real life.
This process has been converted to R from: Simulations: The Practice of Model Development and Use, Robinson, S., 2nd Ed. 2014, Palgrave Macmillan
Remember back to our T test results we saved in res, these gave a confidence interval based on the student’s t distribution
res$conf.int[1]
## [1] -0.4228384
res$conf.int[2]
## [1] -0.3623634
We can use this to look the percent deviation of the confidence limits from the mean as the number of replications increases to help us determine accuracy of our mean from these.
Create a blank data frame to hold the cumulative results and then loop through:
check_baseline_replications <- data.frame(matrix(ncol = 5, nrow = 0))
x <- c("replication", "cmean", "tL", "tU", "percentdeviation")
colnames(check_baseline_replications) <- x
#loop through increasing the number of replications
for (i in 2:50) {
df <- baseline_callwait_replication_summary %>%
dplyr::select(replication, mean_wait) %>%
filter(replication <= i) %>%
summarise(replication = i,
cmean = mean(mean_wait),
tL = t.test(mean_wait)$conf.int[1],
tU = t.test(mean_wait)$conf.int[2],
percentdeviation = (cmean - tL) / cmean * 100) %>%
data.frame()
check_baseline_replications <- bind_rows(check_baseline_replications, df)
}
# show top 10 results
head(check_baseline_replications, n = 10)
replication | cmean | tL | tU | percentdeviation |
---|---|---|---|---|
2 | 3.202403 | 2.956327 | 3.448479 | 7.684104 |
3 | 3.173613 | 3.040728 | 3.306499 | 4.187198 |
4 | 3.189113 | 3.103887 | 3.274340 | 2.672417 |
5 | 3.191663 | 3.133636 | 3.249691 | 1.818099 |
6 | 3.168606 | 3.094869 | 3.242343 | 2.327118 |
7 | 3.145290 | 3.062986 | 3.227594 | 2.616748 |
8 | 3.144801 | 3.075910 | 3.213691 | 2.190611 |
9 | 3.147488 | 3.087915 | 3.207060 | 1.892702 |
10 | 3.139632 | 3.084423 | 3.194840 | 1.758443 |
11 | 3.153205 | 3.095464 | 3.210947 | 1.831200 |