Visualising projections
animated-charts.Rmd
A proven effective way of presenting the outputs from waiting list projections is using animated charts. These can be embedded in many documents, including htmls and powerpoint files. This vignette shows how to generate the charts from the functions provided in the NHSRtt package.
The following libraries are needed for this vignette:
Create the input data
The data used here is dummy data. The calibration period here is two years, and the forecast period is also two years. Validation is not performed here because the purpose of this vignette is simply to show how to create animations.
max_months_waited <- 12
calibration_periods <- 24
seed <- 599
Calibration
The calibration data are created and passed to the
calibrate_capacity_renege_params()
function.
referrals <- create_dummy_data(
type = "referral",
max_months_waited = max_months_waited,
number_periods = calibration_periods,
referral_values = 500:600,
seed = seed
)
incompletes <- create_dummy_data(
type = "incomplete",
max_months_waited = max_months_waited,
number_periods = calibration_periods,
max_incompletes = 500,
seed = seed
)
completes <- create_dummy_data(
type = "complete",
max_months_waited = max_months_waited,
number_periods = calibration_periods,
max_treatments = 20,
seed = seed
)
params <- calibrate_capacity_renege_params(
referrals = referrals,
incompletes = incompletes,
completes = completes,
max_months_waited = max_months_waited,
redistribute_m0_reneges = TRUE
)
Projections - input data
When projecting, it is good to have a variety of projections representing more and less severe scenarios. One way of projecting is using a time series method, such as ARIMA or TBATS. Here we show how to use TBATS, along with the uncertainty that is provided by the function.
For simplicity, the calibration data is re-used as projection data. The inputs to the function are 24 months of referrals, 24 months of treatments (completed pathways) and incomplete pathways for 1 time period.
timesteps <- 24
# this function helps with repeated data manipulation; it contains the ts
# function (which turns the data into a time series - where the 12 indicates
# that is it a monthly time series), and also the tbats function, which performs
# the time series analysis on the time series
forecast_function <- function(rtt_table, number_timesteps) {
fcast <- rtt_table |>
pull(value) |>
ts(frequency = 12) |>
forecast::tbats() |>
forecast::forecast(h = number_timesteps) |>
tidyr::as_tibble()
return(fcast)
}
projection_referrals <- referrals |>
rename(
value = referrals
) |>
forecast_function(
number_timesteps = timesteps
) |>
mutate(
type = "Referrals"
)
This creates a table that looks like this (only the top 15 rows are displayed). This table contains uncertainty in the projections.
Point Forecast | Lo 80 | Hi 80 | Lo 95 | Hi 95 | type |
---|---|---|---|---|---|
553.3873 | 525.5815 | 581.1932 | 510.8620 | 595.9127 | Referrals |
553.8094 | 525.8679 | 581.7509 | 511.0766 | 596.5422 | Referrals |
554.1981 | 526.1424 | 582.2539 | 511.2905 | 597.1058 | Referrals |
554.5562 | 526.4040 | 582.7083 | 511.5012 | 597.6112 | Referrals |
554.8859 | 526.6525 | 583.1193 | 511.7066 | 598.0652 | Referrals |
555.1896 | 526.8876 | 583.4916 | 511.9055 | 598.4738 | Referrals |
555.4693 | 527.1095 | 583.8292 | 512.0967 | 598.8420 | Referrals |
555.7270 | 527.3183 | 584.1356 | 512.2797 | 599.1743 | Referrals |
555.9642 | 527.5144 | 584.4141 | 512.4539 | 599.4745 | Referrals |
556.1828 | 527.6981 | 584.6674 | 512.6193 | 599.7462 | Referrals |
556.3840 | 527.8701 | 584.8980 | 512.7757 | 599.9924 | Referrals |
556.5694 | 528.0307 | 585.1081 | 512.9232 | 600.2156 | Referrals |
556.7401 | 528.1806 | 585.2997 | 513.0620 | 600.4182 | Referrals |
556.8974 | 528.3202 | 585.4746 | 513.1924 | 600.6024 | Referrals |
557.0422 | 528.4502 | 585.6342 | 513.3145 | 600.7699 | Referrals |
This is repeated for the number of treatments and the incomplete pathways.
projection_complete <- completes |>
summarise(
value = sum(treatments),
.by = period_id
) |>
forecast_function(
number_timesteps = timesteps
) |>
mutate(
type = "Treatments"
)
projection_incomplete <- incompletes |>
rename(
value = incompletes
) |>
group_by(months_waited_id) |>
group_split() |>
lapply(
forecast_function,
number_timesteps = 1
) |>
setNames(nm = 0:12) |>
bind_rows(.id = "months_waited_id") |>
mutate(
months_waited_id = as.numeric(months_waited_id)
)
These inputs can be turned into scenarios. The more severe scenarios would be those with higher incomplete pathways and higher referrals combined with lower completed pathways. The less severe scenarios would be the opposite.
# most severe (severe_1)
severe_1_referrals <- projection_referrals |>
pull(`Hi 95`)
severe_1_treatments <- projection_complete |>
pull(`Lo 95`)
severe_1_incomplete <- projection_incomplete |>
select(
"months_waited_id",
incompletes = "Hi 95"
)
# 2nd most severe (severe_2)
severe_2_referrals <- projection_referrals |>
pull(`Hi 80`)
severe_2_treatments <- projection_complete |>
pull(`Lo 80`)
severe_2_incomplete <- projection_incomplete |>
select(
"months_waited_id",
incompletes = "Hi 80"
)
# average
average_referrals <- projection_referrals |>
pull(`Point Forecast`)
average_treatments <- projection_complete |>
pull(`Point Forecast`)
average_incomplete <- projection_incomplete |>
select(
"months_waited_id",
incompletes = "Point Forecast"
)
# 2nd mildest (mild_2)
mild_2_referrals <- projection_referrals |>
pull(`Lo 80`)
mild_2_treatments <- projection_complete |>
pull(`Hi 80`)
mild_2_incomplete <- projection_incomplete |>
select(
"months_waited_id",
incompletes = "Lo 80"
)
# Mildest (mild_1)
mild_1_referrals <- projection_referrals |>
pull(`Lo 95`)
mild_1_treatments <- projection_complete |>
pull(`Hi 95`)
mild_1_incomplete <- projection_incomplete |>
select(
"months_waited_id",
incompletes = "Lo 95"
)
Projections - forecasts
The inputs are passed to the
apply_params_to_projections()
function.
severe_1_projections <- apply_params_to_projections(
capacity_projections = severe_1_treatments,
referrals_projections = severe_1_referrals,
incomplete_pathways = severe_1_incomplete,
renege_capacity_params = params,
max_months_waited = max_months_waited
) |>
mutate(
scenario = "Most severe"
)
severe_2_projections <- apply_params_to_projections(
capacity_projections = severe_2_treatments,
referrals_projections = severe_2_referrals,
incomplete_pathways = severe_2_incomplete,
renege_capacity_params = params,
max_months_waited = max_months_waited
) |>
mutate(
scenario = "2nd most severe"
)
average_projections <- apply_params_to_projections(
capacity_projections = average_treatments,
referrals_projections = average_referrals,
incomplete_pathways = average_incomplete,
renege_capacity_params = params,
max_months_waited = max_months_waited
) |>
mutate(
scenario = "Average"
)
mild_2_projections <- apply_params_to_projections(
capacity_projections = mild_2_treatments,
referrals_projections = mild_2_referrals,
incomplete_pathways = mild_2_incomplete,
renege_capacity_params = params,
max_months_waited = max_months_waited
) |>
mutate(
scenario = "2nd mildest"
)
mild_1_projections <- apply_params_to_projections(
capacity_projections = mild_1_treatments,
referrals_projections = mild_1_referrals,
incomplete_pathways = mild_1_incomplete,
renege_capacity_params = params,
max_months_waited = max_months_waited
) |>
mutate(
scenario = "Mildest"
)
Visualisations
Inputs
Stakeholders are often interested in the inputs to understand the outputs better. These sections give some useful code for styling the data used in bespoke analysis.
pal_bands <- c(
'Middle' = '#6baed6',
'Inner' = '#08519c',
'Median' = '#FFFFFF'
)
bind_rows(
projection_complete,
projection_referrals
) |>
mutate(
period_id = row_number(),
.by = type
) |>
ggplot(
aes(x = period_id)
) +
geom_ribbon(
aes(
ymin = `Lo 80`,
ymax = `Hi 80`,
fill = 'Middle'
),
alpha = 0.6
) +
geom_ribbon(
aes(
ymin = `Lo 95`,
ymax = `Hi 95`,
fill = 'Inner'
),
alpha = 0.6
) +
geom_ribbon(
aes(
ymin = `Point Forecast`,
ymax = `Point Forecast`,
fill = 'Median'
),
color = 'black',
alpha = 0.6
) +
geom_line(
aes(
y = `Point Forecast`,
group = type
),
color = 'white',
linewidth = 1
) +
theme_bw() +
theme(plot.title = element_text(hjust = 0.5)) +
facet_wrap(
facets = vars(type),
scales = "free_y"
) +
labs(
title = "Input range of completes pathways and referrals by period",
x = "Period",
y = "Volume"
) +
scale_fill_manual(
name = "Interval",
breaks = c('Middle', 'Inner', 'Median'),
values = pal_bands,
labels = c(
'Middle' = '10% to 90%',
'Inner' = '2.5% to 97.5%',
'Median' = 'Median'
)
)
Static projections
A similar set of charts can be made for the waiting list projections.
bind_rows(
mild_1_projections,
mild_2_projections,
average_projections,
severe_2_projections,
severe_1_projections
) |>
select(
"period_id", "months_waited_id", "incompletes", "scenario"
) |>
pivot_wider(
names_from = scenario,
values_from = incompletes
) |>
rename(
"Lo 80" = "Mildest",
"Lo 95" = "2nd mildest",
"Median" = "Average",
"Hi 95" = "2nd most severe",
"Hi 80" = "Most severe"
) |>
mutate(
months_waited_id = paste(
months_waited_id,
"months waited"
),
months_waited_id = factor(
months_waited_id,
levels = paste(0:12, "months waited")
)
) |>
ggplot(
aes(x = period_id)
) +
geom_ribbon(
aes(
ymin = `Lo 80`,
ymax = `Hi 80`,
fill = 'Middle'
),
alpha = 0.6
) +
geom_ribbon(
aes(
ymin = `Lo 95`,
ymax = `Hi 95`,
fill = 'Inner'
),
alpha = 0.6
) +
geom_ribbon(
aes(
ymin = `Median`,
ymax = `Median`,
fill = 'Median'
),
color = 'black',
alpha = 0.6
) +
geom_line(
aes(
y = `Median`,
group = months_waited_id
),
color = 'white',
linewidth = 1
) +
theme_bw() +
theme(plot.title = element_text(hjust = 0.5)) +
facet_wrap(
facets = vars(months_waited_id),
scales = "free_y"
) +
labs(
title = "Incomplete pathways by wait time",
x = "Period",
y = "Volume"
) +
scale_fill_manual(
name = "Interval",
breaks = c('Middle', 'Inner', 'Median'),
values = pal_bands,
labels = c(
'Middle' = '10% to 90%',
'Inner' = '2.5% to 97.5%',
'Median' = 'Median'
)
)
Animated projections
This can also be displayed as a rolling animation.
bind_rows(
mild_1_projections,
mild_2_projections,
average_projections,
severe_2_projections,
severe_1_projections
) |>
select(
"period_id", "months_waited_id", "incompletes", "scenario"
) |>
mutate(
scenario = factor(
scenario,
levels = c(
"Mildest",
"2nd mildest",
"Average",
"2nd most severe",
"Most severe"
)
),
period_id = as.integer(period_id)
) |>
ggplot(
aes(
x = months_waited_id,
y = incompletes
)
) +
geom_col(
aes(
group = period_id
),
fill = "#08519c"
) +
theme_bw() +
theme(plot.title = element_text(hjust = 0.5)) +
labs(
title = "Incomplete pathways by number of months waited: period {frame_time}",
x = "Number of months waited",
y = "Number of incompete pathways"
) +
facet_wrap(
facets = vars(scenario)
) +
transition_time(
period_id
)