Nothing
#### history ####
# 2025-01-26 first version
#' Calculate status of recruitment of a study
#'
#' Trial concept calculated: status of recruitment at the time of loading
#' the trial records. Maps the categories that are in fields which specify
#' the state of recruitment. Simplifies the status into three categories.
#'
#' Note that for EUCTR, `NA` is returned for "Trial now transitioned"
#' (into CTIS, from which the status can be obtained) and for
#' "GB - no longer in EU/EEA" (no data source known).
#'
#' @param df data frame such as from \link{dbGetFieldsIntoDf}. If `NULL`,
#' prints fields needed in `df` for calculating this trial concept, which can
#' be used with \link{dbGetFieldsIntoDf}.
#'
#' @returns data frame with columns `_id` and `.statusRecruitment`, which is
#' a factor with levels `ongoing` (includes active, not yet recruiting;
#' temporarily halted; suspended; authorised, not started and similar),
#' `completed` (includes ended; ongoing, recruitment ended),
#' `ended early` (includes prematurely ended, terminated early) and
#' `other` (includes revoked, withdrawn, planned, stopped).
#'
#' @export
#'
#' @importFrom dplyr if_else mutate case_when case_match pull `%>%`
#' @importFrom rlang .data
#'
#' @examples
#' # fields needed
#' f.statusRecruitment()
#'
#' # apply trial concept when creating data frame
#' dbc <- nodbi::src_sqlite(
#' dbname = system.file("extdata", "demo.sqlite", package = "ctrdata"),
#' collection = "my_trials", flags = RSQLite::SQLITE_RO)
#' trialsDf <- dbGetFieldsIntoDf(
#' calculate = "f.statusRecruitment",
#' con = dbc)
#' trialsDf
#'
f.statusRecruitment <- function(df = NULL) {
# check generic, do not edit
stopifnot(is.data.frame(df) || is.null(df))
#### fields ####
fldsNeeded <- list(
"euctr" = c(
"trialInformation.globalEndOfTrialPremature",
"trialInformation.isGlobalEndOfTrialReached",
"p_end_of_trial_status"
#
# in some trials, e.g. 2014-003556-31-GB,
# this fields includes a text description
# while p_end_of_trial_status is empty:
# "subjectDisposition.recruitmentDetails"
),
"ctgov" = c(
# http://clinicaltrials.gov/ct2/html/images/info/public.xsd
"last_known_status",
"overall_status"
),
"ctgov2" = c(
# https://clinicaltrials.gov/data-api/about-api/study-data-structure#enum-Status
# https://clinicaltrials.gov/policy/protocol-definitions#OverallStatus
"protocolSection.statusModule.overallStatus"
),
"isrctn" = c(
"participants.recruitmentEnd",
"participants.recruitmentStart",
"participants.recruitmentStatusOverride"
),
"ctis" = c(
# "decisionDate",
"authorizedApplication.memberStatesConcerned.mscName", # for counting notifications
"mscTrialNotificationsInfoList.mscNotificationsListInfo.notificationType", # CTIS1 e.g. early termination
"events.trialEvents.events.notificationType", # CTIS2, e.g. early termination
"ctPublicStatusCode", # ctPublicStatusCode is in CTIS1 and CTIS2
"ctStatus" # text in CTIS1 but in CTIS2, same as ctPublicStatusCode
))
#### describe ####
if (is.null(df)) {
# generic, do not edit
return(fldsNeeded)
} # end describe
#### calculate ####
# check generic, do not edit
df <- fctChkFlds(df, fldsNeeded)
# helper function
`%>%` <- dplyr::`%>%`
#### . EUCTR ####
df %>% dplyr::mutate(
helper = dplyr::case_when(
.data$trialInformation.globalEndOfTrialPremature ~ "Prematurely Ended",
.data$trialInformation.isGlobalEndOfTrialReached ~ "Completed",
.default = as.character(.data$p_end_of_trial_status)
),
euctr = tolower(.data$helper)
) -> df
#### . CTGOV ####
df %>% dplyr::mutate(
helper = as.character(
# http://clinicaltrials.gov/ct2/html/images/info/public.xsd
# "If a study is open (overall status of recruiting, not yet recruiting, or expanded
# access available), the overall status is supposed to be verified occasionally, and
# updated when it changes. If the status has not been verified in 2 years, and the
# current date is past the study primary completion date or completion date, then the
# status is inconsistent. In this case, the inconsistent value is kept in the new
# last_known_status tag and the overall_status tag is changed to "Unknown status"."
#
# type is logical if all NA
dplyr::if_else(
!is.na(.data$last_known_status),
.data$last_known_status,
.data$overall_status)
),
ctgov = tolower(.data$helper)
) -> df
#### . CTGOV2 ####
df$ctgov2 <- tolower(as.character(
# type is logical if all NA
# "trial is terminated (that is, stopped prematurely)"
df$protocolSection.statusModule.overallStatus))
#### . ISRCTN ####
df %>% dplyr::mutate(
helper = dplyr::case_when(
!is.na(.data$participants.recruitmentStatusOverride) ~
as.character(.data$participants.recruitmentStatusOverride),
Sys.Date() > .data$participants.recruitmentEnd ~ "Completed",
Sys.Date() < .data$participants.recruitmentStart ~ "Planned",
.data$participants.recruitmentEnd > .data$participants.recruitmentStart ~ "Ongoing"
),
isrctn = tolower(.data$helper)
) -> df
#### . CTIS ####
df %>% dplyr::mutate(
helper_ctPublicStatusCode = dplyr::case_match(
as.integer(.data$ctPublicStatusCode),
1 ~ "Under evaluation",
2 ~ "Authorised, recruitment pending",
3 ~ "Authorised, recruiting",
4 ~ "Ongoing, recruiting",
5 ~ "Ongoing, recruitment ended",
6 ~ "Temporarily halted",
7 ~ "Suspended",
8 ~ "Ended", # includes early termination
9 ~ "Expired",
10 ~ "Revoked",
11 ~ "Not authorised",
12 ~ "Cancelled"
),
helper_event1 = sapply(
.data$mscTrialNotificationsInfoList.mscNotificationsListInfo.notificationType,
function(i) grepl("^(|Global end of trial / )Early Termination", i, ignore.case = TRUE)
),
helper_event2 = sapply(
.data$events.trialEvents.events.notificationType,
function(i) stringi::stri_count_fixed(paste0(i, collapse = " "), "EARLY_TERMINATION")
),
helper_event3 = sapply(
stringi::stri_split_fixed(
.data$authorizedApplication.memberStatesConcerned.mscName,
" / "), function(i) if (all(is.na(i))) NA else length(i)
),
helper = dplyr::case_when(
.data$helper_event1 ~ "terminated early",
.data$helper_event2 == .data$helper_event3 ~ "terminated early",
is.na(.data$helper_ctPublicStatusCode) &
!is.na(.data$ctStatus) ~ as.character(.data$ctStatus),
.default = as.character(.data$helper_ctPublicStatusCode)
),
ctis = tolower(.data$helper)
) -> df
#### mapping ####
# merge, last update 2025-08-21
mapped_values <- list(
"ongoing" = c(
"active", "available", "ongoing, not yet recruiting",
"authorised, recruitment pending", "authorised, not started",
"enrolling by invitation", "enrolling_by_invitation",
"ongoing", "ongoing, recruiting", "recruiting", "authorised, recruiting",
"restarted", "suspended", "temporarily halted", "temporarily_not_available"
),
#
"completed" = c(
"completed", "ended", "ongoing, recruitment ended"
),
#
"ended early" = c(
"prematurely ended", "terminated early", "terminated"
),
#
"other" = c(
"active, not recruiting", "active_not_recruiting",
"not yet recruiting", "not_yet_recruiting",
"cancelled", "expired", "gb - no longer in eu/eea",
"no longer available", "no_longer_available",
"not authorised", "revoked", "stopped",
"trial now transitioned", "under evaluation",
"unknown", "withdrawn", "withheld"
)
)
# check for unmapped values:
# setdiff(unique(dfMergeVariablesRelevel(df, names(fldsNeeded))), unlist(mapped_values))
# merge into vector (factor)
df[[".statusRecruitment"]] <- dfMergeVariablesRelevel(
df = df,
colnames = names(fldsNeeded)[names(fldsNeeded) != ""],
levelslist = mapped_values
)
# keep only outcome columns
df <- df[, c("_id", ".statusRecruitment"), drop = FALSE]
#### checks ####
stopifnot(inherits(df[[".statusRecruitment"]], "factor"))
stopifnot(ncol(df) == 2L)
# return
return(df)
} # end f.statusRecruitment
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.