diff --git a/DESCRIPTION b/DESCRIPTION index a0a3cbb..e6a7707 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -9,7 +9,7 @@ Description: An R package designed to help health economic modellers when writin License: MIT + file LICENSE Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.2.3 +RoxygenNote: 7.3.1 Suggests: testthat (>= 3.0.0) Config/testthat/edition: 3 @@ -17,4 +17,5 @@ Imports: assertthat, ggplot2, scales, - tidyr + tidyr, + stringr diff --git a/NAMESPACE b/NAMESPACE index c1f2bc0..e23f960 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -4,7 +4,21 @@ export(check_init) export(check_markov_trace) export(check_trans_prob_array) export(check_trans_prob_mat) +export(extract_function_name) +export(find_next_vector_element) +export(find_previous_vector_element) +export(get_active_functions) +export(get_file_cheers_classifications) +export(get_folder_cheers_classifications) +export(get_summary_table) +export(list_functions_in_script) export(plot_PSA_stability) +export(tabulate_functions_in_folder) +export(tabulate_functions_in_folder_with_tests) +export(tabulate_functions_in_script) import(assertthat) +importFrom(stringr,str_locate_all) +importFrom(stringr,str_replace) +importFrom(stringr,str_replace_all) importFrom(tidyr,pivot_longer) importFrom(utils,capture.output) diff --git a/R/check_functions.R b/R/check_functions.R new file mode 100644 index 0000000..265f66d --- /dev/null +++ b/R/check_functions.R @@ -0,0 +1,339 @@ +#' @title List all of the functions in a script +#' +#' @description This function lists all of the functions in a script. It expects the script to be +#' an R file, and requires that package libraries are loaded when identifying packages. +#' +#' @param filename The name of the file to be checked. +#' @param alphabetic If TRUE, return the functions in alphabetic order. +#' @param by_package If TRUE, return a list of functions by package. Else return a vector of functions. +#' +#' @return A named list of functions in the script, by package. +#' +#' @family checking_functions +#' +#' @export +#' @examples +#' \dontrun{ +#' list_functions_in_script(filename = "./R/check_trans_probs.R") +#' +#' list_functions_in_script(filename = "./R/check_functions.R", by_package = FALSE) +#' list_functions_in_script(filename = +#' paste0("https://raw.githubusercontent.com/dark-peak-analytics/", +#' "sicksickerPack/main/R/create_Markov_trace.R"), by_package = FALSE) +#' +#' } +list_functions_in_script <- + function(filename, + by_package = TRUE, + alphabetic=TRUE) { + + # from hrbrmstr, StackExchange 3/1/2015 + #if(!file.exists(filename)) { stop("couldn't find file ",filename) } + if(!tools::file_ext(filename) == "R") { warning("expecting *.R file, will try to proceed") } + # read in parsed data from script + tmp <- utils::getParseData(parse(filename, keep.source=TRUE)) + # only keep those lines that are identified as function calls + nms <- tmp$text[which(tmp$token=="SYMBOL_FUNCTION_CALL")] + # only keep unique functions for script + funs <- unique(if(alphabetic) { sort(nms) } else { nms }) + + # if don't want by package just return vector of function names + if(!by_package) { + return(funs) + } + + # return a list of functions and the file they were found in + # take the first only + v_packages <- as.vector(sapply(funs, utils::find)) + src <- paste(sapply(X = v_packages, FUN = function(x) x[1])) + outlist <- tapply(funs, factor(src), c) + + return(outlist) + +} + + +#' Create table of all of the functions in a script +#' +#' This function tabulates all of the functions in a script with the name of the +#' package next to it. It expects the script to be an R file, and requires that +#' package libraries are loaded when identifying packages. +#' +#' @param filename The name of the file to be checked. +#' @param packages_to_exclude A vector of packages to exclude from the output (e.g. "base") +#' +#' @return A data-frame of functions and the package the function is from. +#' +#' @family checking_functions +#' +#' @export +#' @examples +#' \dontrun{ +#' tabulate_functions_in_script(filename = "./R/check_functions.R") +#' tabulate_functions_in_script(filename = "./R/check_trans_probs.R", +#' packages_to_exclude = NULL) +#' tabulate_functions_in_script(filename = +#' paste0("https://raw.githubusercontent.com/dark-peak-analytics/", +#' "sicksickerPack/main/R/create_Markov_trace.R"), packages_to_exclude = NULL) +#' } +tabulate_functions_in_script <- function(filename, + packages_to_exclude = c("base", "stats", "utils")) { + # list the functions in the file. + my_packages <- list_functions_in_script(filename) + + # convert nested list to a dataframe where each row gives the function name + # and the package it belongs to: + df <- utils::stack(my_packages) + colnames(df) <- c("foo", "package") + + # remove 'package:' from the strings + df$package <- gsub("package:", "", df$package) + + # remove 'character(0)' and replace with 'local' + df$package[df$package == "character(0)"] <- "unknown" + + # exclude unwanted packages (to reduce size) + exclude_index <- df$package %in% packages_to_exclude + df <- df[!exclude_index, ] + + return(df) +} + + +#' Create table of all of the functions identified in a project folder +#' +#' This function tabulates all of the functions identified in R scripts within a +#' project folder. It requires that package libraries are loaded when identifying +#' a function's package. +#' +#' @param path The path to the folder to be checked. +#' @param collapse If TRUE, return a single data-frame of all functions. Else return a list by file. +#' @param packages_to_exclude A vector of packages to exclude from the output (e.g. "base") +#' @param path_exclude A string which if found in file path removes file from analysis. +#' Defaults to 'testthat/' to exclude functions only found in tests. +#' +#' @family checking_functions +#' +#' @return Either a data-frame of functions and the package the function is from, or a list of functions by file. +#' @export +#' @examples +#' +#' \dontrun{ +#' tabulate_functions_in_folder( +#' path = ".", +#' path_exclude = "testthat/", +#' collapse = T, +#' packages_to_exclude = c("base", "stats", "utils") +#' ) +#' } +tabulate_functions_in_folder <- function(path = ".", + path_exclude = "testthat/", + collapse = T, + packages_to_exclude = c("base", "stats", "utils")) { + # get all files from the path folder, i.e. everything in repo. + my_R_scripts <- list.files( + path = path, + pattern = "\\.R$", + recursive = TRUE, + full.names = TRUE + ) + + # exclude those in the testthat (or other specified) folder if not null + if(!is.null(path_exclude)){ + my_R_scripts <- my_R_scripts[!grepl(pattern = path_exclude, x = my_R_scripts)] + } + + l_foo <- lapply(X = my_R_scripts, + FUN = tabulate_functions_in_script, + packages_to_exclude = packages_to_exclude) + + # collapse the list into a single dataframe + if (collapse) { + df_foo <- do.call(rbind, l_foo) + # remove duplicates + df_foo <- unique(df_foo) + return(df_foo) + } else{ + names(l_foo) <- my_R_scripts + return(l_foo) + } + +} + +#' Find test for a function in a codebase +#' +#' This function finds the test for each in a vector of functions in a specified +#' testing folder, default = tests/testthat as the relative path from the project folder (path). +#' +#' @param v_functions A vector of functions to search for. +#' @param path The path to the folder to be checked. +#' @param test_path The relative path to the test folder from the project folder (path). +#' +#' @family checking_functions +#' +#' @return A vector of file paths to the test scripts for each function. +#' Returns NA where no script can be found. +#' +#' @examples +#' \dontrun{ +#' v_funcs_to_find_tests_for <- c("check_init", "mean", "check_markov_trace", "find_test") +#' find_test(v_functions = v_funcs_to_find_tests_for, +#' path = ".") +#' } +#' +find_test <- function(v_functions, + path = ".", + test_path = "tests/testthat") { + # get all files from the path folder, i.e. everything in repo. + my_test_scripts <- list.files( + path = paste0(path, "/", test_path), + pattern = "\\.R$", + recursive = TRUE, + full.names = TRUE + ) + # create a list of functions in scripts. + l_foo <- lapply(X = my_test_scripts, + FUN = list_functions_in_script, + by_package = FALSE) + names(l_foo) <- my_test_scripts + + # loop through the vector of function names and check if they exist in the list of scripts. + l_function_tests <- sapply(v_functions, + function(function_name) { + # find which elements of the list contain the function + v_test_file <- names(l_foo)[sapply(l_foo, + function(x) + (function_name %in% x) > 0)] + + # check that a file exists + if (length(v_test_file) == 0) { + return(NA) + } else if (length(v_test_file) > 1) { + file_path_match <- + grep(pattern = function_name, + x = v_test_file, + value = TRUE) + if (length(file_path_match) == 1) { + return(file_path_match) + } else{ + return(v_test_file[1]) + } + } else { + return(v_test_file) + } + + }) + + + return(l_function_tests) + +} + + +#' Summarise project functions with details on packages and existence of unit-tests. +#' +#' Creates a summary table containing the name of each function in the project, +#' the package it is from, whether it has a unit-test and the file in which it is tested. +#' +#' @param path The path to the folder to be checked. +#' @param path_exclude A set of strings that will exclude any files if it is present in the file path. +#' @param packages_to_exclude A vector of packages to exclude from the search. +#' @param test_path The relative path to the test folder from the project folder (path). +#' @return A dataframe with the following columns: +#' * function_name: The name of the function. +#' * package: The package the function is from. +#' * file_name: The file in which the function is defined. +#' * test_location: The file in which the function is tested. +#' @export +#' +#' @examples +#' \dontrun{ +#' tabulate_functions_in_folder_with_tests( +#' path = ".", +#' path_exclude = "testthat/", +#' packages_to_exclude = c("base", "stats", "ggplot2"), +#' test_path = "tests/testthat" +#' ) +#' } +#' +tabulate_functions_in_folder_with_tests <- + function(path = ".", + path_exclude = "testthat/", + packages_to_exclude = .packages(TRUE), + test_path = "tests/testthat" + ) { + + # get list of functions (excluding those defined) + df_foo <- tabulate_functions_in_folder( + path = path, + path_exclude = path_exclude, + collapse = T, + packages_to_exclude = packages_to_exclude + ) + + # attempt to identify the testing file for the function + df_foo$test_location <- find_test( + v_functions = df_foo$foo, + path = path, + test_path = test_path + ) + + return(df_foo) + + } + + +#' @title Summarise project functions with details on packages and existence of unit-tests. +#' @description Creates a summary table containing the name of each function in the project, +#' the package it is from, whether it has a unit-test and the file in which it is tested. +#' @param path The path to the folder to be checked. +#' @param path_exclude A set of strings that will exclude any files if it is present in the file path. +#' @param packages_to_exclude A vector of packages to exclude from the search. +#' @param test_path The relative path to the test folder from the project folder (path). +#' @param cheers_pattern A string that will be used to identify the cheers tag. +#' @return A dataframe with the following columns: +#' * function_name: The name of the function. +#' * package: The package the function is from. +#' * file_name: The file in which the function is defined. +#' * test_location: The file in which the function is tested. +#' @export +#' @examples +#' \dontrun{ +#' get_summary_table() +#' } +#' +get_summary_table <- function(path = ".", + path_exclude = "testthat/", + test_path = "tests/", + cheers_pattern = "@family", + packages_to_exclude = c("base", "stats", "utils")) { + + # table with list of functions, packages and tests. + df_tests <- tabulate_functions_in_folder_with_tests(path = path, + path_exclude = path_exclude, + test_path = test_path, + packages_to_exclude = packages_to_exclude) + + # tags relating to classifications and file paths + df_cheers <- get_folder_cheers_classifications(path = path, + cheers_pattern = cheers_pattern, + path_ignore = path_exclude) + + # merge dataframes together to get one summary dataframe + df_merged <- merge(df_tests, + df_cheers, + by.x = "foo", + by.y = "function_name", + all = T) + + # sort by tag in base R + column_names <- + c("foo", "tag", "package", "script", "test_location") + df_summary <- df_merged[order(df_merged$tag), column_names] + + return(df_summary) + +} + + + diff --git a/R/cheers_checker.R b/R/cheers_checker.R new file mode 100644 index 0000000..e9260ae --- /dev/null +++ b/R/cheers_checker.R @@ -0,0 +1,272 @@ +#' @title Find the next element of the vector after a value +#' @description Find the next element of the vector after a value +#' @param value A value of numeric values +#' @param vector A vector of numeric values +#' @return The next element of the vector after the value +#' @export +#' @examples +#' \dontrun{ +#' find_next_vector_element(value = 5, vector = c(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) +#' } +#' +find_next_vector_element <- function(value, vector) { + # Find the elements in the vector that are greater than the specified value + greater_than_value <- vector[vector > value] + + # If there are no elements greater than the specified value, return NULL or a default value + if (length(greater_than_value) == 0) { + return(max(vector)) # or return a default value as needed + } + + # Find the minimum value among the elements greater than the specified value + next_element <- min(greater_than_value) + + return(next_element) +} + + +#' @title Find the previous element of the vector before a value +#' @description Find the previous element of the vector before a value +#' @param value A value of numeric values +#' @param vector A vector of numeric values +#' @return The previous element of the vector before the value +#' @export +#' @examples +#' \dontrun{ +#' find_previous_vector_element(value = 5, vector = c(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) +#' } +#' +find_previous_vector_element <- function(value, vector){ + # Find the elements in the vector that are less than the specified value + less_than_value <- vector[vector < value] + + # If there are no elements less than the specified value, return the value + if (length(less_than_value) == 0) { + return(value) + } + + # Find the maximum value among the elements less than the specified value + previous_element <- max(less_than_value) + + return(previous_element) + } + + + +#' @title Extract function name from a string +#' +#' @description Extract function name from a long string. This works by +#' identifying "function(" in the string and then finding the operand before and splitting +#' on that before keeping the character there. +#' +#' @param string A string containing a function definition, this must contain the word 'function' +#' +#' @return A string containing the function name +#' @importFrom stringr str_locate_all str_replace_all +#' +#' @export +#' +#' +extract_function_name <- function(string){ + + # regex pattern to match comments (note: greedy match with '?') + # assumes comments won't appear in quoted strings (i.e. print("this # will match") ) + pattern <- "#.*?\\n" + + # Replace the comment (to end of line) with an empty string + string <- gsub(pattern, "", string, perl = TRUE) + + # Convert newlines to spaces (remove newlines) + string <- stringr::str_replace_all(string, pattern = c("\n"), replacement = " ") + + assign_op <- stringr::str_locate_all(string, pattern = "(=|<-)\\s*function\\s*\\(") + assign_op <- unlist(x = assign_op) + assign_op <- assign_op[1] + + string <- substr(string, 1, assign_op-1) + string <- gsub("\\s*", "", string, perl = TRUE) + + return(string) + +} + +#assertHE::extract_function_name(string = "#' } +# +#create_Markov_trace <- function(transition_matrix_,") + + + + +#' @title Get cheers classification tags from a given file +#' @description For a provided filepath, identify the cheers classification tags +#' and the function names that follow them. +#' @param filename A string containing the filepath to the file to be checked +#' @param cheers_pattern A string containing the roxygen tag for cheers which is used as an identifier +#' @param function_pattern A string containing the pattern to identify functions +#' @return A list containing the cheers tags and the function names that follow them +#' @family cheers +#' @importFrom stringr str_replace str_replace_all +#' +#' @export +#' +get_file_cheers_classifications <- function(filename, + cheers_pattern, + function_pattern = "(\\s|=|-)function\\("){ +lines <- NULL +# check files exists +#if(file.exists(filename)) +lines <- readLines(filename) #else stop("Not a file") + +# find the rows on which the cheers pattern occurs +cheers_indices <- + sapply(X = lines, + FUN = function(line) grepl(pattern = cheers_pattern, x = line) + ) |> which() + +# remove the cheers pattern from that row name +cheers_indices <- stats::setNames( + object = cheers_indices, + nm = stringr::str_replace( + string = stringr::str_replace_all( + string = names(cheers_indices), + pattern = " ", + replacement = "" + ), + pattern = paste0("#'", cheers_pattern), + replacement = "" + ) +) + +# find the row number for each function +function_indices <- + sapply(X = lines, + FUN = function(line) grepl(pattern = function_pattern, x = line) + ) |> which() + + +# for each cheers row, find the next function row. +next_function_index <- sapply(X = cheers_indices, + FUN = find_next_vector_element, + vector = function_indices) + +if(length(as.vector(stats::na.omit(next_function_index))) == 0) return(NA) + +# for each function identified after a cheers tag, extract the function name +v_function_names <- sapply( + X = next_function_index, + FUN = function(function_index) { + + function_index_minus_one <- max(c(function_index - 1, 1)) + + function_line <- + lines[function_index_minus_one:function_index] |> + paste(collapse = "\n") + + return(extract_function_name(function_line)) + + } +) + +if(length(v_function_names) == 0) return(NA) + +# return the function names as a vector, named with the cheers tag. +return(v_function_names) + +} + + +#' @title Get cheers classification tags from a given folder +#' +#' @description For a provided folder path, identify the cheers classification tags +#' and the function names that follow them. +#' +#' @param path A string containing the filepath to the folder to be checked +#' @param cheers_pattern A string containing the roxygen tag for cheers which is used as an identifier +#' @param path_ignore A string containing the pattern to identify files to ignore +#' +#' @return A list containing the cheers tags and the function names that follow them +#' +#' @family cheers +#' +#' @export +#' +get_folder_cheers_classifications <- function(path, + cheers_pattern, + path_ignore = "tests/"){ + +my_R_scripts <- list.files( + path = path, + pattern = "\\.R$", + recursive = TRUE, + full.names = TRUE +) + +my_R_scripts <- my_R_scripts[!grepl(x = my_R_scripts, pattern = path_ignore) ] + +tmp <- lapply(X = my_R_scripts, + FUN = get_file_cheers_classifications, + cheers_pattern = cheers_pattern) + +names(tmp) <- my_R_scripts + +df_foos <- do.call(rbind.data.frame, + lapply( + tmp, + FUN = function(x) { + if (all(!is.na(x))) + utils::stack(x, drop = T) + else + NULL + } + )) + +df_foos$script <- rownames(df_foos) +rownames(df_foos) <- NULL + +if(ncol(df_foos) == 3){ +colnames(df_foos) <- c("function_name", "tag", "script") +} + +return(df_foos) + +} + + + + +#' @title get all active functions that exist in the global environment +#' +#' @description get all active functions that exist in the global environment +#' +#' @param packages a vector containing the names of packages to include in the search +#' +#' @return a vector containing the names of all active functions in the global environment +#' +#' @export +get_active_functions <- function(packages = "assertHE") { + # set environment to global + v_global_objects <- ls(envir = .GlobalEnv) + + v_global <- v_global_objects[sapply(v_global_objects, + function(x) + is.function(x = get(x)))] + + v_packages_include <- packages[!packages %in% c(".GlobalEnv", "NA", NA)] + + v_packages <- v_packages_include |> + lapply( + FUN = function(x) { + ls(eval(paste0("package:", x)))[ + sapply(ls(eval(paste0("package:", x))), + FUN = function(x) { + is.function(x = get(x)) + } + )] + } + ) |> + unlist() |> + unique() + + return(c(v_global, v_packages)) + +} diff --git a/inst/example_script/create_markov_trace.R b/inst/example_script/create_markov_trace.R new file mode 100644 index 0000000..6155ca6 --- /dev/null +++ b/inst/example_script/create_markov_trace.R @@ -0,0 +1,99 @@ +#' Create Markov Trace +#' +#' @description Create a Markov trace for a State-Transition Model (STM) Markov +#' providing a transition matrix, time horizon, states' names and the starting +#' state-occupancy. +#' +#' @param transition_matrix_ Numeric matrix containing the model's transition +#' matrix. +#' @param time_horizon_ Numeric scalar defining the number of cycles in the +#' model. +#' @param states_nms_ Character vector containing the names of the Markov model +#' states. +#' @param initial_trace_ Named numeric vector describing the states' occupancy +#' in the first cycle of the model. +#' +#' @return A matrix containing the Markov trace. +#' +#' @family simulation +#' +#' @export +#' +#' @examples +#' \dontrun{ +#' library("sicksickerPack") +#' transition_matrix <- matrix( +#' data = c(0.845, 0.15, 0, 0.005, +#' 0.5, 0.3800749, 0.105, 0.01492512, +#' 0, 0, 0.9511101, 0.04888987, +#' 0, 0, 0, 1), +#' nrow = 4, +#' byrow = TRUE, +#' dimnames = list( +#' c("H", "S1", "S2", "D"), +#' c("H", "S1", "S2", "D") +#' ) +#' ) +#' +#' Markov_trace <- create_Markov_trace( +#' transition_matrix_ = transition_matrix, +#' time_horizon_ = 5, +#' states_nms_ = c("H", "S1", "S2", "D"), +#' initial_trace_ = c("H" = 1, "S1" = 0, "S2" = 0, "D" = 0) +#' ) +#' } +create_Markov_trace <- function(transition_matrix_, + time_horizon_, + states_nms_, + initial_trace_) { + ## Sanity testing - inputs: + + # confirm inputs are of correct type + assertthat::assert_that( + is.vector(x = states_nms_, mode = "character"), + msg = paste( + "The states_nms_ argument is not of class character" + ) + ) + assertthat::assert_that( + is.vector(x = initial_trace_, mode = "numeric"), + msg = paste( + "The initial_trace_ argument is not of class numeric" + ) + ) + # confirm inputs are concordant + assertthat::assert_that( + all(length(states_nms_) == length(initial_trace_), + length(states_nms_) == nrow(transition_matrix_), + ncol(transition_matrix_) == nrow(transition_matrix_)), + msg = paste( + "The number of states in the trace or transition matrix do not match the", + "number of named states" + ) + ) + + ## Markov trace: + + # create empty Markov trace + Markov_trace <- matrix( + data = NA, + nrow = time_horizon_, + ncol = length(states_nms_), + dimnames = list( + 1:time_horizon_, + states_nms_) + ) + + # initialize Markov trace + Markov_trace[1, ] <- initial_trace_ + + # loop throughout the number of cycles + for (t in 2:time_horizon_) { + + # estimate cycle of Markov trace + Markov_trace[t, ] <- Markov_trace[t - 1, ] %*% transition_matrix_ + + } + + return(Markov_trace) +} diff --git a/inst/example_script/define_transition_matrix.R b/inst/example_script/define_transition_matrix.R new file mode 100644 index 0000000..ff48752 --- /dev/null +++ b/inst/example_script/define_transition_matrix.R @@ -0,0 +1,102 @@ +#' Define a transition matrix +#' +#' @description Define the transition matrix of a State-Transition Markov (STM) +#' Model. +#' +#' @param states_nms_ A character vector containing the names of the states of a +#' Markov model. +#' @param transition_probs_ A numeric vector containing the transition +#' probabilities of length `n x n`, where `n` is the length of, number of names +#' in, the `states_nms_` vector. +#' +#' @return An `n x n`, where `n` is the number of states in an STM model, +#' containing the transition probabilities between the model states. +#' +#' @export +#' +#' @family transitions +#' +#' @examples +#' \dontrun{ +#' library("sicksickerPack") +#' define_transition_matrix( +#' states_nms_ = c("A", "B"), +#' transition_probs_ = c( +#' 0.2, 0.8, # transitions from state A -> A and A -> B +#' 0, 1 # transitions from state B -> A and B -> B +#' ) +#' ) +#' } +define_transition_matrix <- function(states_nms_, + transition_probs_) { + ## Sanity testing - inputs: + + # confirm names vector is of class character, n = length(states_nms_) + assertthat::assert_that( + is.vector(x = states_nms_, mode = "character"), + msg = paste( + "The states_nms_ argument is not of class character" + ) + ) + # confirm transition probabilities vector is of class numeric + assertthat::assert_that( + is.vector(x = transition_probs_, mode = "numeric"), + msg = paste( + "The transition_probs_ argument is not of class numeric" + ) + ) + # ensure all transition probabilities are between 0 and 1 + assertthat::assert_that( + all(transition_probs_ >= 0, transition_probs_ <= 1), + msg = paste( + "One or more of the values passed to the transition_probs_ argument are", + "not between 0 and 1." + ) + ) + # confirm transition probabilities vector is, n = sqrt(transition_probs_) + assertthat::assert_that( + length(states_nms_) == sqrt(length(transition_probs_)), + msg = paste( + "Please pass", + length(transition_probs_), + "probabilties for the transition between the", + length(states_nms_), + "markov states." + ) + ) + + ## Construct the transition probabilities' matrix: + + # fill matrix with transition probabilities + tranistion_matrix <- matrix( + data = transition_probs_, + nrow = length(states_nms_), + ncol = length(states_nms_), + byrow = TRUE, + dimnames = list( + states_nms_, + states_nms_ + ) + ) + + ## Sanity testing - outputs: + + # check tranistion_matrix rows add up to 1 + assertthat::assert_that( + all(round(rowSums(tranistion_matrix), digits = 5) == 1), + msg = paste( + "Transition probabilities from the", + sub( + pattern = ",([^,]*)$", + replacement = " &\\1", + paste( + which(round(rowSums(tranistion_matrix), digits = 5) != 1) |> names(), + collapse = ", " + ) + ), + "state(s) do not add up to 1." + ) + ) + + return(tranistion_matrix) +} diff --git a/inst/example_script/example_script.R b/inst/example_script/example_script.R new file mode 100644 index 0000000..fa5b6b3 --- /dev/null +++ b/inst/example_script/example_script.R @@ -0,0 +1,185 @@ +rm(list = ls()) + +# basic information: +Strategies <- c("No Treatment", "Treatment") # strategy names +n_age_init <- 25 # age at baseline +n_age_max <- 55 # maximum age of follow up +n_t <- n_age_max - n_age_init # time horizon, number of cycles +d_r <- 0.035 # equal discount of costs and QALYs by 3% + +# Transition probabilities (per cycle) +p_HD <- 0.005 # probability to die when healthy +p_HS1 <- 0.15 # probability to become sick when healthy +p_S1H <- 0.5 # probability to become healthy when sick +p_S1S2 <- 0.105 # probability to become sicker when sick +hr_S1 <- 3 # hazard ratio of death in sick vs healthy +hr_S2 <- 10 # hazard ratio of death in sicker vs healthy + +# Cost and utility inputs +c_H <- 2000 # cost of remaining one cycle in the healthy state +c_S1 <- 4000 # cost of remaining one cycle in the sick state +c_S2 <- 15000 # cost of remaining one cycle in the sicker state +c_Trt <- 12000 # cost of treatment(per cycle) +c_D <- 0 # cost of being in the death state +u_H <- 1 # utility when healthy +u_S1 <- 0.75 # utility when sick +u_S2 <- 0.5 # utility when sicker +u_D <- 0 # utility when dead +u_Trt <- 0.95 # utility when being treated + +# stm_1_probs_calcs + +# rate of death in healthy +r_HD <- - log(1 - p_HD) + +# rate of death in sick +r_S1D <- hr_S1 * r_HD +# rate of death in sicker +r_S2D <- hr_S2 * r_HD + +# probability of death in sick +p_S1D <- 1 - exp(-r_S1D) +# probability of death in sicker +p_S2D <- 1 - exp(-r_S2D) + +# stm_1_discweight + +# calculate discount weight for each cycle +v_dwe <- v_dwc <- 1 / (1 + d_r) ^ (0:(n_t-1)) # discount weight (equal discounting is assumed for costs and effects) + + +# stm_1_names + +v_n <- c("H", "S1", "S2", "D") # the 4 states of the model: Healthy (H), Sick (S1), Sicker (S2), Dead (D) +n_states <- length(v_n) # number of health states + + +# stm_1_transmat + +#transition probability matrix for NO treatment +m_P <- matrix(data = NA, + nrow = n_states, + ncol = n_states, + dimnames = list(v_n, v_n)) + +m_P + +### From Healthy +m_P["H", ] <- c(1 - (p_HS1 + p_HD), p_HS1, 0, p_HD) + +### From Sick +m_P["S1", ] <- c(p_S1H, 1 - (p_S1H + p_S1S2 + p_S1D), p_S1S2, p_S1D) + +### From Sicker +m_P["S2", ] <- c(0, 0, 1 - p_S2D, p_S2D) + +### From Dead +m_P["D", ] <- c(0, 0, 0, 1) + +# check rows add up to 1 +rowSums(m_P) +m_P + +# ============================== # +# Check the probability matrix # +# ============================== # + +check_trans_prob_mat(m_P) + +# ============================== # + + +## @knitr stm_1_trace + +# create empty Markov trace +m_TR <- matrix(data = NA, + nrow = n_t, + ncol = n_states, + dimnames = list(1:n_t, v_n)) + +head(m_TR) # The head() function enables you to view the top of a table rather than the full matrix + +# initialize Markov trace +m_TR[1, ] <- c("H" = 1, "S1" = 0, "S2" = 0, "D" = 0) + +head(m_TR) # head shows us the first six rows by default. + +# =================================== # +# Check the initialisation conforms # +# =================================== # + +check_init(x = m_TR[1, ]) + +# =================================== # + +## @knitr stm_1_runfirst + +# Run the model for a single cycle +m_TR[2, ] <- m_TR[1, ] %*% m_P + +# Display the first two rows of the markov trace matrix +m_TR[c(1:2), ] + + + +# stm_1_runmod +for (cycle in 2:n_t){ # throughout the number of cycles + # estimate cycle of Markov trace + m_TR[cycle, ] <- m_TR[cycle - 1, ] %*% m_P +} + +head(m_TR) # head shows us the first six rows by default. + +# ============================== # +# we can check the markov trace # +# ============================== # + +check_markov_trace(m_TR = m_TR, dead_state = "D", confirm_ok = T) + +# ============================== # + +# check to make sure looks correct +plot(m_TR[, "H"], + col = "green", + type = "l", + ylim = c(0,1) ) +lines(m_TR[, "S1"], col = "purple") +lines(m_TR[, "S2"], col = "orange") +lines(m_TR[, "D"], col = "red") + + +# stm_1_costutilvecs + +# 1. Create vectors for the costs and utility of each treatment group +v_u_trt <- c("H" = u_H, "S1" = u_Trt, "S2" = u_S2, "D" = u_D) +v_u_no_trt <- c("H" = u_H, "S1" = u_S1, "S2" = u_S2, "D" = u_D) +v_c_trt <- c("H" = c_H, "S1" = c_S1 + c_Trt, "S2" = c_S2 + c_Trt, "D" = c_D) +v_c_no_trt <- c("H" = c_H, "S1" = c_S1, "S2" = c_S2, "D" = c_D) + +# stm_1_outputs + +# 2. Estimate mean costs and QALYs for each year (hint: need to use matrix multiplication) +v_E_no_trt <- m_TR %*% v_u_no_trt +v_E_trt <- m_TR %*% v_u_trt +v_C_no_trt <- m_TR %*% v_c_no_trt +v_C_trt <- m_TR %*% v_c_trt + +# stm_1_discount + +te_no_trt <- sum(v_E_no_trt * v_dwe) +te_trt <- sum(v_E_trt * v_dwe) +tc_no_trt <- sum(v_C_no_trt * v_dwc) +tc_trt <- sum(v_C_trt * v_dwc) + +# stm_1_results + +results <- c( + "Cost_NoTrt" = tc_no_trt, + "Cost_Trt" = tc_trt, + "QALY_NoTrt" = te_no_trt, + "QALY_Trt" = te_trt +) + +ICER <- (results["Cost_Trt"] - results["Cost_NoTrt"]) / (results["QALY_Trt"] - results["QALY_NoTrt"]) + +ICER diff --git a/man/extract_function_name.Rd b/man/extract_function_name.Rd new file mode 100644 index 0000000..4401e45 --- /dev/null +++ b/man/extract_function_name.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/cheers_checker.R +\name{extract_function_name} +\alias{extract_function_name} +\title{Extract function name from a string} +\usage{ +extract_function_name(string) +} +\arguments{ +\item{string}{A string containing a function definition, this must contain the word 'function'} +} +\value{ +A string containing the function name +} +\description{ +Extract function name from a long string. This works by +identifying "function(" in the string and then finding the operand before and splitting +on that before keeping the character there. +} diff --git a/man/find_next_vector_element.Rd b/man/find_next_vector_element.Rd new file mode 100644 index 0000000..3df7b3e --- /dev/null +++ b/man/find_next_vector_element.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/cheers_checker.R +\name{find_next_vector_element} +\alias{find_next_vector_element} +\title{Find the next element of the vector after a value} +\usage{ +find_next_vector_element(value, vector) +} +\arguments{ +\item{value}{A value of numeric values} + +\item{vector}{A vector of numeric values} +} +\value{ +The next element of the vector after the value +} +\description{ +Find the next element of the vector after a value +} +\examples{ +\dontrun{ +find_next_vector_element(value = 5, vector = c(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) +} + +} diff --git a/man/find_previous_vector_element.Rd b/man/find_previous_vector_element.Rd new file mode 100644 index 0000000..33271f3 --- /dev/null +++ b/man/find_previous_vector_element.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/cheers_checker.R +\name{find_previous_vector_element} +\alias{find_previous_vector_element} +\title{Find the previous element of the vector before a value} +\usage{ +find_previous_vector_element(value, vector) +} +\arguments{ +\item{value}{A value of numeric values} + +\item{vector}{A vector of numeric values} +} +\value{ +The previous element of the vector before the value +} +\description{ +Find the previous element of the vector before a value +} +\examples{ +\dontrun{ +find_previous_vector_element(value = 5, vector = c(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) +} + +} diff --git a/man/find_test.Rd b/man/find_test.Rd new file mode 100644 index 0000000..902de47 --- /dev/null +++ b/man/find_test.Rd @@ -0,0 +1,38 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/check_functions.R +\name{find_test} +\alias{find_test} +\title{Find test for a function in a codebase} +\usage{ +find_test(v_functions, path = ".", test_path = "tests/testthat") +} +\arguments{ +\item{v_functions}{A vector of functions to search for.} + +\item{path}{The path to the folder to be checked.} + +\item{test_path}{The relative path to the test folder from the project folder (path).} +} +\value{ +A vector of file paths to the test scripts for each function. +Returns NA where no script can be found. +} +\description{ +This function finds the test for each in a vector of functions in a specified +testing folder, default = tests/testthat as the relative path from the project folder (path). +} +\examples{ +\dontrun{ + v_funcs_to_find_tests_for <- c("check_init", "mean", "check_markov_trace", "find_test") + find_test(v_functions = v_funcs_to_find_tests_for, + path = ".") +} + +} +\seealso{ +Other checking_functions: +\code{\link{list_functions_in_script}()}, +\code{\link{tabulate_functions_in_folder}()}, +\code{\link{tabulate_functions_in_script}()} +} +\concept{checking_functions} diff --git a/man/get_active_functions.Rd b/man/get_active_functions.Rd new file mode 100644 index 0000000..7020035 --- /dev/null +++ b/man/get_active_functions.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/cheers_checker.R +\name{get_active_functions} +\alias{get_active_functions} +\title{get all active functions that exist in the global environment} +\usage{ +get_active_functions(packages = "assertHE") +} +\arguments{ +\item{packages}{a vector containing the names of packages to include in the search} +} +\value{ +a vector containing the names of all active functions in the global environment +} +\description{ +get all active functions that exist in the global environment +} diff --git a/man/get_file_cheers_classifications.Rd b/man/get_file_cheers_classifications.Rd new file mode 100644 index 0000000..7da58fe --- /dev/null +++ b/man/get_file_cheers_classifications.Rd @@ -0,0 +1,31 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/cheers_checker.R +\name{get_file_cheers_classifications} +\alias{get_file_cheers_classifications} +\title{Get cheers classification tags from a given file} +\usage{ +get_file_cheers_classifications( + filename, + cheers_pattern, + function_pattern = "(\\\\s|=|-)function\\\\(" +) +} +\arguments{ +\item{filename}{A string containing the filepath to the file to be checked} + +\item{cheers_pattern}{A string containing the roxygen tag for cheers which is used as an identifier} + +\item{function_pattern}{A string containing the pattern to identify functions} +} +\value{ +A list containing the cheers tags and the function names that follow them +} +\description{ +For a provided filepath, identify the cheers classification tags +and the function names that follow them. +} +\seealso{ +Other cheers: +\code{\link{get_folder_cheers_classifications}()} +} +\concept{cheers} diff --git a/man/get_folder_cheers_classifications.Rd b/man/get_folder_cheers_classifications.Rd new file mode 100644 index 0000000..17ad844 --- /dev/null +++ b/man/get_folder_cheers_classifications.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/cheers_checker.R +\name{get_folder_cheers_classifications} +\alias{get_folder_cheers_classifications} +\title{Get cheers classification tags from a given folder} +\usage{ +get_folder_cheers_classifications(path, cheers_pattern, path_ignore = "tests/") +} +\arguments{ +\item{path}{A string containing the filepath to the folder to be checked} + +\item{cheers_pattern}{A string containing the roxygen tag for cheers which is used as an identifier} + +\item{path_ignore}{A string containing the pattern to identify files to ignore} +} +\value{ +A list containing the cheers tags and the function names that follow them +} +\description{ +For a provided folder path, identify the cheers classification tags +and the function names that follow them. +} +\seealso{ +Other cheers: +\code{\link{get_file_cheers_classifications}()} +} +\concept{cheers} diff --git a/man/get_summary_table.Rd b/man/get_summary_table.Rd new file mode 100644 index 0000000..5936f99 --- /dev/null +++ b/man/get_summary_table.Rd @@ -0,0 +1,44 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/check_functions.R +\name{get_summary_table} +\alias{get_summary_table} +\title{Summarise project functions with details on packages and existence of unit-tests.} +\usage{ +get_summary_table( + path = ".", + path_exclude = "testthat/", + test_path = "tests/", + cheers_pattern = "@family", + packages_to_exclude = c("base", "stats", "utils") +) +} +\arguments{ +\item{path}{The path to the folder to be checked.} + +\item{path_exclude}{A set of strings that will exclude any files if it is present in the file path.} + +\item{test_path}{The relative path to the test folder from the project folder (path).} + +\item{cheers_pattern}{A string that will be used to identify the cheers tag.} + +\item{packages_to_exclude}{A vector of packages to exclude from the search.} +} +\value{ +A dataframe with the following columns: +\itemize{ +\item function_name: The name of the function. +\item package: The package the function is from. +\item file_name: The file in which the function is defined. +\item test_location: The file in which the function is tested. +} +} +\description{ +Creates a summary table containing the name of each function in the project, +the package it is from, whether it has a unit-test and the file in which it is tested. +} +\examples{ +\dontrun{ +get_summary_table() +} + +} diff --git a/man/list_functions_in_script.Rd b/man/list_functions_in_script.Rd new file mode 100644 index 0000000..10324ae --- /dev/null +++ b/man/list_functions_in_script.Rd @@ -0,0 +1,40 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/check_functions.R +\name{list_functions_in_script} +\alias{list_functions_in_script} +\title{List all of the functions in a script} +\usage{ +list_functions_in_script(filename, by_package = TRUE, alphabetic = TRUE) +} +\arguments{ +\item{filename}{The name of the file to be checked.} + +\item{by_package}{If TRUE, return a list of functions by package. Else return a vector of functions.} + +\item{alphabetic}{If TRUE, return the functions in alphabetic order.} +} +\value{ +A named list of functions in the script, by package. +} +\description{ +This function lists all of the functions in a script. It expects the script to be +an R file, and requires that package libraries are loaded when identifying packages. +} +\examples{ +\dontrun{ +list_functions_in_script(filename = "./R/check_trans_probs.R") + +list_functions_in_script(filename = "./R/check_functions.R", by_package = FALSE) +list_functions_in_script(filename = +paste0("https://raw.githubusercontent.com/dark-peak-analytics/", +"sicksickerPack/main/R/create_Markov_trace.R"), by_package = FALSE) + +} +} +\seealso{ +Other checking_functions: +\code{\link{find_test}()}, +\code{\link{tabulate_functions_in_folder}()}, +\code{\link{tabulate_functions_in_script}()} +} +\concept{checking_functions} diff --git a/man/tabulate_functions_in_folder.Rd b/man/tabulate_functions_in_folder.Rd new file mode 100644 index 0000000..3847af2 --- /dev/null +++ b/man/tabulate_functions_in_folder.Rd @@ -0,0 +1,49 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/check_functions.R +\name{tabulate_functions_in_folder} +\alias{tabulate_functions_in_folder} +\title{Create table of all of the functions identified in a project folder} +\usage{ +tabulate_functions_in_folder( + path = ".", + path_exclude = "testthat/", + collapse = T, + packages_to_exclude = c("base", "stats", "utils") +) +} +\arguments{ +\item{path}{The path to the folder to be checked.} + +\item{path_exclude}{A string which if found in file path removes file from analysis. +Defaults to 'testthat/' to exclude functions only found in tests.} + +\item{collapse}{If TRUE, return a single data-frame of all functions. Else return a list by file.} + +\item{packages_to_exclude}{A vector of packages to exclude from the output (e.g. "base")} +} +\value{ +Either a data-frame of functions and the package the function is from, or a list of functions by file. +} +\description{ +This function tabulates all of the functions identified in R scripts within a +project folder. It requires that package libraries are loaded when identifying +a function's package. +} +\examples{ + +\dontrun{ +tabulate_functions_in_folder( + path = ".", + path_exclude = "testthat/", + collapse = T, + packages_to_exclude = c("base", "stats", "utils") + ) +} +} +\seealso{ +Other checking_functions: +\code{\link{find_test}()}, +\code{\link{list_functions_in_script}()}, +\code{\link{tabulate_functions_in_script}()} +} +\concept{checking_functions} diff --git a/man/tabulate_functions_in_folder_with_tests.Rd b/man/tabulate_functions_in_folder_with_tests.Rd new file mode 100644 index 0000000..ecd7691 --- /dev/null +++ b/man/tabulate_functions_in_folder_with_tests.Rd @@ -0,0 +1,46 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/check_functions.R +\name{tabulate_functions_in_folder_with_tests} +\alias{tabulate_functions_in_folder_with_tests} +\title{Summarise project functions with details on packages and existence of unit-tests.} +\usage{ +tabulate_functions_in_folder_with_tests( + path = ".", + path_exclude = "testthat/", + packages_to_exclude = .packages(TRUE), + test_path = "tests/testthat" +) +} +\arguments{ +\item{path}{The path to the folder to be checked.} + +\item{path_exclude}{A set of strings that will exclude any files if it is present in the file path.} + +\item{packages_to_exclude}{A vector of packages to exclude from the search.} + +\item{test_path}{The relative path to the test folder from the project folder (path).} +} +\value{ +A dataframe with the following columns: +\itemize{ +\item function_name: The name of the function. +\item package: The package the function is from. +\item file_name: The file in which the function is defined. +\item test_location: The file in which the function is tested. +} +} +\description{ +Creates a summary table containing the name of each function in the project, +the package it is from, whether it has a unit-test and the file in which it is tested. +} +\examples{ +\dontrun{ +tabulate_functions_in_folder_with_tests( +path = ".", +path_exclude = "testthat/", +packages_to_exclude = c("base", "stats", "ggplot2"), +test_path = "tests/testthat" +) +} + +} diff --git a/man/tabulate_functions_in_script.Rd b/man/tabulate_functions_in_script.Rd new file mode 100644 index 0000000..ab87da1 --- /dev/null +++ b/man/tabulate_functions_in_script.Rd @@ -0,0 +1,41 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/check_functions.R +\name{tabulate_functions_in_script} +\alias{tabulate_functions_in_script} +\title{Create table of all of the functions in a script} +\usage{ +tabulate_functions_in_script( + filename, + packages_to_exclude = c("base", "stats", "utils") +) +} +\arguments{ +\item{filename}{The name of the file to be checked.} + +\item{packages_to_exclude}{A vector of packages to exclude from the output (e.g. "base")} +} +\value{ +A data-frame of functions and the package the function is from. +} +\description{ +This function tabulates all of the functions in a script with the name of the +package next to it. It expects the script to be an R file, and requires that +package libraries are loaded when identifying packages. +} +\examples{ +\dontrun{ +tabulate_functions_in_script(filename = "./R/check_functions.R") +tabulate_functions_in_script(filename = "./R/check_trans_probs.R", + packages_to_exclude = NULL) +tabulate_functions_in_script(filename = +paste0("https://raw.githubusercontent.com/dark-peak-analytics/", +"sicksickerPack/main/R/create_Markov_trace.R"), packages_to_exclude = NULL) +} +} +\seealso{ +Other checking_functions: +\code{\link{find_test}()}, +\code{\link{list_functions_in_script}()}, +\code{\link{tabulate_functions_in_folder}()} +} +\concept{checking_functions} diff --git a/tests/testthat/example_scripts/create_markov_trace.R b/tests/testthat/example_scripts/create_markov_trace.R new file mode 100644 index 0000000..6155ca6 --- /dev/null +++ b/tests/testthat/example_scripts/create_markov_trace.R @@ -0,0 +1,99 @@ +#' Create Markov Trace +#' +#' @description Create a Markov trace for a State-Transition Model (STM) Markov +#' providing a transition matrix, time horizon, states' names and the starting +#' state-occupancy. +#' +#' @param transition_matrix_ Numeric matrix containing the model's transition +#' matrix. +#' @param time_horizon_ Numeric scalar defining the number of cycles in the +#' model. +#' @param states_nms_ Character vector containing the names of the Markov model +#' states. +#' @param initial_trace_ Named numeric vector describing the states' occupancy +#' in the first cycle of the model. +#' +#' @return A matrix containing the Markov trace. +#' +#' @family simulation +#' +#' @export +#' +#' @examples +#' \dontrun{ +#' library("sicksickerPack") +#' transition_matrix <- matrix( +#' data = c(0.845, 0.15, 0, 0.005, +#' 0.5, 0.3800749, 0.105, 0.01492512, +#' 0, 0, 0.9511101, 0.04888987, +#' 0, 0, 0, 1), +#' nrow = 4, +#' byrow = TRUE, +#' dimnames = list( +#' c("H", "S1", "S2", "D"), +#' c("H", "S1", "S2", "D") +#' ) +#' ) +#' +#' Markov_trace <- create_Markov_trace( +#' transition_matrix_ = transition_matrix, +#' time_horizon_ = 5, +#' states_nms_ = c("H", "S1", "S2", "D"), +#' initial_trace_ = c("H" = 1, "S1" = 0, "S2" = 0, "D" = 0) +#' ) +#' } +create_Markov_trace <- function(transition_matrix_, + time_horizon_, + states_nms_, + initial_trace_) { + ## Sanity testing - inputs: + + # confirm inputs are of correct type + assertthat::assert_that( + is.vector(x = states_nms_, mode = "character"), + msg = paste( + "The states_nms_ argument is not of class character" + ) + ) + assertthat::assert_that( + is.vector(x = initial_trace_, mode = "numeric"), + msg = paste( + "The initial_trace_ argument is not of class numeric" + ) + ) + # confirm inputs are concordant + assertthat::assert_that( + all(length(states_nms_) == length(initial_trace_), + length(states_nms_) == nrow(transition_matrix_), + ncol(transition_matrix_) == nrow(transition_matrix_)), + msg = paste( + "The number of states in the trace or transition matrix do not match the", + "number of named states" + ) + ) + + ## Markov trace: + + # create empty Markov trace + Markov_trace <- matrix( + data = NA, + nrow = time_horizon_, + ncol = length(states_nms_), + dimnames = list( + 1:time_horizon_, + states_nms_) + ) + + # initialize Markov trace + Markov_trace[1, ] <- initial_trace_ + + # loop throughout the number of cycles + for (t in 2:time_horizon_) { + + # estimate cycle of Markov trace + Markov_trace[t, ] <- Markov_trace[t - 1, ] %*% transition_matrix_ + + } + + return(Markov_trace) +} diff --git a/tests/testthat/example_scripts/define_transition_matrix.R b/tests/testthat/example_scripts/define_transition_matrix.R new file mode 100644 index 0000000..ff48752 --- /dev/null +++ b/tests/testthat/example_scripts/define_transition_matrix.R @@ -0,0 +1,102 @@ +#' Define a transition matrix +#' +#' @description Define the transition matrix of a State-Transition Markov (STM) +#' Model. +#' +#' @param states_nms_ A character vector containing the names of the states of a +#' Markov model. +#' @param transition_probs_ A numeric vector containing the transition +#' probabilities of length `n x n`, where `n` is the length of, number of names +#' in, the `states_nms_` vector. +#' +#' @return An `n x n`, where `n` is the number of states in an STM model, +#' containing the transition probabilities between the model states. +#' +#' @export +#' +#' @family transitions +#' +#' @examples +#' \dontrun{ +#' library("sicksickerPack") +#' define_transition_matrix( +#' states_nms_ = c("A", "B"), +#' transition_probs_ = c( +#' 0.2, 0.8, # transitions from state A -> A and A -> B +#' 0, 1 # transitions from state B -> A and B -> B +#' ) +#' ) +#' } +define_transition_matrix <- function(states_nms_, + transition_probs_) { + ## Sanity testing - inputs: + + # confirm names vector is of class character, n = length(states_nms_) + assertthat::assert_that( + is.vector(x = states_nms_, mode = "character"), + msg = paste( + "The states_nms_ argument is not of class character" + ) + ) + # confirm transition probabilities vector is of class numeric + assertthat::assert_that( + is.vector(x = transition_probs_, mode = "numeric"), + msg = paste( + "The transition_probs_ argument is not of class numeric" + ) + ) + # ensure all transition probabilities are between 0 and 1 + assertthat::assert_that( + all(transition_probs_ >= 0, transition_probs_ <= 1), + msg = paste( + "One or more of the values passed to the transition_probs_ argument are", + "not between 0 and 1." + ) + ) + # confirm transition probabilities vector is, n = sqrt(transition_probs_) + assertthat::assert_that( + length(states_nms_) == sqrt(length(transition_probs_)), + msg = paste( + "Please pass", + length(transition_probs_), + "probabilties for the transition between the", + length(states_nms_), + "markov states." + ) + ) + + ## Construct the transition probabilities' matrix: + + # fill matrix with transition probabilities + tranistion_matrix <- matrix( + data = transition_probs_, + nrow = length(states_nms_), + ncol = length(states_nms_), + byrow = TRUE, + dimnames = list( + states_nms_, + states_nms_ + ) + ) + + ## Sanity testing - outputs: + + # check tranistion_matrix rows add up to 1 + assertthat::assert_that( + all(round(rowSums(tranistion_matrix), digits = 5) == 1), + msg = paste( + "Transition probabilities from the", + sub( + pattern = ",([^,]*)$", + replacement = " &\\1", + paste( + which(round(rowSums(tranistion_matrix), digits = 5) != 1) |> names(), + collapse = ", " + ) + ), + "state(s) do not add up to 1." + ) + ) + + return(tranistion_matrix) +} diff --git a/tests/testthat/example_scripts/example_script.R b/tests/testthat/example_scripts/example_script.R new file mode 100644 index 0000000..fa5b6b3 --- /dev/null +++ b/tests/testthat/example_scripts/example_script.R @@ -0,0 +1,185 @@ +rm(list = ls()) + +# basic information: +Strategies <- c("No Treatment", "Treatment") # strategy names +n_age_init <- 25 # age at baseline +n_age_max <- 55 # maximum age of follow up +n_t <- n_age_max - n_age_init # time horizon, number of cycles +d_r <- 0.035 # equal discount of costs and QALYs by 3% + +# Transition probabilities (per cycle) +p_HD <- 0.005 # probability to die when healthy +p_HS1 <- 0.15 # probability to become sick when healthy +p_S1H <- 0.5 # probability to become healthy when sick +p_S1S2 <- 0.105 # probability to become sicker when sick +hr_S1 <- 3 # hazard ratio of death in sick vs healthy +hr_S2 <- 10 # hazard ratio of death in sicker vs healthy + +# Cost and utility inputs +c_H <- 2000 # cost of remaining one cycle in the healthy state +c_S1 <- 4000 # cost of remaining one cycle in the sick state +c_S2 <- 15000 # cost of remaining one cycle in the sicker state +c_Trt <- 12000 # cost of treatment(per cycle) +c_D <- 0 # cost of being in the death state +u_H <- 1 # utility when healthy +u_S1 <- 0.75 # utility when sick +u_S2 <- 0.5 # utility when sicker +u_D <- 0 # utility when dead +u_Trt <- 0.95 # utility when being treated + +# stm_1_probs_calcs + +# rate of death in healthy +r_HD <- - log(1 - p_HD) + +# rate of death in sick +r_S1D <- hr_S1 * r_HD +# rate of death in sicker +r_S2D <- hr_S2 * r_HD + +# probability of death in sick +p_S1D <- 1 - exp(-r_S1D) +# probability of death in sicker +p_S2D <- 1 - exp(-r_S2D) + +# stm_1_discweight + +# calculate discount weight for each cycle +v_dwe <- v_dwc <- 1 / (1 + d_r) ^ (0:(n_t-1)) # discount weight (equal discounting is assumed for costs and effects) + + +# stm_1_names + +v_n <- c("H", "S1", "S2", "D") # the 4 states of the model: Healthy (H), Sick (S1), Sicker (S2), Dead (D) +n_states <- length(v_n) # number of health states + + +# stm_1_transmat + +#transition probability matrix for NO treatment +m_P <- matrix(data = NA, + nrow = n_states, + ncol = n_states, + dimnames = list(v_n, v_n)) + +m_P + +### From Healthy +m_P["H", ] <- c(1 - (p_HS1 + p_HD), p_HS1, 0, p_HD) + +### From Sick +m_P["S1", ] <- c(p_S1H, 1 - (p_S1H + p_S1S2 + p_S1D), p_S1S2, p_S1D) + +### From Sicker +m_P["S2", ] <- c(0, 0, 1 - p_S2D, p_S2D) + +### From Dead +m_P["D", ] <- c(0, 0, 0, 1) + +# check rows add up to 1 +rowSums(m_P) +m_P + +# ============================== # +# Check the probability matrix # +# ============================== # + +check_trans_prob_mat(m_P) + +# ============================== # + + +## @knitr stm_1_trace + +# create empty Markov trace +m_TR <- matrix(data = NA, + nrow = n_t, + ncol = n_states, + dimnames = list(1:n_t, v_n)) + +head(m_TR) # The head() function enables you to view the top of a table rather than the full matrix + +# initialize Markov trace +m_TR[1, ] <- c("H" = 1, "S1" = 0, "S2" = 0, "D" = 0) + +head(m_TR) # head shows us the first six rows by default. + +# =================================== # +# Check the initialisation conforms # +# =================================== # + +check_init(x = m_TR[1, ]) + +# =================================== # + +## @knitr stm_1_runfirst + +# Run the model for a single cycle +m_TR[2, ] <- m_TR[1, ] %*% m_P + +# Display the first two rows of the markov trace matrix +m_TR[c(1:2), ] + + + +# stm_1_runmod +for (cycle in 2:n_t){ # throughout the number of cycles + # estimate cycle of Markov trace + m_TR[cycle, ] <- m_TR[cycle - 1, ] %*% m_P +} + +head(m_TR) # head shows us the first six rows by default. + +# ============================== # +# we can check the markov trace # +# ============================== # + +check_markov_trace(m_TR = m_TR, dead_state = "D", confirm_ok = T) + +# ============================== # + +# check to make sure looks correct +plot(m_TR[, "H"], + col = "green", + type = "l", + ylim = c(0,1) ) +lines(m_TR[, "S1"], col = "purple") +lines(m_TR[, "S2"], col = "orange") +lines(m_TR[, "D"], col = "red") + + +# stm_1_costutilvecs + +# 1. Create vectors for the costs and utility of each treatment group +v_u_trt <- c("H" = u_H, "S1" = u_Trt, "S2" = u_S2, "D" = u_D) +v_u_no_trt <- c("H" = u_H, "S1" = u_S1, "S2" = u_S2, "D" = u_D) +v_c_trt <- c("H" = c_H, "S1" = c_S1 + c_Trt, "S2" = c_S2 + c_Trt, "D" = c_D) +v_c_no_trt <- c("H" = c_H, "S1" = c_S1, "S2" = c_S2, "D" = c_D) + +# stm_1_outputs + +# 2. Estimate mean costs and QALYs for each year (hint: need to use matrix multiplication) +v_E_no_trt <- m_TR %*% v_u_no_trt +v_E_trt <- m_TR %*% v_u_trt +v_C_no_trt <- m_TR %*% v_c_no_trt +v_C_trt <- m_TR %*% v_c_trt + +# stm_1_discount + +te_no_trt <- sum(v_E_no_trt * v_dwe) +te_trt <- sum(v_E_trt * v_dwe) +tc_no_trt <- sum(v_C_no_trt * v_dwc) +tc_trt <- sum(v_C_trt * v_dwc) + +# stm_1_results + +results <- c( + "Cost_NoTrt" = tc_no_trt, + "Cost_Trt" = tc_trt, + "QALY_NoTrt" = te_no_trt, + "QALY_Trt" = te_trt +) + +ICER <- (results["Cost_Trt"] - results["Cost_NoTrt"]) / (results["QALY_Trt"] - results["QALY_NoTrt"]) + +ICER diff --git a/tests/testthat/test-check_functions.R b/tests/testthat/test-check_functions.R new file mode 100644 index 0000000..77bb788 --- /dev/null +++ b/tests/testthat/test-check_functions.R @@ -0,0 +1,152 @@ +test_that("test no error when running folder function", { + + if (testthat::testing_package() != ""){ + path <- dirname(dirname(getwd())) + }else{ + path <- getwd() + } + + testthat::expect_silent( + assertHE::tabulate_functions_in_folder( + path = path, + collapse = T, + packages_to_exclude = c("base", "stats", "utils") + ) + ) + + tmp <- assertHE::tabulate_functions_in_folder( + path = path, + collapse = T, + packages_to_exclude = c("base", "stats", "utils") + ) + + testthat::expect_s3_class(tmp, + "data.frame") + + testthat::expect_type( + assertHE::tabulate_functions_in_folder( + path = path, + collapse = F, + packages_to_exclude = c("base", "stats", "utils") + ), + "list" + ) + + + testthat::expect_type( + assertHE::tabulate_functions_in_folder( + path = path, + collapse = F, + packages_to_exclude = NULL + ), + "list" + ) + +}) + + + + +test_that("find_test can identify a test where it exists", { + + if (testthat::testing_package() != ""){ + path <- dirname(dirname(getwd())) + }else{ + path <- getwd() + } + + #if(testthat::testing_package() == ""){ + path_to_test1 <- + assertHE:::find_test(v_functions = "check_markov_trace", + path = path, + test_path = "tests/testthat") + + testthat::expect_length(object = path_to_test1, n = 1) + + path_to_test2 <- + assertHE:::find_test( + v_functions = c("check_trans_prob_array", "mean"), + path = path, + test_path = "tests/testthat" + ) + + testthat::expect_length(object = path_to_test2 , n = 2) + #} +}) + + + + + + + + + + + +test_that( + "tabulate_functions_in_folder_with_tests can identify functions, packages and test locations", + { + if (testthat::testing_package() != "") { + path <- dirname(dirname(getwd())) + } else{ + path <- getwd() + } + + #if(testthat::testing_package() == ""){ + testthat::expect_silent( + assertHE:::tabulate_functions_in_folder_with_tests( + path = path, + path_exclude = "tests/testthat", + packages_to_exclude = c("base", "stats", "utils"), + test_path = "tests/testthat" + ) + ) + + testthat::expect_silent( + assertHE:::tabulate_functions_in_folder_with_tests( + path = path, + path_exclude = "tests/testthat", + packages_to_exclude = NULL, + test_path = "tests/testthat" + ) + ) + + + df_tests <- + assertHE:::tabulate_functions_in_folder_with_tests( + path = path, + path_exclude = "tests/testthat", + packages_to_exclude = c("base", "stats", "utils"), + test_path = "tests/testthat" + ) + + df_tests2 <- + assertHE:::tabulate_functions_in_folder_with_tests( + path = path, + path_exclude = "tests/testthat", + packages_to_exclude = NULL, + test_path = "tests/testthat" + ) + + testthat::expect_s3_class(df_tests, + "data.frame") + + testthat::expect_s3_class(df_tests2, + "data.frame") + + testthat::expect_equal(object = ncol(df_tests2), + expected = ncol(df_tests)) + testthat::expect_gt(object = nrow(df_tests2), + expected = nrow(df_tests)) + + testthat::expect_false(object = { + "base" %in% df_tests$package + }) + testthat::expect_true(object = { + "base" %in% df_tests2$package + }) + + #} + } +) diff --git a/tests/testthat/test-cheers_checker.R b/tests/testthat/test-cheers_checker.R new file mode 100644 index 0000000..52078fb --- /dev/null +++ b/tests/testthat/test-cheers_checker.R @@ -0,0 +1,158 @@ +test_that("Extracting function names works as intended", + { + + example1 <- " .function.name. <- + + function + (a, b, c){ + + + }" + + example2 <- "function_name <- function(a, b, c){}" + + example3 <-"__function.name__ = + + + + function(a, b, c){}" + + example4 <- "extract_function_name = function(string) { + # does the string 'function' exist in the string +" + + example5 <- "function_name <-function(a){}" + example6 <- "function_name =function(a){}" + example7 <- "function_name<-function(a){}" + example8 <- "function_name=function(a){}" + + exampleHORRID <- " .function.name. = #comment + function # more comment + (a) # yet more comment + {}" + + exampleComplicated <- "# Here are some comments for foo + #' @family test + + foo <- + + #a comment here - function indented by whitespace too ! + + function ( a ) + + { + + a <- some_function(x = a) + a = another_function(x = 'hello') + + }" + + simple <- " test_4 <- + #' adding text here + + function( x) {y <- x/2 + return(y) + + }" + + + function_commented <- "#' test_my_foo <- function(){} + myfoo <- function(){ + + }" + + + double_function <- "#' double function + # two functions here, damn. + + myfoo1 <- + + + function(){ + + hello_function(a) + } + + + myfoo2 <- functon(){}" + + expect_equal(extract_function_name(example1), ".function.name.") + expect_equal(extract_function_name(example2), "function_name") + expect_equal(extract_function_name(example3), "__function.name__") + expect_equal(extract_function_name(example4), "extract_function_name") + expect_equal(extract_function_name(example5), "function_name") + expect_equal(extract_function_name(example6), "function_name") + expect_equal(extract_function_name(example7), "function_name") + expect_equal(extract_function_name(example8), "function_name") + expect_equal(extract_function_name(exampleHORRID), ".function.name.") + expect_equal(extract_function_name(simple), "test_4") + expect_equal(extract_function_name(exampleComplicated), "foo") + expect_equal(extract_function_name(function_commented), "myfoo") # NOT test_my_foo + expect_equal(extract_function_name(double_function), "myfoo1") + + }) + + + + + +test_that("Next element after integer in vector works as intended", +{ + expect_equal(find_next_vector_element(10, 1:12), 11) + expect_equal(find_next_vector_element(4, seq(1, 120, 5)), 6) + expect_equal(find_next_vector_element(120, seq(1, 120, 5)), 116) +}) + + +test_that("Previous element before integer in vector works as intended", +{ + expect_equal(find_previous_vector_element(10, 1:12), 9) + expect_equal(find_previous_vector_element(4, seq(1, 120, 5)), 1) + expect_equal(find_previous_vector_element(1, seq(1, 120, 5)), 1) +}) + + + + + + + + + + test_that("get_file_cheers_classifications works for a few example scripts", + { + expect_silent({ + + expect_equal( + get_file_cheers_classifications(filename = testthat::test_path("example_scripts/create_markov_trace.R"), + cheers_pattern = "@family"), + stats::setNames(object = "create_Markov_trace", nm = "simulation") + ) + + expect_equal( + get_file_cheers_classifications(filename = testthat::test_path("example_scripts/define_transition_matrix.R"), + cheers_pattern = "@family"), + stats::setNames(object = "define_transition_matrix", nm = "transitions") + ) + + expect_equal( + get_file_cheers_classifications(filename = testthat::test_path("example_scripts/example_script.R"), + cheers_pattern = "@family"), + NA + ) + + }) + }) + + + +test_that("get_folder_cheers_classifications works for a few example folders", + { + expect_silent({ + get_folder_cheers_classifications(path = testthat::test_path("example_scripts"), + cheers_pattern = "@family") + }) + }) + + +