# Load in packages
library(tidyverse)
library(table1)
library(lubridate)
library(e1071)
library(labelled)
library(janitor)
library(readxl)
library(survival)
library(survminer)
library(officer)
library(ggthemes)
library(gt)TDM-TIME
A prospective cohort feasibility study of real-time beta-lactam antimicrobial therapeutic drug monitoring in critically ill patients with lower respiratory infection and development of an automated research notification tool: The TDM-TIME Study
Jan Hansel✉, Anthony Wilson, Jake Lain, Emmanuel Erhieyovwe, James Orr, Brian G. Keevil, Kayode Ogungbenro, Paul M. Dark, Timothy W. Felton
Division of Immunology, Immunity to Infection and Respiratory Medicine, The University of Manchester, Manchester, UK
Abstract
Background: Timely interventions are crucial for ICU patients with severe respiratory infections, particularly those with sepsis. Prompt initiation of antimicrobials is recommended to reduce mortality, but balancing this urgency with the risk of antimicrobial resistance is vital. Beta-lactam therapeutic drug monitoring (TDM) can be used to optimise dosing, but is limited by delays in delivering actionable results. In parallel, electronic health records (EHR) offer real-time operational support and can be used to identify patients eligible for trials. This study assessed the feasibility of improving turnaround times for beta-lactam TDM and enhancing recruitment into time-sensitive trials using novel EHR-based tools.
Methods: We conducted a prospective feasibility study in two ICUs in Manchester, UK, between December 2023 and June 2024. Adults receiving piperacillin/tazobactam or meropenem for respiratory infection were enrolled. An automated research notification (ARN) tool embedded in the EHR was designed to identify eligible participants in real time with simulations used to test efficacy. The primary outcome was availability of TDM results within two dose intervals. Secondary outcomes evaluated process times, survival and ICU/hospital length of stay.
Results: Of 276 potential participants identified via ARNs, 30 were enrolled, with TDM results available within two dose intervals for 67%. Recruitment prior to the first antimicrobial dose was achieved for 40%. Median (IQR) time from ARN to eligibility assessment was 0.5 (0.2-1.2) hours, significantly faster than simulated intermittent screening at 4.6 (3.1-14.9) hours (P < 0.001).
Discussion: While TDM results within desired timeframes were feasible, logistical constraints highlight the need for improved resource availability. ARNs enabled efficient participant identification and recruitment into time-sensitive research. Future work should explore broader applications of ARNs to clinical trials and precision interventions.
Registration: NCT05971979
Aims & Objectives
This study aimed to investigate turnaround times for delivering actionable results from TDM of beta-lactam antimicrobials in patients admitted to the intensive care unit. Furthermore, we sought to identify technology-enabled approaches to improve participant recruitment into future time-sensitive TDM trials.
Analyses
# Set working directories - if static
# if (interactive() && requireNamespace("rstudioapi", quietly = TRUE)) {
# script_path <- rstudioapi::getActiveDocumentContext()$path
# script_dir <- dirname(script_path)
# setwd(script_dir)
#}
#data_dir <- file.path(dirname(getwd()), "Data")
#getwd()# Identify the latest TDM-TIME dataset in the current folder
files <- list.files(pattern = "^TDMTIME_DATA_.*\\.csv$")
dates <- str_extract(files, "(?<=TDMTIME_DATA_)[0-9]{4}-[0-9]{2}-[0-9]{2}")
date_formats <- as.Date(dates, format = "%Y-%m-%d")
latest_date_index <- which.max(date_formats)
latest_file <- files[latest_date_index]
# Read in the latest TDM-TIME dataset
df <- read_csv(latest_file)
# Remove incomplete cases
df <- df %>% filter(!is.na(baseline_sample_collected))
# Remove invalid data - PID 11 had subsequent dose administered prior to fourth sample
df[df$record_id == 11, "tdm4_pip_conc"] <- NA
df[df$record_id == 11, "tdm4_taz_conc"] <- NA# Apply labels
# Read in dictionary and clean names
dictionary <- read_csv("TDMTIME_DataDictionary_2024-12-16.csv")
dictionary <- clean_names(dictionary)
# Create a coded variables dataframe
coded_vars <- dictionary %>%
filter(field_type %in% c("yesno", "radio", "dropdown")) %>%
select(variable_field_name, choices_calculations_or_slider_labels) %>%
filter(if_all(everything(), ~ !is.na(.)))
# Parse the variables using a for loop
for (i in seq_len(nrow(coded_vars))) {
var <- coded_vars$variable_field_name[i]
codes <- coded_vars$choices_calculations_or_slider_labels[i]
if (var %in% names(df)) {
parsed_codes <- str_split(codes, "\\|")[[1]]
parsed_levels_labels <- str_split(parsed_codes, ",")
levels <- sapply(parsed_levels_labels, function(x) str_trim(x[1]))
labels <- sapply(parsed_levels_labels, function(x) str_trim(x[2]))
df[[var]] <- factor(df[[var]], levels = levels, labels = labels)
} else {
warning(paste("Variable", var, "not found in the dataframe."))
}
}# Calculate the SOFA score and its components
df <- df %>%
mutate(pf_ratio = (pao2*7.50062)/(fio2*0.01)) %>%
mutate(
resp_score = case_when(
pf_ratio <= 100 ~ 4,
pf_ratio <= 200 ~ 3,
pf_ratio <= 300 ~ 2,
pf_ratio <= 400 ~ 1,
TRUE ~ 0
),
coag_score = case_when(
platelets >= 150 ~ 0,
platelets <= 149 ~ 1,
platelets <= 99 ~ 2,
platelets <= 49 ~ 3,
platelets <= 19 ~ 4,
TRUE ~ 0
),
liver_score = case_when(
bilirubin >= 205 ~ 4,
bilirubin >= 102 ~ 3,
bilirubin >= 33 ~ 2,
bilirubin >= 20 ~ 1,
TRUE ~ 0
),
cardio_score = case_when(
map > 70 & vasopressor == 0 ~ 0,
map < 70 ~ 1,
vasopressor == 1 & vasopressor_dose <= 0.1 ~ 3,
vasopressor == 1 & vasopressor_dose > 0.1 ~ 4,
TRUE ~ 0
),
cns_score = case_when(
gcs <= 5 ~ 4,
gcs <= 9 ~ 3,
gcs <= 12 ~ 2,
gcs <= 14 ~ 1,
TRUE ~ 0
),
renal_score = case_when(
creatinine <= 110 ~ 0,
creatinine <= 170 ~ 1,
creatinine <= 299 ~ 2,
creatinine <= 440 ~ 3,
creatinine > 440 ~ 4,
TRUE ~ 0
),
sofa = resp_score + coag_score + liver_score + cardio_score + cns_score + renal_score
)Participant baseline characteristics
# Baseline characteristics table
df <- df %>%
mutate(
sex = fct_infreq(as.factor(sex)), # Reorder 'sex'
postop = factor(ifelse(postop == 1, "Yes", "No"), levels = c("Yes", "No")), # Convert 1/0 to Yes/No
abx_indication = fct_infreq(as.factor(abx_indication)), # Reorder 'abx_indication'
mechanical_ventilation = factor(ifelse(mechanical_ventilation == 1, "Yes", "No"), levels = c("Yes", "No"))
)
label(df$sex) <- "Sex"
label(df$pt_age) <- "Age (years)"
label(df$bmi) <- "Body mass index (BMI)"
label(df$postop) <- "Surgery during admission"
label(df$abx_indication) <- "Antimicrobial indication"
label(df$sofa) <- "SOFA score"
label(df$crrt) <- "CRRT"
label(df$mechanical_ventilation) <- "Mechanical ventilation"
label(df$vasopressor) <- "Vasopressor requirement"
label(df$crp_day_0) <- "C-reactive protein"
label(df$pct_day_0) <- "Procalcitonin"
label(df$bicarbonate) <- "Bicarbonate"
label(df$pf_ratio) <- "P/F Ratio"
label(df$wcc_day_0) <- "WBC Count"
# Create the table
table1(~ sex + pt_age + bmi + postop + abx_indication + sofa + crrt + mechanical_ventilation + vasopressor + crp_day_0 + bicarbonate + pct_day_0 + wcc_day_0 + pf_ratio | abx_name, data = df, render.continuous=c(.="Median [Q1, Q3]"))| Piperacillin/tazobactam (N=26) |
Meropenem (N=4) |
Overall (N=30) |
|
|---|---|---|---|
| Sex | |||
| Male | 14 (53.8%) | 2 (50.0%) | 16 (53.3%) |
| Female | 12 (46.2%) | 2 (50.0%) | 14 (46.7%) |
| Other | 0 (0%) | 0 (0%) | 0 (0%) |
| Age (years) | |||
| Median [Q1, Q3] | 63.0 [55.3, 75.8] | 61.5 [55.8, 68.8] | 63.0 [55.3, 74.8] |
| Body mass index (BMI) | |||
| Median [Q1, Q3] | 29.8 [24.0, 35.8] | 30.1 [27.7, 33.4] | 29.8 [24.7, 35.8] |
| Surgery during admission | |||
| Yes | 17 (65.4%) | 1 (25.0%) | 18 (60.0%) |
| No | 9 (34.6%) | 3 (75.0%) | 12 (40.0%) |
| Antimicrobial indication | |||
| Hospital-acquired pneumonia (HAP) | 20 (76.9%) | 2 (50.0%) | 22 (73.3%) |
| Other | 2 (7.7%) | 1 (25.0%) | 3 (10.0%) |
| Sepsis (likely respiratory origin) | 2 (7.7%) | 0 (0%) | 2 (6.7%) |
| Ventilator-associated pneumonia (VAP) | 1 (3.8%) | 1 (25.0%) | 2 (6.7%) |
| Community-acquired pneumonia (CAP) | 1 (3.8%) | 0 (0%) | 1 (3.3%) |
| SOFA score | |||
| Median [Q1, Q3] | 6.00 [3.00, 8.00] | 9.00 [8.50, 10.0] | 6.50 [3.00, 9.00] |
| CRRT | |||
| Median [Q1, Q3] | 0 [0, 0] | 0.500 [0, 1.00] | 0 [0, 0] |
| Mechanical ventilation | |||
| Yes | 15 (57.7%) | 4 (100%) | 19 (63.3%) |
| No | 11 (42.3%) | 0 (0%) | 11 (36.7%) |
| Vasopressor requirement | |||
| Median [Q1, Q3] | 0 [0, 0.750] | 0.500 [0, 1.00] | 0 [0, 1.00] |
| C-reactive protein | |||
| Median [Q1, Q3] | 218 [141, 281] | 143 [63.0, 221] | 211 [133, 272] |
| Bicarbonate | |||
| Median [Q1, Q3] | 25.4 [23.9, 26.9] | 25.1 [24.1, 26.6] | 25.4 [23.9, 26.9] |
| Procalcitonin | |||
| Median [Q1, Q3] | 0.150 [0, 0.683] | 0 [0, 3.80] | 0.0950 [0, 0.683] |
| WBC Count | |||
| Median [Q1, Q3] | 12.6 [9.98, 15.8] | 21.4 [19.5, 22.3] | 13.6 [10.3, 16.2] |
| P/F Ratio | |||
| Median [Q1, Q3] | 209 [134, 307] | 254 [205, 291] | 216 [134, 307] |
# Create a dataframe with the times based on intervals from REDCap
times_df <- df %>%
mutate(ref_time = case_when(
abx_name == 'Piperacillin/tazobactam' & tazocin_interval == 'Three times a day' ~ 16,
abx_name == 'Piperacillin/tazobactam' & tazocin_interval == 'Four times a day' ~ 12,
abx_name == 'Piperacillin/tazobactam' & tazocin_interval == 'Twice daily' ~ 24,
abx_name == 'Piperacillin/tazobactam' & tazocin_interval == 'Other' ~ as.integer(tazocin_interval_other)*2,
abx_name == 'Meropenem' & meropenem_interval == 'Three times a day' ~ 48,
abx_name == 'Meropenem' & meropenem_interval == 'Four times a day' ~ 24,
abx_name == 'Meropenem' & meropenem_interval == 'Twice daily' ~ 16,
abx_name == 'Meropenem' & meropenem_interval == 'Other' ~ as.integer(meropenem_interval_other)*2,
TRUE ~ 0
))
df <- df %>%
mutate(tdm1_diff = difftime(df$tdm1_time_collected, df$datetime_current_abx_dose, units = 'hours'),
tdm2_diff = difftime(df$tdm2_time_collected, df$datetime_current_abx_dose, units = 'hours'),
tdm3_diff = difftime(df$tdm3_time_collected, df$datetime_current_abx_dose, units = 'hours'),
tdm4_diff = difftime(df$tdm4_time_collected, df$datetime_current_abx_dose, units = 'hours')
)
df <- df %>%
mutate(
pre_analytical = difftime(time_thawed, datetime_current_abx_dose, units = "mins"),
analytical = difftime(time_lcmsms_finish, time_thawed, units = "mins")
)Primary outcome
LC-MS/MS available within two dosing intervals from sampling
# Calculate the primary outcome (LC-MS/MS available within two dosing intervals from sampling)
times_df <- times_df %>%
mutate(primary_outcome = ifelse(time_tdm_final - datetime_current_abx_dose > ref_time, 0, 1)) %>%
mutate(secondary_outcome_time_from_abx_start = ifelse(time_tdm_final - abx_start_time > ref_time, 0, 1)) %>%
mutate(time_sampling = time_tdm_final - datetime_current_abx_dose) %>%
mutate(time_start = time_tdm_final - abx_start_time) %>%
select(primary_outcome, secondary_outcome_time_from_abx_start, time_sampling, time_start, abx_name)
# Output the primary outcome as a percentage
times_df %>%
summarise(
total = n(),
success = sum(primary_outcome, na.rm = TRUE)
) %>%
mutate(
percentage = (success/total) * 100
)# A tibble: 1 × 3
total success percentage
<int> <dbl> <dbl>
1 30 21 70
# Group by antimicrobial
times_df %>%
group_by(abx_name) %>%
summarise(
total = n(),
success = sum(primary_outcome, na.rm = TRUE)
) %>%
mutate(
percentage = (success/total) * 100
)# A tibble: 2 × 4
abx_name total success percentage
<fct> <int> <dbl> <dbl>
1 Piperacillin/tazobactam 26 18 69.2
2 Meropenem 4 3 75
Secondary outcomes
# Output the secondary outcome as a percentage
times_df %>%
summarise(
total = n(),
success = sum(secondary_outcome_time_from_abx_start, na.rm = TRUE)
) %>%
mutate(
percentage = (success/total) * 100
)# A tibble: 1 × 3
total success percentage
<int> <dbl> <dbl>
1 30 11 36.7
# Group by antimicrobial
times_df %>%
group_by(abx_name) %>%
summarise(
total = n(),
success = sum(secondary_outcome_time_from_abx_start, na.rm = TRUE)
) %>%
mutate(
percentage = (success/total) * 100
)# A tibble: 2 × 4
abx_name total success percentage
<fct> <int> <dbl> <dbl>
1 Piperacillin/tazobactam 26 8 30.8
2 Meropenem 4 3 75
# Output the time from sampling to LCMS available
times_df %>%
summarise(
median = median(time_sampling),
iqr_25 = quantile(time_sampling, 0.25, na.rm = TRUE),
iqr_75 = quantile(time_sampling, 0.75, na.rm = TRUE)
) %>%
mutate(
iqr_range = paste0(iqr_25, " - ", iqr_75)
)# A tibble: 1 × 4
median iqr_25 iqr_75 iqr_range
<drtn> <drtn> <drtn> <chr>
1 10.95 hours 9.5875 hours 21.35833 hours 9.5875 - 21.3583333333333
# Group by antimicrobial
times_df %>%
group_by(abx_name) %>%
summarise(
median = median(time_sampling),
iqr_25 = quantile(time_sampling, 0.25, na.rm = TRUE),
iqr_75 = quantile(time_sampling, 0.75, na.rm = TRUE)
) %>%
mutate(
iqr_range = paste0(iqr_25, " - ", iqr_75)
)# A tibble: 2 × 5
abx_name median iqr_25 iqr_75 iqr_range
<fct> <drtn> <drtn> <drtn> <chr>
1 Piperacillin/tazobactam 10.64167 hours 9.58750 hours 20.20833 hours 9.5875 -…
2 Meropenem 21.40000 hours 17.95833 hours 22.45417 hours 17.95833…
# Output the time from abx start to LCMS available
times_df %>%
summarise(
median = median(time_start),
iqr_25 = quantile(time_start, 0.25, na.rm = TRUE),
iqr_75 = quantile(time_start, 0.75, na.rm = TRUE)
) %>%
mutate(
iqr_range = paste0(iqr_25, " - ", iqr_75)
)# A tibble: 1 × 4
median iqr_25 iqr_75 iqr_range
<drtn> <drtn> <drtn> <chr>
1 25.35 hours 12.3 hours 30.87917 hours 12.3 - 30.8791666666667
# Group by antimicrobial
times_df %>%
group_by(abx_name) %>%
summarise(
median = median(time_start),
iqr_25 = quantile(time_start, 0.25, na.rm = TRUE),
iqr_75 = quantile(time_start, 0.75, na.rm = TRUE)
) %>%
mutate(
iqr_range = paste0(iqr_25, " - ", iqr_75)
)# A tibble: 2 × 5
abx_name median iqr_25 iqr_75 iqr_range
<fct> <drtn> <drtn> <drtn> <chr>
1 Piperacillin/tazobactam 25.55833 hours 10.64583 hours 30.87917 hours 10.64583…
2 Meropenem 24.63333 hours 22.02500 hours 29.82917 hours 22.025 -…
# Pre- and analytical phases
df %>%
summarise(
pre_ana_median = median(pre_analytical, na.rm = TRUE),
pre_ana_iqr_25 = quantile(pre_analytical, 0.25, na.rm = TRUE),
pre_ana_iqr_75 = quantile(pre_analytical, 0.75, na.rm = TRUE),
ana_median = median(analytical, na.rm = TRUE),
ana_iqr_25 = quantile(analytical, 0.25, na.rm = TRUE),
ana_iqr_75 = quantile(analytical, 0.75, na.rm = TRUE)
)# A tibble: 1 × 6
pre_ana_median pre_ana_iqr_25 pre_ana_iqr_75 ana_median ana_iqr_25 ana_iqr_75
<drtn> <drtn> <drtn> <drtn> <drtn> <drtn>
1 497 mins 460.75 mins 1127.75 mins 138 mins 122.5 mins 163.5 mins
# Group by antimicrobial
df %>%
group_by(abx_name) %>%
summarise(
pre_ana_median = median(pre_analytical, na.rm = TRUE) / 60,
pre_ana_iqr_25 = quantile(pre_analytical, 0.25, na.rm = TRUE) / 60,
pre_ana_iqr_75 = quantile(pre_analytical, 0.75, na.rm = TRUE) / 60,
ana_median = median(analytical, na.rm = TRUE),
ana_iqr_25 = quantile(analytical, 0.25, na.rm = TRUE),
ana_iqr_75 = quantile(analytical, 0.75, na.rm = TRUE)
)# A tibble: 2 × 7
abx_name pre_ana_median pre_ana_iqr_25 pre_ana_iqr_75 ana_median ana_iqr_25
<fct> <drtn> <drtn> <drtn> <drtn> <drtn>
1 Piperacill… 8.116667 mins 7.679167 mins 17.98333 mins 138.0 mins 115.25 mi…
2 Meropenem 18.991667 mins 15.258333 mins 20.24583 mins 144.5 mins 129.50 mi…
# ℹ 1 more variable: ana_iqr_75 <drtn>
# 28-day survival
df %>%
summarise(
total = n(),
success = sum(alive_28d, na.rm = TRUE)
) %>%
mutate(
percentage = (success/total) * 100
)# A tibble: 1 × 3
total success percentage
<int> <dbl> <dbl>
1 30 21 70
# Group by antimicrobial
df %>%
group_by(abx_name) %>%
summarise(
total = n(),
success = sum(alive_28d, na.rm = TRUE)
) %>%
mutate(
percentage = (success/total) * 100
)# A tibble: 2 × 4
abx_name total success percentage
<fct> <int> <dbl> <dbl>
1 Piperacillin/tazobactam 26 20 76.9
2 Meropenem 4 1 25
# 28-day ICU
df %>%
summarise(
total = n(),
success = sum(discharged_icu_28d, na.rm = TRUE)
) %>%
mutate(
percentage = (success/total) * 100
)# A tibble: 1 × 3
total success percentage
<int> <dbl> <dbl>
1 30 15 50
# Group by antimicrobial
df %>%
group_by(abx_name) %>%
summarise(
total = n(),
success = sum(discharged_icu_28d, na.rm = TRUE)
) %>%
mutate(
percentage = (success/total) * 100
)# A tibble: 2 × 4
abx_name total success percentage
<fct> <int> <dbl> <dbl>
1 Piperacillin/tazobactam 26 14 53.8
2 Meropenem 4 1 25
# ICU length of stay
df %>%
mutate(icu_los = as.numeric(discharged_icu_date - icu_admitted_date)) %>%
summarise(
median = median(icu_los, na.rm = TRUE),
iqr_25 = quantile(icu_los, 0.25, na.rm = TRUE),
iqr_75 = quantile(icu_los, 0.75, na.rm = TRUE)
)# A tibble: 1 × 3
median iqr_25 iqr_75
<dbl> <dbl> <dbl>
1 7 5 25
# Group by antimicrobial
df %>%
group_by(abx_name) %>%
mutate(icu_los = as.numeric(discharged_icu_date - icu_admitted_date)) %>%
summarise(
median = median(icu_los, na.rm = TRUE),
iqr_25 = quantile(icu_los, 0.25, na.rm = TRUE),
iqr_75 = quantile(icu_los, 0.75, na.rm = TRUE)
)# A tibble: 2 × 4
abx_name median iqr_25 iqr_75
<fct> <dbl> <dbl> <dbl>
1 Piperacillin/tazobactam 6.5 5 23
2 Meropenem 23 23 23
# 28-day hospital discharge
df %>%
summarise(
total = n(),
success = sum(discharged_28d, na.rm = TRUE)
) %>%
mutate(
percentage = (success/total) * 100
)# A tibble: 1 × 3
total success percentage
<int> <dbl> <dbl>
1 30 9 30
# Hospital length of stay
df %>%
mutate(hosp_los = as.numeric(discharged_date - admitted_date)) %>%
summarise(
median = median(hosp_los, na.rm = TRUE),
iqr_25 = quantile(hosp_los, 0.25, na.rm = TRUE),
iqr_75 = quantile(hosp_los, 0.75, na.rm = TRUE)
)# A tibble: 1 × 3
median iqr_25 iqr_75
<dbl> <dbl> <dbl>
1 17 10 27
# Group by antimicrobial
df %>%
mutate(hosp_los = as.numeric(discharged_date - admitted_date)) %>%
group_by(abx_name) %>%
summarise(
median = median(hosp_los, na.rm = TRUE),
iqr_25 = quantile(hosp_los, 0.25, na.rm = TRUE),
iqr_75 = quantile(hosp_los, 0.75, na.rm = TRUE)
)# A tibble: 2 × 4
abx_name median iqr_25 iqr_75
<fct> <dbl> <dbl> <dbl>
1 Piperacillin/tazobactam 17 10 27
2 Meropenem NA NA NA
df <- df %>%
mutate(first_dose = ifelse(abs(difftime(datetime_current_abx_dose, abx_start_time, units = "hours")) <= 4, 1, 0))
# Proportion of participants recruited at time of first dose
df %>%
select(first_dose) %>%
summarise(
total = n(),
success = sum(first_dose, na.rm = TRUE)
) %>%
mutate(
percentage = (success/total) * 100
)# A tibble: 1 × 3
total success percentage
<int> <dbl> <dbl>
1 30 12 40
Antimicrobial doses
# Antimicrobial concentration analyses
# Gather the concentrations
df2 <- df %>%
select(record_id, tdm1_diff, tdm2_diff, tdm3_diff, tdm4_diff,
tdm1_pip_conc, tdm2_pip_conc, tdm3_pip_conc, tdm4_pip_conc,
tdm1_taz_conc, tdm2_taz_conc, tdm3_taz_conc, tdm4_taz_conc,
tdm1_mer_conc, tdm2_mer_conc, tdm3_mer_conc, tdm4_mer_conc) %>%
mutate(tdm1_time = as.numeric(tdm1_diff, units = 'hours'),
tdm2_time = as.numeric(tdm2_diff, units = 'hours'),
tdm3_time = as.numeric(tdm3_diff, units = 'hours'),
tdm4_time = as.numeric(tdm4_diff, units = 'hours'))
# Pivot the dataframe to a long dataset
long_df <- df2 %>%
pivot_longer(
cols = -record_id,
names_to = c("timepoint", "antibiotic", ".value"),
names_pattern = "(tdm\\d+)_?(pip|taz|mer)?_(time|conc)"
)
# Select time data
time_data <- long_df %>%
filter(!is.na(time)) %>% # Select only time rows
select(-conc, -antibiotic) # Drop the concentration column
# Select concentration data
conc_data <- long_df %>%
filter(!is.na(antibiotic)) %>% # Select only concentration rows
select(-time) # Drop the time column
# Join time and concentration datasets
clean_df <- conc_data %>%
left_join(time_data, by = c("record_id", "timepoint")) %>%
filter(!is.na(conc)) %>%
filter(antibiotic != 'taz')
# Define MIC breakpoints
pip_mic <- 16
taz_mic <- 4
mer_mic <- 8
# Calculate target attainment
clean_df %>%
filter(antibiotic == "pip" & timepoint == 'tdm4') %>%
mutate(below = ifelse((conc * 0.7) < pip_mic, 1, 0)) %>%
summarise(count = n(), sum = sum(below))# A tibble: 1 × 2
count sum
<int> <dbl>
1 24 11
clean_df %>%
filter(antibiotic == "taz" & timepoint == 'tdm4') %>%
mutate(below = ifelse(conc < taz_mic, 1, 0)) %>%
summarise(count = n(), sum = sum(below))# A tibble: 1 × 2
count sum
<int> <int>
1 0 0
clean_df %>%
filter(antibiotic == "mer" & timepoint == 'tdm4') %>%
mutate(below = ifelse(conc < mer_mic, 1, 0)) %>%
summarise(count = n(), sum = sum(below))# A tibble: 1 × 2
count sum
<int> <dbl>
1 3 0
ggplot(clean_df, aes(x = time, y = conc, group = interaction(record_id, antibiotic), color = antibiotic)) +
geom_point() +
geom_line() +
labs(x = "Time (hours)", y = "Concentration (mg/L)", title = "Antimicrobial concentrations over dosing interval") +
theme_minimal() +
xlim(0,8.3) +
scale_color_discrete(
name = "Antimicrobial",
labels = c("Meropenem", "Piperacillin", "Tazobactam")
)below_pip <- clean_df %>%
filter(antibiotic == "pip" & timepoint == 'tdm4') %>%
mutate(below = ifelse((conc * 0.7) < pip_mic, 1, 0)) %>%
left_join(df, by = c("record_id")) %>%
mutate(vd = 4500/tdm1_pip_conc)
below_pip_2 <- below_pip %>%
select(record_id, below) %>%
right_join(clean_df, by = 'record_id') %>%
filter(!is.na(below))
ggplot(below_pip_2, aes(x = time, y = conc, group = interaction(record_id, as.factor(below)), color = as.factor(below))) +
geom_point() +
geom_line() +
labs(x = "Time (hours)", y = "Concentration (mg/L)", title = "Antimicrobial concentrations over dosing interval") +
theme_minimal() +
xlim(0,8.3)Culture data
### Culture data
# Proportion with positive culture isolates
df %>%
summarise(
total = n(),
positive = sum(cultures, na.rm = TRUE)
) %>%
mutate(
percentage = (positive/total) * 100
)# A tibble: 1 × 3
total positive percentage
<int> <dbl> <dbl>
1 30 21 70
# Count of culture sources
df %>%
count(culture_source)# A tibble: 5 × 2
culture_source n
<fct> <int>
1 Blood 4
2 Sputum 8
3 Bronchial washing/BAL 8
4 Other 1
5 <NA> 9
df %>%
group_by(culture_source) %>%
count(pathogen) %>%
print(n = Inf)# A tibble: 22 × 3
# Groups: culture_source [5]
culture_source pathogen n
<fct> <chr> <int>
1 Blood Escherichia coli 1
2 Blood Serratia marcescens 1
3 Blood Staphylococcus capitis 1
4 Blood Staphylococcus epidermidis 1
5 Sputum Acinetobacter baumannii; Aspergillus fumigatus s… 1
6 Sputum Candida albicans 1
7 Sputum Candida sp, Corynebacterium, adenovirus 1
8 Sputum Enterobacter cloacae complex 1
9 Sputum Pseudomonas aeruginosa 1
10 Sputum Staphylococcus aureus, Streptococcus pneumoniae,… 1
11 Sputum Yeast 3+ 1
12 Sputum rhinovirus PCR 1
13 Bronchial washing/BAL E.coli 1
14 Bronchial washing/BAL Escherichia coli, Haemophilus influenzae, Stenot… 1
15 Bronchial washing/BAL Escherichia coli, candida albicans 1
16 Bronchial washing/BAL Escherichia coli, candida tropicalis, staphyloco… 1
17 Bronchial washing/BAL Haemophilus influenzae 1
18 Bronchial washing/BAL Klebsiella pneumoniae 1
19 Bronchial washing/BAL Serratia marcescens and Klebsiella oxytoca 1
20 Bronchial washing/BAL Staphylococcus aureus 1
21 Other Parainfluenza-3 1
22 <NA> <NA> 9
# Clean up free text data
pathogens <- df %>%
filter(!is.na(pathogen)) %>% # Remove NAs
separate_rows(pathogen, sep = "[,;]") %>% # Split by comma or semicolon
separate_rows(pathogen, sep = "\\band\\b") %>% # Split by 'and'
mutate(pathogen = str_trim(pathogen)) %>% # Trim white spaces
mutate(pathogen = str_to_title(pathogen)) %>% # Standardize capitalization
#distinct(pathogen, .keep_all = TRUE) %>% # Remove duplicates
arrange(pathogen) %>% # Sort for readability
select(pathogen, culture_source)
# View cleaned data
print(pathogens, n = Inf)# A tibble: 33 × 2
pathogen culture_source
<chr> <fct>
1 Acinetobacter Baumannii Sputum
2 Adenovirus Sputum
3 Aspergillus Fumigatus Species Complex Sputum
4 Aspergillus Pcr Sputum
5 Candida Albicans Bronchial washing/BAL
6 Candida Albicans Sputum
7 Candida Sp Sputum
8 Candida Tropicalis Bronchial washing/BAL
9 Corynebacterium Sputum
10 E.coli Bronchial washing/BAL
11 Enterobacter Cloacae Complex Sputum
12 Escherichia Coli Blood
13 Escherichia Coli Bronchial washing/BAL
14 Escherichia Coli Bronchial washing/BAL
15 Escherichia Coli Bronchial washing/BAL
16 Haemophilus Influenzae Bronchial washing/BAL
17 Haemophilus Influenzae Bronchial washing/BAL
18 Human Metapneumovirus Pcr Sputum
19 Klebsiella Oxytoca Bronchial washing/BAL
20 Klebsiella Pneumoniae Bronchial washing/BAL
21 Parainfluenza-3 Other
22 Pseudomonas Aeruginosa Sputum
23 Rhinovirus Pcr Sputum
24 Serratia Marcescens Blood
25 Serratia Marcescens Bronchial washing/BAL
26 Staphylococcus Aureus Sputum
27 Staphylococcus Aureus Bronchial washing/BAL
28 Staphylococcus Capitis Blood
29 Staphylococcus Epidermidis Blood
30 Staphylococcus Epidermidis Bronchial washing/BAL
31 Stenotrophomonas Maltophilia Bronchial washing/BAL
32 Streptococcus Pneumoniae Sputum
33 Yeast 3+ Sputum
# Susceptible proportion
df %>%
count(susceptible_to_abx)# A tibble: 4 × 2
susceptible_to_abx n
<fct> <int>
1 Yes 9
2 No 4
3 Susceptibilities not available 8
4 <NA> 9
# Resistant bugs
df %>%
filter(susceptible_to_abx == 'No') %>%
select(pathogen)# A tibble: 4 × 1
pathogen
<chr>
1 Acinetobacter baumannii; Aspergillus fumigatus species complex
2 E.coli
3 Escherichia coli
4 Enterobacter cloacae complex
Assessment of Automated Research Notifications (ARN)
#### ARN assesssment
# Read in ARN file
df_bpa <- read_excel('TDMTIME_BPA_DATA_2024-12-12.xlsx', sheet = 'Anonymised')
# Create IDs and keep only BPA timings
df_bpa <- df_bpa %>%
mutate(pid = row_number()) %>%
select(pid, everything()) %>%
filter(method_screen == 'BPA')
df_bpa$date_screen <- as.Date(df_bpa$date_screen)
df_bpa$time_text <- as.POSIXct(paste(df_bpa$date_screen, df_bpa$time_text))
df_bpa$time_rx_screen <- as.numeric(df_bpa$time_rx_screen)
df_alt <- df_bpa %>%
mutate(
# Ensure datetime is in POSIXct format
datetime = ymd_hms(time_text),
day_of_week = wday(datetime, week_start = 1), # Ensure week starts on Monday (1 = Monday, 7 = Sunday)
# Calculate the next target time based on the provided rules
next_target_time = case_when(
# Weekdays before 09:00
hour(datetime) < 9 & day_of_week %in% 1:5 ~ update(datetime, hour = 9, minute = 0, second = 0),
# Weekdays between 09:00 and 14:59
hour(datetime) >= 9 & hour(datetime) < 14 & day_of_week %in% 1:5 ~ update(datetime, hour = 14, minute = 0, second = 0),
# Weekdays after 15:00
hour(datetime) >= 14 & day_of_week %in% 1:4 ~ update(datetime + days(1), hour = 9, minute = 0, second = 0),
# Friday after 15:00 through the weekend
(hour(datetime) >= 14 & day_of_week == 5) | day_of_week %in% 6:7 ~ update(datetime + days(8 - day_of_week), hour = 9, minute = 0, second = 0),
# Default case to handle unexpected scenarios
TRUE ~ datetime
),
BPA_daytime = case_when(
# Within the weekday window
day_of_week %in% 1:5 & hour(datetime) >= 8 & hour(datetime) < 16 ~ time_rx_screen,
# Before the window on weekdays
day_of_week %in% 1:5 & hour(datetime) < 8 ~ as.numeric(difftime(update(datetime, hour = 8), datetime, units = "mins")),
# After the window on weekdays except Friday
day_of_week %in% 1:4 & hour(datetime) >= 16 ~ as.numeric(difftime(update(datetime + days(1), hour = 8), datetime, units = "mins")),
# After the window on Friday or weekends
(day_of_week == 5 & hour(datetime) >= 16) | day_of_week %in% 6:7 ~ as.numeric(difftime(update(datetime + days(8 - day_of_week), hour = 8), datetime, units = "mins")),
# Default to 0 if none of the conditions match
TRUE ~ 0
)
) %>%
mutate(
# Additional calculations based on next_target_time
minutes_to_next_target = as.numeric(difftime(next_target_time, datetime, units = "mins")),
less_than_24hrs = minutes_to_next_target < 1440,
less_than_2hrs = minutes_to_next_target < 120,
less_than_1hr = minutes_to_next_target < 60,
BPA_less_than_1hr = time_rx_screen < 60, # Ensure time_rx_screen is defined or convert it appropriately
weekend = if_else((day_of_week %in% c(6, 7) | (hour(datetime) >= 16 & day_of_week %in% c(5))), TRUE, FALSE),
daytime = hour(datetime) >= 8 & hour(datetime) < 16
)
df3 <- df_alt %>%
filter(!is.na(time_rx_screen)) %>%
filter(!weekend)
df_long3 <- df3 %>%
pivot_longer(cols = c(time_rx_screen, minutes_to_next_target, BPA_daytime),
names_to = "measurement_type",
values_to = "time_in_minutes") %>%
mutate(time_in_hours = time_in_minutes / 60 + 0.01) %>%
mutate(colour_group = ifelse(time_in_hours > 24, "More than 24", "Less than 24"))
df_long3 <- df_long3 %>%
filter(measurement_type != 'BPA_daytime')
df_long3$measurement_type <- factor(df_long3$measurement_type,
levels = c("minutes_to_next_target", "time_rx_screen"),
labels = c("Intermittent screening", "ARN"))
df_long3$measurement_type <- relevel(factor(df_long3$measurement_type), ref = "ARN")
# Summary stats for BPA comparison
df_long3 %>%
group_by(measurement_type) %>%
summarise(
median = median(time_in_minutes) / 60,
iqr_25 = quantile(time_in_minutes, 0.25, na.rm = TRUE) / 60,
iqr_75 = quantile(time_in_minutes, 0.75, na.rm = TRUE) / 60
) %>%
mutate(
iqr_range = paste0(iqr_25, " - ", iqr_75)
)# A tibble: 2 × 5
measurement_type median iqr_25 iqr_75 iqr_range
<fct> <dbl> <dbl> <dbl> <chr>
1 ARN 0.375 0.2 1.18 0.2 - 1.175
2 Intermittent screening 4.63 3.14 14.9 3.14166666666667 - 14.87083333333…
# Time to event analysis
surv_obj <- Surv(time = df_long3$time_in_hours, event = df_long3$less_than_24hrs)
fit <- survfit(surv_obj ~ measurement_type, data = df_long3)
cox_model <- coxph(surv_obj ~ measurement_type, data = df_long3)
cox_modelCall:
coxph(formula = surv_obj ~ measurement_type, data = df_long3)
coef exp(coef) se(coef) z p
measurement_typeIntermittent screening -1.6745 0.1874 0.1332 -12.57 <2e-16
Likelihood ratio test=160.4 on 1 df, p=< 2.2e-16
n= 332, number of events= 322
summary(cox_model) #output provides HR CIsCall:
coxph(formula = surv_obj ~ measurement_type, data = df_long3)
n= 332, number of events= 322
coef exp(coef) se(coef) z
measurement_typeIntermittent screening -1.6745 0.1874 0.1332 -12.57
Pr(>|z|)
measurement_typeIntermittent screening <2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
exp(coef) exp(-coef) lower .95 upper .95
measurement_typeIntermittent screening 0.1874 5.336 0.1443 0.2433
Concordance= 0.701 (se = 0.008 )
Likelihood ratio test= 160.4 on 1 df, p=<2e-16
Wald test = 158 on 1 df, p=<2e-16
Score (logrank) test = 186.4 on 1 df, p=<2e-16
#####
## The KM plot
#####
ggsurv_plot <- ggsurvplot(
fit, data = df_long3, pval = FALSE, risk.table = TRUE, risk.table.height = 0.25, conf.int = FALSE,
palette = c("#0073C2FF", "#EFC000FF", "#E65C5C", '#37C78E'),
ggtheme = theme_minimal(),
xlab = "Time in Hours", ylab = "Proportion identified",
legend.title="", surv.scale="percent",
tables.col="strata",
risk.table.col = "strata",
risk.table.y.text = FALSE,
break.x.by = 1,
xlim = c(0,16)
)
main_plot <- ggsurv_plot$plot +
scale_y_reverse() + # Reverse the y-axis
theme(axis.title.y = element_text(hjust = 0.5))
# Combine the reversed main plot with the risk table
combined_plot <- cowplot::plot_grid(
main_plot,
ggsurv_plot$table, # Keep the risk table unchanged
ncol = 1,
rel_heights = c(3, 1) # Adjust heights for better balance
)
print(combined_plot)# SENSITIVIY ANALYSIS BPA COMPARISON
df3 <- df_alt %>%
filter(!is.na(time_rx_screen)) %>%
filter(!weekend)
df_long3 <- df3 %>%
pivot_longer(cols = c(time_rx_screen, minutes_to_next_target, BPA_daytime),
names_to = "measurement_type",
values_to = "time_in_minutes") %>%
mutate(time_in_hours = time_in_minutes / 60 + 0.01) %>%
mutate(colour_group = ifelse(time_in_hours > 24, "More than 24", "Less than 24"))
df_long3$measurement_type <- factor(df_long3$measurement_type,
levels = c("minutes_to_next_target", "time_rx_screen", "BPA_daytime"),
labels = c("Intermittent screening", "ARN", "ARN daytime"))
# Summary stats for BPA comparison
df_long3 %>%
group_by(measurement_type) %>%
summarise(
median = median(time_in_minutes) / 60,
iqr_25 = quantile(time_in_minutes, 0.25, na.rm = TRUE) / 60,
iqr_75 = quantile(time_in_minutes, 0.75, na.rm = TRUE) / 60
) %>%
mutate(
iqr_range = paste0(iqr_25, " - ", iqr_75)
)# A tibble: 3 × 5
measurement_type median iqr_25 iqr_75 iqr_range
<fct> <dbl> <dbl> <dbl> <chr>
1 Intermittent screening 4.63 3.14 14.9 3.14166666666667 - 14.87083333333…
2 ARN 0.375 0.2 1.18 0.2 - 1.175
3 ARN daytime 0.617 0.2 7 0.2 - 7
df_long3$measurement_type <- relevel(factor(df_long3$measurement_type), ref = "ARN")
# Time to event analysis
surv_obj <- Surv(time = df_long3$time_in_hours)
fit <- survfit(surv_obj ~ measurement_type, data = df_long3)
cox_model <- coxph(surv_obj ~ measurement_type, data = df_long3)
cox_modelCall:
coxph(formula = surv_obj ~ measurement_type, data = df_long3)
coef exp(coef) se(coef) z
measurement_typeIntermittent screening -1.5767 0.2066 0.1269 -12.422
measurement_typeARN daytime -0.6766 0.5083 0.1158 -5.844
p
measurement_typeIntermittent screening < 2e-16
measurement_typeARN daytime 5.09e-09
Likelihood ratio test=163.5 on 2 df, p=< 2.2e-16
n= 498, number of events= 498
log_rank <- survdiff(surv_obj ~ measurement_type, data = df_long3)
log_rankCall:
survdiff(formula = surv_obj ~ measurement_type, data = df_long3)
N Observed Expected (O-E)^2/E
measurement_type=ARN 166 166 79.5 94.05
measurement_type=Intermittent screening 166 166 280.9 47.00
measurement_type=ARN daytime 166 166 137.6 5.87
(O-E)^2/V
measurement_type=ARN 125.89
measurement_type=Intermittent screening 123.49
measurement_type=ARN daytime 8.56
Chisq= 172 on 2 degrees of freedom, p= <2e-16
summary(cox_model) #output provides HR CIsCall:
coxph(formula = surv_obj ~ measurement_type, data = df_long3)
n= 498, number of events= 498
coef exp(coef) se(coef) z
measurement_typeIntermittent screening -1.5767 0.2066 0.1269 -12.422
measurement_typeARN daytime -0.6766 0.5083 0.1158 -5.844
Pr(>|z|)
measurement_typeIntermittent screening < 2e-16 ***
measurement_typeARN daytime 5.09e-09 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
exp(coef) exp(-coef) lower .95 upper .95
measurement_typeIntermittent screening 0.2066 4.839 0.1611 0.2650
measurement_typeARN daytime 0.5083 1.967 0.4051 0.6378
Concordance= 0.671 (se = 0.009 )
Likelihood ratio test= 163.5 on 2 df, p=<2e-16
Wald test = 155 on 2 df, p=<2e-16
Score (logrank) test = 174 on 2 df, p=<2e-16
#####
## The KM plot
#####
ggsurv_plot <- ggsurvplot(
fit, data = df_long3, pval = FALSE, risk.table = TRUE, risk.table.height = 0.25, conf.int = FALSE,
palette = c("#0073C2FF", "#EFC000FF", "#E65C5C", '#37C78E'),
ggtheme = theme_minimal(),
xlab = "Time in Hours", ylab = "Proportion identified",
legend.title="", surv.scale="percent",
tables.col="strata",
risk.table.col = "strata",
risk.table.y.text = FALSE,
break.x.by = 1,
xlim = c(0,16)
)
main_plot <- ggsurv_plot$plot +
scale_y_reverse() + # Reverse the y-axis
theme(axis.title.y = element_text(hjust = 0.5))
# Combine the reversed main plot with the risk table
combined_plot <- cowplot::plot_grid(
main_plot,
ggsurv_plot$table, # Keep the risk table unchanged
ncol = 1,
rel_heights = c(3, 1) # Adjust heights for better balance
)
print(combined_plot)Supplementary analyses
# Summary of reasons for exclusion
summary_table <- df_bpa %>%
filter(!is.na(ineligible_reason)) %>% # Remove NA values
count(ineligible_reason, name = "Count") %>% # Count occurrences
mutate(Percentage = round(Count / sum(Count) * 100, 1)) %>% # Calculate percentages
arrange(desc(Count)) # Sort by count
# Create a gt table
gt_table <- summary_table %>%
gt() %>%
tab_header(
title = "Reasons for ineligibility"
) %>%
cols_label(
ineligible_reason = "Ineligible reason",
Count = "Count",
Percentage = "Percentage (%)"
) %>%
fmt_number(
columns = vars(Percentage),
decimals = 1
) %>%
tab_style(
style = list(cell_text(weight = "bold")),
locations = cells_column_labels(everything())
)
# Print the table
gt_table| Reasons for ineligibility | ||
|---|---|---|
| Ineligible reason | Count | Percentage (%) |
| No capacity | 173 | 70.3 |
| Non-respiratory infection | 45 | 18.3 |
| Antimicrobial started > 24 hrs | 10 | 4.1 |
| Previously enrolled | 6 | 2.4 |
| Unlikely to survive 24 hours | 4 | 1.6 |
| Re-prescription | 3 | 1.2 |
| Non-English speaker | 2 | 0.8 |
| Screening failure | 1 | 0.4 |
| Stepdown to ward imminent | 1 | 0.4 |
| Withdrawn from study | 1 | 0.4 |
# Day of week analyses
df_bpa <- df_bpa %>%
mutate(
# Ensure datetime is in POSIXct format
datetime = ymd_hms(time_text),
day_of_week = wday(datetime, week_start = 1), # Ensure week starts on Monday (1 = Monday, 7 = Sunday)
# Calculate the next target time based on the provided rules
next_target_time = case_when(
# Weekdays before 09:00
hour(datetime) < 9 & day_of_week %in% 1:5 ~ update(datetime, hour = 9, minute = 0, second = 0),
# Weekdays between 09:00 and 14:59
hour(datetime) >= 9 & hour(datetime) < 14 & day_of_week %in% 1:5 ~ update(datetime, hour = 14, minute = 0, second = 0),
# Weekdays after 15:00
hour(datetime) >= 14 & day_of_week %in% 1:4 ~ update(datetime + days(1), hour = 9, minute = 0, second = 0),
# Friday after 15:00 through the weekend
(hour(datetime) >= 14 & day_of_week == 5) | day_of_week %in% 6:7 ~ update(datetime + days(8 - day_of_week), hour = 9, minute = 0, second = 0),
# Default case to handle unexpected scenarios
TRUE ~ datetime
),
BPA_daytime = case_when(
# Within the weekday window
day_of_week %in% 1:5 & hour(datetime) >= 8 & hour(datetime) < 16 ~ time_rx_screen,
# Before the window on weekdays
day_of_week %in% 1:5 & hour(datetime) < 8 ~ as.numeric(difftime(update(datetime, hour = 8), datetime, units = "mins")),
# After the window on weekdays except Friday
day_of_week %in% 1:4 & hour(datetime) >= 16 ~ as.numeric(difftime(update(datetime + days(1), hour = 8), datetime, units = "mins")),
# After the window on Friday or weekends
(day_of_week == 5 & hour(datetime) >= 16) | day_of_week %in% 6:7 ~ as.numeric(difftime(update(datetime + days(8 - day_of_week), hour = 8), datetime, units = "mins")),
# Default to 0 if none of the conditions match
TRUE ~ 0
)
) %>%
mutate(
# Additional calculations based on next_target_time
minutes_to_next_target = as.numeric(difftime(next_target_time, datetime, units = "mins")),
less_than_24hrs = minutes_to_next_target < 1440,
less_than_2hrs = minutes_to_next_target < 120,
less_than_1hr = minutes_to_next_target < 60,
BPA_less_than_1hr = time_rx_screen < 60, # Ensure time_rx_screen is defined or convert it appropriately
weekend = if_else((day_of_week %in% c(6, 7) | (hour(datetime) >= 16 & day_of_week %in% c(5))), TRUE, FALSE),
daytime = hour(datetime) >= 8 & hour(datetime) < 16
)
df_bpa <- df_bpa %>%
mutate(
hour = as.numeric(format(time_abx_prescribed, "%H")),
day_of_week = factor(day_of_week,
levels = c(1, 2, 3, 4, 5, 6, 7),
labels = c("Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday", "Sunday"))
)
ggplot(df_bpa, aes(x = hour, fill = day_of_week)) +
geom_histogram(binwidth = 1, position = "stack", color = "black") +
labs(
title = "Frequency of Prescriptions by Time of Day and Day of Week",
x = "Hour of Day",
y = "Frequency",
fill = "Day of Week"
) +
scale_x_continuous(breaks = 0:23) +
theme_minimal()ggplot(df_bpa, aes(x = hour)) +
geom_histogram(binwidth = 1, fill = "steelblue", color = "black") +
facet_wrap(~ day_of_week, ncol = 2) +
labs(
title = "Frequency of Prescriptions by Time of Day",
x = "Hour of Day",
y = "Frequency"
) +
scale_x_continuous(breaks = 0:23) +
theme_minimal() +
theme(
strip.text = element_text(size = 10, face = "bold")
)ggplot(df_bpa, aes(x = hour)) +
geom_histogram(binwidth = 1, fill = "steelblue", color = "black") +
#facet_wrap(~ day_of_week, ncol = 2) +
labs(
title = "Frequency of Prescriptions by Time of Day",
x = "Hour of Day",
y = "Frequency"
) +
scale_x_continuous(breaks = 0:23) +
theme_minimal() +
theme(
strip.text = element_text(size = 10, face = "bold")
)# Summarize counts by day of week and hour
heatmap_data <- df_bpa %>%
count(day_of_week, hour)
# Plot heatmap
ggplot(heatmap_data, aes(x = hour, y = day_of_week, fill = n)) +
geom_tile(color = "white") +
scale_fill_gradient(low = "lightgreen", high = "lightcoral") +
scale_x_continuous(breaks = seq(0,23, by =1)) +
labs(title = "Heatmap of Prescription Frequencies",
x = "Hour of Day", y = "Day of Week", fill = "Count") +
theme_minimal()# Create a table of counts by hour
hour_counts <- table(df_bpa$hour)
# Perform Chi-squared test
chisq_test <- chisq.test(hour_counts)
print(chisq_test)
Chi-squared test for given probabilities
data: hour_counts
X-squared = 120.87, df = 23, p-value = 2.902e-15
ggplot(df_bpa, aes(x = hour)) +
geom_density(fill = "skyblue", alpha = 0.5) +
labs(title = "Density Plot of Prescription Hours", x = "Hour of Day", y = "Density") +
theme_minimal()summary_time_rx <- df_bpa %>%
# group_by(enrolled) %>%
summarise(
mean_time = mean(time_rx_screen, na.rm = TRUE),
median_time = median(time_rx_screen, na.rm = TRUE),
sd_time = sd(time_rx_screen, na.rm = TRUE),
iqr_25 = quantile(time_rx_screen, 0.25, na.rm = TRUE),
iqr_75 = quantile(time_rx_screen, 0.75, na.rm = TRUE),
n = n()
)
print(summary_time_rx)# A tibble: 1 × 6
mean_time median_time sd_time iqr_25 iqr_75 n
<dbl> <dbl> <dbl> <dbl> <dbl> <int>
1 83.2 30 138. 12 69.5 276
ggplot(df_bpa, aes(x = enrolled, y = time_rx_screen, fill = enrolled)) +
geom_boxplot(outlier.shape = 16, outlier.colour = "red", alpha = 0.6) +
labs(
title = "Comparison of time_rx_screen by Enrollment Status",
x = "Enrollment Status",
y = "Time Rx Screen"
) +
theme_minimal() +
scale_fill_manual(values = c("Yes" = "steelblue", "No" = "salmon"))ggplot(df_bpa, aes(x = enrolled, y = time_rx_screen)) +
geom_boxplot() +
labs(
title = "Time from prescription to eligibility screening according to enrolment status",
x = 'Enrolled', y = 'Time (minutes)'
)# Density plot for time_rx_screen
ggplot(df_bpa, aes(x = time_rx_screen, fill = enrolled)) +
geom_density(alpha = 0.5) +
labs(
title = "Density Plot of time_rx_screen by Enrollment Status",
x = "Time Rx Screen",
y = "Density"
) +
theme_minimal() +
scale_fill_manual(values = c("Yes" = "steelblue", "No" = "salmon"))# Filter rows without missing time_rx_screen values
df_clean <- df_bpa %>% filter(!is.na(time_rx_screen))
# Summarize medians and IQR
summary_median <- df_clean %>%
group_by(enrolled) %>%
summarise(
median_time = median(time_rx_screen, na.rm = TRUE),
IQR_time = IQR(time_rx_screen, na.rm = TRUE),
n = n()
)
print(summary_median)# A tibble: 2 × 4
enrolled median_time IQR_time n
<chr> <dbl> <dbl> <int>
1 No 31.5 58.2 244
2 Yes 18.5 39.2 32
df_bpa <- df_bpa %>%
mutate(hour = as.numeric(format(time_abx_prescribed, "%H")),
minute = as.numeric(format(time_abx_prescribed, "%M")),
time_of_day = hour + minute / 60, # Convert to decimal hours
color_group = case_when(
(time_of_day > 9 & time_of_day < 13) ~ "Light Red", # 09:01 - 12:59
(time_of_day > 15 | time_of_day <= 7) ~ "Light Red", # 15:01 - 06:59
TRUE ~ "Light Green" # Rest of the time
))
# Scatterplot with conditional colors
ggplot(df_bpa, aes(x = time_of_day, y = 1, color = color_group)) +
geom_jitter(width = 0, height = 0.2, alpha = 0.7, size = 2) +
scale_color_manual(values = c("Light Red" = "lightcoral",
"Light Green" = "lightgreen")) +
labs(
title = "Scatter Plot of Prescription Times with Conditional Coloring",
x = "Time of Day (24-Hour Format)",
y = "Distribution (Jittered)",
color = "Time Group"
) +
scale_x_continuous(breaks = seq(0, 24, by = 1)) +
theme_minimal() +
theme(axis.text.y = element_blank(), # Remove Y-axis labels
axis.ticks.y = element_blank()) # Remove Y-axis tickscolor_proportions <- df_bpa %>%
group_by(color_group) %>%
summarise(count = n()) %>%
mutate(proportion = count / sum(count) * 100)
print(color_proportions)# A tibble: 2 × 3
color_group count proportion
<chr> <int> <dbl>
1 Light Green 42 15.2
2 Light Red 234 84.8
# Extract hour and minute from time_abx_prescribed and calculate decimal hours
df_bpa <- df_bpa %>%
mutate(hour = as.numeric(format(time_abx_prescribed, "%H")),
minute = as.numeric(format(time_abx_prescribed, "%M")),
time_of_day = hour + minute / 60) # Convert to decimal hours
# Scatter plot with enrolled vs non-enrolled coloring
ggplot(df_bpa, aes(x = time_of_day, y = 1, color = enrolled)) +
geom_jitter(width = 0, height = 0.2, alpha = 0.7, size = 2) +
scale_color_manual(values = c("Yes" = "lightgreen", "No" = 'white')) +
labs(
title = "Scatter Plot of Prescription Times by Enrollment Status",
x = "Time of Day (24-Hour Format)",
y = "Distribution (Jittered)",
color = "Enrollment Status"
) +
scale_x_continuous(breaks = seq(0, 24, by = 1)) +
theme_minimal() +
theme(axis.text.y = element_blank(), # Remove Y-axis labels
axis.ticks.y = element_blank()) # Remove Y-axis ticksSome fringe simulations
##### Experimental - simulations
df_bpa$time_abx_prescribed_time <- format(df_bpa$time_abx_prescribed, format = "%H:%M:%S")
df_bpa1 <- df_bpa %>%
mutate(event_datetime = as.POSIXct(paste(date_abx_prescribed, time_abx_prescribed_time), format = "%Y-%m-%d %H:%M:%S"))
# Filter for events within working hours (Monday-Friday, 09:00 to Friday 16:00)
df_bpa1 <- df_bpa1 %>%
filter(!(wday(event_datetime) %in% c(1, 7)), # Exclude Saturday (7) and Sunday (1)
!(wday(event_datetime) == 6 & hour(event_datetime) > 16), # Exclude Friday after 16:00
!(wday(event_datetime) == 2 & hour(event_datetime) < 8)) # Exclude Monday before 08:00
df_bpa1 <- df_bpa1 %>%
mutate(
event_date = as.Date(event_datetime),
# Add the two cross-sectional assessment times for each event's date
cross_sectional_09 = as.POSIXct(paste(event_date, "09:00:00"), format = "%Y-%m-%d %H:%M:%S"),
cross_sectional_14 = as.POSIXct(paste(event_date, "14:00:00"), format = "%Y-%m-%d %H:%M:%S"),
# Handle edge cases: If the event occurred after 14:00, the next cross-sectional assessment will be the next day's 09:00
cross_sectional_09_next_day = cross_sectional_09 + days(1),
cross_sectional_14_next_day = cross_sectional_14 + days(1),
# Assign the nearest cross-sectional time to each event
nearest_cross_sectional = case_when(
event_datetime <= cross_sectional_09 ~ cross_sectional_09,
event_datetime > cross_sectional_09 & event_datetime <= cross_sectional_14 ~ cross_sectional_14,
TRUE ~ cross_sectional_09_next_day
)
)
df_bpa1 <- df_bpa1 %>%
mutate(
# Time difference for real-time assessment
real_time_diff = time_rx_screen,
# Time difference for cross-sectional assessments
cross_sectional_diff = as.numeric(difftime(nearest_cross_sectional, event_datetime, units = "mins"))
)
library(dplyr)
df_bpa1 <- df_bpa1 %>%
mutate(
detected_real_time = real_time_diff <= 60, # Real-time detection within 1 hour
detected_cross_sectional = cross_sectional_diff <= 60 # Cross-sectional detection within 1 hour
)
results <- df_bpa1 %>%
summarise(
prop_detected_real_time = mean(detected_real_time, na.rm = TRUE),
prop_detected_cross_sectional = mean(detected_cross_sectional, na.rm = TRUE)
)
print(results)# A tibble: 1 × 2
prop_detected_real_time prop_detected_cross_sectional
<dbl> <dbl>
1 0.748 0.0675
results_long <- results %>%
pivot_longer(cols = everything(), names_to = "method", values_to = "proportion")
ggplot(results_long, aes(x = method, y = proportion, fill = method)) +
geom_bar(stat = "identity", position = "dodge") +
labs(title = "Comparison of Event Detection Proportions",
x = "Method",
y = "Proportion of Events Detected Within 1 Hour") +
theme_minimal()df_times <- df_bpa1[, c("real_time_diff", "cross_sectional_diff")]
library(mice)
# Perform multiple imputations
imputed_data <- mice(df_times, m = 5, method = "pmm", maxit = 50, seed = 123)
iter imp variable
1 1
1 2
1 3
1 4
1 5
2 1
2 2
2 3
2 4
2 5
3 1
3 2
3 3
3 4
3 5
4 1
4 2
4 3
4 4
4 5
5 1
5 2
5 3
5 4
5 5
6 1
6 2
6 3
6 4
6 5
7 1
7 2
7 3
7 4
7 5
8 1
8 2
8 3
8 4
8 5
9 1
9 2
9 3
9 4
9 5
10 1
10 2
10 3
10 4
10 5
11 1
11 2
11 3
11 4
11 5
12 1
12 2
12 3
12 4
12 5
13 1
13 2
13 3
13 4
13 5
14 1
14 2
14 3
14 4
14 5
15 1
15 2
15 3
15 4
15 5
16 1
16 2
16 3
16 4
16 5
17 1
17 2
17 3
17 4
17 5
18 1
18 2
18 3
18 4
18 5
19 1
19 2
19 3
19 4
19 5
20 1
20 2
20 3
20 4
20 5
21 1
21 2
21 3
21 4
21 5
22 1
22 2
22 3
22 4
22 5
23 1
23 2
23 3
23 4
23 5
24 1
24 2
24 3
24 4
24 5
25 1
25 2
25 3
25 4
25 5
26 1
26 2
26 3
26 4
26 5
27 1
27 2
27 3
27 4
27 5
28 1
28 2
28 3
28 4
28 5
29 1
29 2
29 3
29 4
29 5
30 1
30 2
30 3
30 4
30 5
31 1
31 2
31 3
31 4
31 5
32 1
32 2
32 3
32 4
32 5
33 1
33 2
33 3
33 4
33 5
34 1
34 2
34 3
34 4
34 5
35 1
35 2
35 3
35 4
35 5
36 1
36 2
36 3
36 4
36 5
37 1
37 2
37 3
37 4
37 5
38 1
38 2
38 3
38 4
38 5
39 1
39 2
39 3
39 4
39 5
40 1
40 2
40 3
40 4
40 5
41 1
41 2
41 3
41 4
41 5
42 1
42 2
42 3
42 4
42 5
43 1
43 2
43 3
43 4
43 5
44 1
44 2
44 3
44 4
44 5
45 1
45 2
45 3
45 4
45 5
46 1
46 2
46 3
46 4
46 5
47 1
47 2
47 3
47 4
47 5
48 1
48 2
48 3
48 4
48 5
49 1
49 2
49 3
49 4
49 5
50 1
50 2
50 3
50 4
50 5
# Choose one of the completed datasets
df_times <- complete(imputed_data, 1)
ggplot(df_times, aes(x = real_time_diff)) +
geom_histogram(binwidth = 50, fill = "blue", alpha = 0.7) +
labs(title = "Distribution of Real Time Differences", x = "Real Time Diff", y = "Frequency")ggplot(df_times, aes(x = cross_sectional_diff)) +
geom_histogram(binwidth = 50, fill = "green", alpha = 0.7) +
labs(title = "Distribution of Cross Sectional Differences", x = "Cross Sectional Diff", y = "Frequency")library(fitdistrplus)
# Fit distributions
fit_real_time <- fitdist(df_times$real_time_diff, "lnorm") # Example: gamma
# Summary of the fits
summary(fit_real_time)Fitting of the distribution ' lnorm ' by maximum likelihood
Parameters :
estimate Std. Error
meanlog 3.330683 0.10431079
sdlog 1.331751 0.07375868
Loglikelihood: -820.887 AIC: 1645.774 BIC: 1651.961
Correlation matrix:
meanlog sdlog
meanlog 1.000000e+00 3.061404e-09
sdlog 3.061404e-09 1.000000e+00
set.seed(123) # For reproducibility
# Simulate 1000 values for real_time_diff and cross_sectional_diff
sim_real_time <- rgamma(1000, shape = fit_real_time$estimate["shape"], rate = fit_real_time$estimate["rate"])
fit_real_time$estimate meanlog sdlog
3.330683 1.331751
summary(df_times$real_time_diff) Min. 1st Qu. Median Mean 3rd Qu. Max.
1.00 12.00 22.00 73.53 61.00 621.00
any(df_times$real_time_diff <= 0, na.rm = TRUE) # Check for non-positive values[1] FALSE
fit_real_time <- fitdist(df_times$real_time_diff, "gamma")
summary(fit_real_time)Fitting of the distribution ' gamma ' by maximum likelihood
Parameters :
estimate Std. Error
shape 0.629339091 0.058327967
rate 0.008621599 0.001146604
Loglikelihood: -849.987 AIC: 1703.974 BIC: 1710.162
Correlation matrix:
shape rate
shape 1.0000000 0.6810348
rate 0.6810348 1.0000000
library(mixtools)
fit <- normalmixEM(df_times$cross_sectional_diff, k = 2)number of iterations= 21
summary(fit)summary of normalmixEM object:
comp 1 comp 2
lambda 0.585493 0.414507
mu 194.625596 903.118105
sigma 98.929108 185.688352
loglik at estimate: -1129.279
# Number of samples to simulate
n <- 10000
# Simulate component membership based on mixing proportions
component <- sample(1:2, size = n, replace = TRUE, prob = fit$lambda)
# Simulate data based on the assigned components
sim_cross_sectional <- sapply(component, function(k) rnorm(1, mean = fit$mu[k], sd = fit$sigma[k]))
sim_real_time <- rgamma(10000, shape = fit_real_time$estimate["shape"], rate = fit_real_time$estimate["rate"])
wilcox.test(sim_cross_sectional, sim_real_time)
Wilcoxon rank sum test with continuity correction
data: sim_cross_sectional and sim_real_time
W = 90469820, p-value < 2.2e-16
alternative hypothesis: true location shift is not equal to 0
hist(sim_cross_sectional, breaks = 30, col = 'skyblue', freq = FALSE, main = "Density-Normalized Histograms")
hist(df_times$cross_sectional_diff, breaks = 60, col = rgb(1, 0, 0, 0.5), freq = FALSE, add = TRUE)
legend("topright", legend = c("Simulated times (n = 10000)", "Original times (n = 166)"),
fill = c("skyblue", rgb(1, 0, 0, 0.5)))hist(sim_real_time, breaks = 30, col = 'skyblue', freq = FALSE, main = "Density-Normalized Histograms")
hist(df_times$real_time_diff, breaks = 60, col = rgb(1, 0, 0, 0.5), freq = FALSE, add = TRUE)
legend("topright", legend = c("Simulated times (n = 10000)", "Original times (n = 166)"),
fill = c("skyblue", rgb(1, 0, 0, 0.5)))# Boxplots for a quick summary
boxplot(sim_cross_sectional, sim_real_time,
names = c("Cross Sectional", "Real Time"),
col = c("skyblue", "salmon"), main = "Boxplot Comparison")
# Extract the p-value from the test result
p_value <- "< 0.001"
# Add the p-value as text to the plot
text(x = 1.5, y = max(c(sim_cross_sectional, sim_real_time)) * 0.95,
labels = "Wilcoxon test P < 0.001")