diff --git a/.Rbuildignore b/.Rbuildignore index 3cf63732..6f184bf9 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -23,3 +23,4 @@ vignettes/benchmarks* ^vignettes/precompile.R ^CRAN-SUBMISSION$ ^Makefile$ +^CODEOWNERS$ diff --git a/DESCRIPTION b/DESCRIPTION index a12e6ea6..86dafafc 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: hesim Type: Package Title: Health Economic Simulation Modeling and Decision Analysis -Version: 0.5.5 +Version: 0.5.6 Authors@R: c( person("Devin", "Incerti", , "devin.incerti@gmail.com", role = c("aut", "cre")), person("Jeroen P.", "Jansen", role = "aut"), diff --git a/R/RcppExports.R b/R/RcppExports.R index ac42a7a6..fee8dce4 100644 --- a/R/RcppExports.R +++ b/R/RcppExports.R @@ -69,6 +69,10 @@ C_indiv_ctstm_starting <- function(R_disease_prog, strategy_idx, patient_idx, R_ .Call('_hesim_C_indiv_ctstm_starting', PACKAGE = 'hesim', R_disease_prog, strategy_idx, patient_idx, R_StateVal, dr, type) } +C_indiv_ctstm_trans <- function(R_CtstmTrans, R_disease_prog, strategy_idx, patient_idx, R_StateVal, dr, type) { + .Call('_hesim_C_indiv_ctstm_trans', PACKAGE = 'hesim', R_CtstmTrans, R_disease_prog, strategy_idx, patient_idx, R_StateVal, dr, type) +} + C_indiv_ctstm_los <- function(R_disease_prog, strategy_idx, patient_idx, dr) { .Call('_hesim_C_indiv_ctstm_los', PACKAGE = 'hesim', R_disease_prog, strategy_idx, patient_idx, dr) } diff --git a/R/ctstm.R b/R/ctstm.R index 44dc8ba6..4b709d28 100644 --- a/R/ctstm.R +++ b/R/ctstm.R @@ -104,7 +104,8 @@ indiv_ctstm_sim_disease <- function(trans_model, max_t = 100, max_age = 100, setattr(disprog, "class", c("disprog", "data.table", "data.frame")) setattr(disprog, "size", - c(get_size(trans_model), n_states = nrow(trans_model$trans_mat))) + c(get_size(trans_model), n_states = nrow(trans_model$trans_mat), + n_transitions = max(trans_model$trans_mat, na.rm = TRUE))) setattr(disprog, "absorbing", absorbing(trans_model$trans_mat)) return(disprog[, ]) } @@ -577,8 +578,15 @@ IndivCtstm <- R6::R6Class("IndivCtstm", private$disprog_idx$patient_idx, stateval_list[[i]], dr[j], sim_type) + } else if (stateval_list[[i]]$method == "transition"){ + C_ev <- C_indiv_ctstm_trans(self$trans_model, + self$disprog_, + private$disprog_idx$strategy_idx, + private$disprog_idx$patient_idx, + stateval_list[[i]], dr[j], + sim_type) } else{ - stop("The 'StateVals' 'method' must either be 'wlos' or 'starting'.") + stop("The 'StateVals' 'method' must be 'wlos', 'starting' or 'transition'.") } self$disprog_[, ev := C_ev] if (lys){ diff --git a/R/sim-general.R b/R/sim-general.R index 942d0473..dd9fb2b1 100644 --- a/R/sim-general.R +++ b/R/sim-general.R @@ -23,8 +23,8 @@ #' #' The object also contains `size` and `absorbing` attributes. #' The `size` attribute is a numeric vector with the elements `n_samples`, -#' `n_strategies`, `n_patients`, and `n_states` denoting the number of samples, -#' treatment strategies, patients, and health states The `absorbing` attribute +#' `n_strategies`, `n_patients`, `n_states` and `n_transitions` denoting the number of samples, +#' treatment strategies, patients, health states and transitions. The `absorbing` attribute #' is a numeric vector containing the absorbing health states; i.e., the #' health states that cannot be transitioned from. Operationally, an #' absorbing state is a row in a transition matrix (as in the `trans_mat` field @@ -433,7 +433,8 @@ sim_ev.NULL <- function(object, ...) { #' the cohort. The method used to simulate expected values depends on the #' `$method` field in the [`StateVals`] object(s) stored in `model(s)`. If #' `$method = "starting"`, then state values represent a one-time value that -#' occurs at time 0. +#' occurs at time 0. If `$method = "transition"`, then the state values represent +#' a value for the transition at the transition time. #' #' The more common use case is `$method = "wlos"`, or a "weighted length of stay". #' That is, expected values for each health state can be thought of as state values diff --git a/R/statevals.R b/R/statevals.R index e9e043fc..f81a54c3 100644 --- a/R/statevals.R +++ b/R/statevals.R @@ -32,7 +32,9 @@ StateVals <- R6::R6Class( #' used in a cohort model, the state values only accrue at time 0; #' in contrast, in an individual-level model, state values #' accrue each time a patient enters a new state and are discounted based on - #' time of entrance into that state. + #' time of entrance into that state. When `transition`, then state values + #' represent a value that occurs at the transition time; only used for + #' individual-level models. method = NULL, #' @field time_reset If `FALSE` then time intervals are based on time since @@ -49,7 +51,7 @@ StateVals <- R6::R6Class( #' @param time_reset The `time_reset` field. #' @return A new `StateVals` object. initialize = function(params, input_data = NULL, - method = c("wlos", "starting"), + method = c("wlos", "starting", "transition"), time_reset = FALSE) { self$params <- params self$input_data <- input_data @@ -362,9 +364,12 @@ create_StateVals.lm <- function(object, input_data = NULL, n = 1000, } #' @rdname create_StateVals +#' @param method String matching a list of methods used for the `StateVals` object. #' @export -create_StateVals.stateval_tbl <- function(object, hesim_data = NULL, n = 1000, ...){ - +create_StateVals.stateval_tbl <- function(object, hesim_data = NULL, n = 1000, + method = c("wlos","starting","transition"), + ...){ + method <- match.arg(method) grp_var <- attr(object, "grp_var") # For backwards compatibility, use hesim_data attribute of object @@ -505,21 +510,24 @@ create_StateVals.stateval_tbl <- function(object, hesim_data = NULL, n = 1000, . } else{ time_intervals <- NULL } - + + transp <- (method == "transition") tparams <- new_tparams_mean(value = mu, n_samples = n, strategy_id = tbl$strategy_id, n_strategies = length(unique(tbl$strategy_id)), patient_id = tbl$patient_id, n_patients = length(unique(tbl$patient_id)), - state_id = tbl$state_id, - n_states = length(unique(tbl$state_id)), + state_id = if (transp) NULL else tbl$state_id, + transition_id = if (transp) tbl$state_id else NULL, + n_states = if (transp) NULL else length(unique(tbl$state_id)), + n_transitions = if (transp) length(unique(tbl$state_id)) else NULL, time_id = tbl$time_id, time_intervals = time_intervals, n_times = nrow(time_intervals), grp_id = tbl$grp_id, patient_wt = tbl$patient_wt) - return(StateVals$new(params = tparams, ...)) + return(StateVals$new(params = tparams, method=method, ...)) } #' @export diff --git a/R/utils.R b/R/utils.R index f6482d0b..cd742d32 100644 --- a/R/utils.R +++ b/R/utils.R @@ -36,7 +36,7 @@ check_dr <- function(dr){ } check_StateVals <- function(models, object, - object_name = c("stateprobs", "disprog")) { + object_name = c("stateprobs", "disprog", "trans_model")) { if(!is.list(models)){ stop("'models' must be a list", call. = FALSE) @@ -84,14 +84,22 @@ check_StateVals <- function(models, object, check_size(get_id_object(models[[i]])$n_patients, expected_size[["n_patients"]], z = "patients") - check_size( - get_id_object(models[[i]])$n_states, - expected_states, - msg = paste0("The number of states in each 'StateVals' model ", - "must equal the number of states in the ", - "'", object_name, "' object less the number of ", - "absorbing states, which is ", expected_states, ".") - ) + if (models[[i]]$method == "transition") { + if (object_name %in% c("disprog", "trans_model")) { + check_size(get_id_object(models[[i]])$n_transitions, + expected_size[["n_transitions"]], + z = "transitions") + } else stop(paste0("Transition-specific 'StateVals' not available for object_name == '", + object_name,"'.")) + } else # method != "transition" + check_size( + get_id_object(models[[i]])$n_states, + expected_states, + msg = paste0("The number of states in each 'StateVals' model ", + "must equal the number of states in the ", + "'", object_name, "' object less the number of ", + "absorbing states, which is ", expected_states, ".") + ) } # End loop over state value models } diff --git a/man/StateVals.Rd b/man/StateVals.Rd index 365e57af..6f8889ff 100644 --- a/man/StateVals.Rd +++ b/man/StateVals.Rd @@ -57,7 +57,9 @@ that occurs when a patient enters a health state. When \code{starting} is used in a cohort model, the state values only accrue at time 0; in contrast, in an individual-level model, state values accrue each time a patient enters a new state and are discounted based on -time of entrance into that state.} +time of entrance into that state. When \code{transition}, then state values +represent a value that occurs at the transition time; only used for +individual-level models.} \item{\code{time_reset}}{If \code{FALSE} then time intervals are based on time since the start of the simulation. If \code{TRUE}, then time intervals reset each @@ -84,7 +86,7 @@ Create a new \code{StateVals} object. \if{html}{\out{
}}\preformatted{StateVals$new( params, input_data = NULL, - method = c("wlos", "starting"), + method = c("wlos", "starting", "transition"), time_reset = FALSE )}\if{html}{\out{
}} } diff --git a/man/create_StateVals.Rd b/man/create_StateVals.Rd index 8dbc362d..dc921f63 100644 --- a/man/create_StateVals.Rd +++ b/man/create_StateVals.Rd @@ -16,7 +16,13 @@ create_StateVals(object, ...) ... ) -\method{create_StateVals}{stateval_tbl}(object, hesim_data = NULL, n = 1000, ...) +\method{create_StateVals}{stateval_tbl}( + object, + hesim_data = NULL, + n = 1000, + method = c("wlos", "starting", "transition"), + ... +) } \arguments{ \item{object}{A model object of the appropriate class.} @@ -34,6 +40,8 @@ documentation in \code{\link[=create_params]{create_params()}}.} \item{hesim_data}{A \code{\link{hesim_data}} object. Only required when \code{object} is of class \code{\link{stateval_tbl}}. See "details".} + +\item{method}{String matching a list of methods used for the \code{StateVals} object.} } \value{ A \code{\link{StateVals}} object. diff --git a/man/disprog.Rd b/man/disprog.Rd index 89f91bf9..e410e886 100644 --- a/man/disprog.Rd +++ b/man/disprog.Rd @@ -27,8 +27,8 @@ state during the simulation and 0 otherwise.} The object also contains \code{size} and \code{absorbing} attributes. The \code{size} attribute is a numeric vector with the elements \code{n_samples}, -\code{n_strategies}, \code{n_patients}, and \code{n_states} denoting the number of samples, -treatment strategies, patients, and health states The \code{absorbing} attribute +\code{n_strategies}, \code{n_patients}, \code{n_states} and \code{n_transitions} denoting the number of samples, +treatment strategies, patients, health states and transitions. The \code{absorbing} attribute is a numeric vector containing the absorbing health states; i.e., the health states that cannot be transitioned from. Operationally, an absorbing state is a row in a transition matrix (as in the \code{trans_mat} field diff --git a/man/sim_ev.Rd b/man/sim_ev.Rd index ef966661..4e546545 100644 --- a/man/sim_ev.Rd +++ b/man/sim_ev.Rd @@ -89,7 +89,8 @@ the \code{\link{CohortDtstm}} and \code{\link{Psm}} classes) are mean outcomes f the cohort. The method used to simulate expected values depends on the \verb{$method} field in the \code{\link{StateVals}} object(s) stored in \code{model(s)}. If \verb{$method = "starting"}, then state values represent a one-time value that -occurs at time 0. +occurs at time 0. If \verb{$method = "transition"}, then the state values represent +a value for the transition at the transition time. The more common use case is \verb{$method = "wlos"}, or a "weighted length of stay". That is, expected values for each health state can be thought of as state values diff --git a/src/RcppExports.cpp b/src/RcppExports.cpp index 37e3fb61..b462e3e0 100644 --- a/src/RcppExports.cpp +++ b/src/RcppExports.cpp @@ -261,6 +261,23 @@ BEGIN_RCPP return rcpp_result_gen; END_RCPP } +// C_indiv_ctstm_trans +std::vector C_indiv_ctstm_trans(Rcpp::Environment R_CtstmTrans, Rcpp::DataFrame R_disease_prog, std::vector strategy_idx, std::vector patient_idx, Rcpp::Environment R_StateVal, double dr, std::string type); +RcppExport SEXP _hesim_C_indiv_ctstm_trans(SEXP R_CtstmTransSEXP, SEXP R_disease_progSEXP, SEXP strategy_idxSEXP, SEXP patient_idxSEXP, SEXP R_StateValSEXP, SEXP drSEXP, SEXP typeSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< Rcpp::Environment >::type R_CtstmTrans(R_CtstmTransSEXP); + Rcpp::traits::input_parameter< Rcpp::DataFrame >::type R_disease_prog(R_disease_progSEXP); + Rcpp::traits::input_parameter< std::vector >::type strategy_idx(strategy_idxSEXP); + Rcpp::traits::input_parameter< std::vector >::type patient_idx(patient_idxSEXP); + Rcpp::traits::input_parameter< Rcpp::Environment >::type R_StateVal(R_StateValSEXP); + Rcpp::traits::input_parameter< double >::type dr(drSEXP); + Rcpp::traits::input_parameter< std::string >::type type(typeSEXP); + rcpp_result_gen = Rcpp::wrap(C_indiv_ctstm_trans(R_CtstmTrans, R_disease_prog, strategy_idx, patient_idx, R_StateVal, dr, type)); + return rcpp_result_gen; +END_RCPP +} // C_indiv_ctstm_los std::vector C_indiv_ctstm_los(Rcpp::DataFrame R_disease_prog, std::vector strategy_idx, std::vector patient_idx, double dr); RcppExport SEXP _hesim_C_indiv_ctstm_los(SEXP R_disease_progSEXP, SEXP strategy_idxSEXP, SEXP patient_idxSEXP, SEXP drSEXP) { @@ -685,6 +702,7 @@ static const R_CallMethodDef CallEntries[] = { {"_hesim_C_ctstm_indiv_stateprobs", (DL_FUNC) &_hesim_C_ctstm_indiv_stateprobs, 11}, {"_hesim_C_indiv_ctstm_wlos", (DL_FUNC) &_hesim_C_indiv_ctstm_wlos, 7}, {"_hesim_C_indiv_ctstm_starting", (DL_FUNC) &_hesim_C_indiv_ctstm_starting, 6}, + {"_hesim_C_indiv_ctstm_trans", (DL_FUNC) &_hesim_C_indiv_ctstm_trans, 7}, {"_hesim_C_indiv_ctstm_los", (DL_FUNC) &_hesim_C_indiv_ctstm_los, 4}, {"_hesim_C_psm_curves_summary", (DL_FUNC) &_hesim_C_psm_curves_summary, 4}, {"_hesim_C_psm_sim_stateprobs", (DL_FUNC) &_hesim_C_psm_sim_stateprobs, 1}, diff --git a/src/indiv-ctstm.cpp b/src/indiv-ctstm.cpp index aa3f7358..741b7535 100644 --- a/src/indiv-ctstm.cpp +++ b/src/indiv-ctstm.cpp @@ -339,6 +339,68 @@ std::vector C_indiv_ctstm_starting(Rcpp::DataFrame R_disease_prog, return out; } +/***************************************************************************//** + * @ingroup ctstm + * Transition-specific expected values. + * Simulate expected values in an individual patient simulation based on transition- + * specific values that occur when a patient follows a transition. Values + * accrue each time a patient has a transition and are discounted based on the + * transition. + * @param R_disease_prog An R object of simulating disease progression generated + * using C_ctstm_sim_disease. + * @param strategy_idx The strategy index starting at 0. + * @param patient_idx The patient index starting at 0. + * @param R_StateVal An R object of class @c StateVal. + * @param dr The discount rate. + * @param type @c predict for mean values or @c random for random samples. + * @return A vector of expected values in each row in R_disease_prog. These + * values are then summed by @c patient_id using @c data.table at the @c R level + * in the private member function @c IndivCtstm$sim_trans. + ******************************************************************************/ +// [[Rcpp::export]] +std::vector C_indiv_ctstm_trans(Rcpp::Environment R_CtstmTrans, + Rcpp::DataFrame R_disease_prog, + std::vector strategy_idx, + std::vector patient_idx, + Rcpp::Environment R_StateVal, + double dr, std::string type + ){ + std::unique_ptr transmod = hesim::ctstm::transmod::create(R_CtstmTrans); + hesim::ctstm::disease_prog disease_prog(R_disease_prog); + bool time_reset = Rcpp::as(R_StateVal["time_reset"]); + hesim::statmods::obs_index obs_index(hesim::statmods::get_id_object(R_StateVal)); + hesim::statevals stvals(R_StateVal); + + int N = disease_prog.sample_.size(); + std::vector out(N); + for (unsigned int i = 0; (int) i < N; ++i){ + std::vector ids = transmod->trans_mat_.trans_id(disease_prog.from_[i]); + std::vector tos = transmod->trans_mat_.to(disease_prog.from_[i]); + // find the transition index + int transition_idx = ids.size(); // default: no match + for (int j=0; j<(int)ids.size(); ++j) { + if (tos[j] == disease_prog.to_[i]) { + transition_idx = ids[j]; + break; + } + } + if (transition_idx != (int) ids.size()) { + double time = time_reset ? (disease_prog.to_[i]-disease_prog.from_[i]) : + disease_prog.to_[i]; + int time_idx = hesim::find_interval(time, obs_index.time_start_); + int obs = obs_index(strategy_idx[i], + patient_idx[i], + transition_idx, + time_idx); + double yhat = stvals.sim(disease_prog.sample_[i], obs, type); + out[i] = exp(-dr * time) * yhat; + } else { + out[i] = 0.0; + } + } + return out; +} + /***************************************************************************//** * @ingroup ctstm * Simulate length of stay given simulated disease progression diff --git a/tests/testthat/test-cpp-utils.R b/tests/testthat/test-cpp-utils.R index e2276c7e..2d152e05 100644 --- a/tests/testthat/test-cpp-utils.R +++ b/tests/testthat/test-cpp-utils.R @@ -40,7 +40,7 @@ test_that("max_lt", { }) # Test c++ function hesim_bound ----------------------------------------------------- -test_that("hesim_bound", { +test_that("find_interval", { vec = c(0,3,5) expect_equal(hesim:::C_test_find_interval(-1,vec),0) expect_equal(hesim:::C_test_find_interval(0,vec),0) diff --git a/tests/testthat/test-ctstm.R b/tests/testthat/test-ctstm.R index a86ee53b..06aa8402 100644 --- a/tests/testthat/test-ctstm.R +++ b/tests/testthat/test-ctstm.R @@ -552,6 +552,25 @@ test_that("Simulate costs and QALYs", { } test_starting(ictstm2) + + ## Using method = "transition" + drugcost_tbl2 <- stateval_tbl(tbl = data.frame(state_id = 1:3, + est = c(10, 20, 30)), + dist = "fixed") + drugcostsmod2 <- create_StateVals(drugcost_tbl2, n = n_samples, + time_reset = FALSE, method = "transition", + hesim_data = hesim_dat) + ictstm2 <- ictstm$clone() + ictstm2$cost_models <- list(drugs=drugcostsmod2) + ictstm2$sim_costs(dr = 0, by_patient = TRUE) + test_transition_costs <- function(model){ + model$disprog_[, costs := ifelse(from==1 & to==2, 10, + ifelse(from==1 & to==3, 20, 30))] + expect_equal(model$disprog_[, sum(costs), by=from]$V1, + model$costs_[, sum(costs), by=state_id]$V1) + } + test_transition_costs(ictstm2) + # Summarize costs and QALYs ## By patient = TRUE ce_summary <- ictstm$summarize()