From 87b0a58cbc8a0a4c74b66769afe7b7c72cdbe8e5 Mon Sep 17 00:00:00 2001 From: Eric Novik Date: Thu, 13 Jun 2024 17:32:27 -0400 Subject: [PATCH] Initial commit --- .Rbuildignore | 14 + .github/.gitignore | 1 + .github/workflows/check.yml | 96 +++++ .gitignore | 15 + DESCRIPTION | 39 ++ LICENSE | 28 ++ NAMESPACE | 35 ++ R/aaa.R | 80 +++++ R/data.R | 25 ++ R/fits-JointModelFit.R | 169 +++++++++ R/fits-StanModelFit.R | 114 ++++++ R/fits-TSModelFit.R | 273 ++++++++++++++ R/main-sim.R | 87 +++++ R/main-treatment_effect.R | 185 ++++++++++ R/misc-FormulaParser.R | 150 ++++++++ R/misc-FunctionDraws.R | 439 +++++++++++++++++++++++ R/misc-Transform.R | 113 ++++++ R/models-BaselineHazard.R | 73 ++++ R/models-JointModel.R | 211 +++++++++++ R/models-StanModel.R | 203 +++++++++++ R/models-TSModel.R | 372 +++++++++++++++++++ R/models-TTEModel.R | 134 +++++++ R/terms-EmaxTerm.R | 63 ++++ R/terms-FormulaTerm.R | 279 ++++++++++++++ R/terms-GPTerm.R | 224 ++++++++++++ R/terms-OffsetTerm.R | 154 ++++++++ R/terms-SFTerm.R | 252 +++++++++++++ R/terms-TermList.R | 347 ++++++++++++++++++ R/utils-data.R | 225 ++++++++++++ R/utils-misc.R | 23 ++ R/utils-pipe.R | 14 + R/utils-rvar.R | 34 ++ R/utils-stancode.R | 110 ++++++ R/utils-string.R | 46 +++ R/utils-trajectory_metrics.R | 127 +++++++ R/zzz.R | 28 ++ README.md | 27 ++ data/example_data_jm.rda | Bin 0 -> 3793 bytes data/testdata.rda | Bin 0 -> 5761 bytes inst/functions/emax.stan | 7 + inst/functions/gp/basisfun.stan | 22 ++ inst/functions/gp/group.stan | 14 + inst/functions/gp/shared.stan | 8 + inst/functions/hazard/inst_hazard.stan | 13 + inst/functions/hazard/integrate.stan | 69 ++++ inst/functions/hazard/misc.stan | 27 ++ inst/functions/hazard/weibull.stan | 5 + inst/functions/sf.stan | 11 + inst/functions/utils.stan | 88 +++++ inst/header.stan | 3 + man/BaselineHazard.Rd | 71 ++++ man/FunctionDraws.Rd | 384 ++++++++++++++++++++ man/JointModel.Rd | 206 +++++++++++ man/JointModelFit.Rd | 272 ++++++++++++++ man/StanCodeCreator.Rd | 168 +++++++++ man/StanModel.Rd | 145 ++++++++ man/StanModelFit.Rd | 191 ++++++++++ man/TSModel.Rd | 272 ++++++++++++++ man/TSModelFit.Rd | 306 ++++++++++++++++ man/TTEModel.Rd | 125 +++++++ man/TermList.Rd | 248 +++++++++++++ man/WeibullHazard.Rd | 77 ++++ man/add_sff_input.Rd | 19 + man/create_jm_grid.Rd | 31 ++ man/create_termlist.Rd | 19 + man/dot-FunctionDraws.Rd | 19 + man/example.Rd | 24 ++ man/example2.Rd | 29 ++ man/example_data_jm.Rd | 22 ++ man/extend_df.Rd | 21 ++ man/extend_df2.Rd | 27 ++ man/new_draws.Rd | 21 ++ man/pipe.Rd | 20 ++ man/plot_metric.Rd | 20 ++ man/plus-.FunctionDraws.Rd | 19 + man/predict_new_subjects.Rd | 38 ++ man/prior_sigma_informed.Rd | 18 + man/sample_subjects.Rd | 21 ++ man/sfgp-package.Rd | 39 ++ man/sfsim.Rd | 60 ++++ man/stancode_ts.Rd | 22 ++ man/stanmodel_functions.Rd | 17 + man/term_to_code.Rd | 14 + man/testdata.Rd | 30 ++ man/trajectory_metrics.Rd | 33 ++ man/treatment_effect.Rd | 38 ++ sfgp.Rproj | 20 ++ tests/testthat.R | 4 + tests/testthat/test-FormulaSF.R | 29 ++ tests/testthat/test-FormulaTerm.R | 8 + tests/testthat/test-OffsetTerm.R | 7 + tests/testthat/test-TermList.R | 53 +++ tests/testthat/test-Transform.R | 41 +++ tests/testthat/test-emax.R | 35 ++ tests/testthat/test-models-JointModel.R | 48 +++ tests/testthat/test-models-StanModel.R | 6 + tests/testthat/test-models-TSModel.R | 183 ++++++++++ tests/testthat/test-sim.R | 34 ++ tests/testthat/test-stan-integrate.R | 31 ++ tests/testthat/test-trajectory_metrics.R | 66 ++++ tests/testthat/test-utils-misc.R | 4 + tests/testthat/test-utils-rvar.R | 16 + tests/testthat/test-utils.R | 19 + vignettes/.gitignore | 2 + vignettes/math.Rmd | 276 ++++++++++++++ vignettes/models-formula-sf.Rmd | 217 +++++++++++ vignettes/models-manycomp-gp.Rmd | 57 +++ vignettes/models-sf-plus-gp.Rmd | 97 +++++ vignettes/models-simple-gp.Rmd | 53 +++ vignettes/models-survival-check.Rmd | 273 ++++++++++++++ vignettes/models-survival.Rmd | 136 +++++++ vignettes/references.bib | 13 + vignettes/treatment-effect.Rmd | 296 +++++++++++++++ 113 files changed, 10186 insertions(+) create mode 100644 .Rbuildignore create mode 100644 .github/.gitignore create mode 100644 .github/workflows/check.yml create mode 100644 .gitignore create mode 100644 DESCRIPTION create mode 100644 LICENSE create mode 100644 NAMESPACE create mode 100644 R/aaa.R create mode 100644 R/data.R create mode 100644 R/fits-JointModelFit.R create mode 100644 R/fits-StanModelFit.R create mode 100644 R/fits-TSModelFit.R create mode 100644 R/main-sim.R create mode 100644 R/main-treatment_effect.R create mode 100644 R/misc-FormulaParser.R create mode 100644 R/misc-FunctionDraws.R create mode 100644 R/misc-Transform.R create mode 100644 R/models-BaselineHazard.R create mode 100644 R/models-JointModel.R create mode 100644 R/models-StanModel.R create mode 100644 R/models-TSModel.R create mode 100644 R/models-TTEModel.R create mode 100644 R/terms-EmaxTerm.R create mode 100644 R/terms-FormulaTerm.R create mode 100644 R/terms-GPTerm.R create mode 100644 R/terms-OffsetTerm.R create mode 100644 R/terms-SFTerm.R create mode 100644 R/terms-TermList.R create mode 100644 R/utils-data.R create mode 100644 R/utils-misc.R create mode 100644 R/utils-pipe.R create mode 100644 R/utils-rvar.R create mode 100644 R/utils-stancode.R create mode 100644 R/utils-string.R create mode 100644 R/utils-trajectory_metrics.R create mode 100644 R/zzz.R create mode 100644 README.md create mode 100644 data/example_data_jm.rda create mode 100644 data/testdata.rda create mode 100644 inst/functions/emax.stan create mode 100644 inst/functions/gp/basisfun.stan create mode 100644 inst/functions/gp/group.stan create mode 100644 inst/functions/gp/shared.stan create mode 100644 inst/functions/hazard/inst_hazard.stan create mode 100644 inst/functions/hazard/integrate.stan create mode 100644 inst/functions/hazard/misc.stan create mode 100644 inst/functions/hazard/weibull.stan create mode 100644 inst/functions/sf.stan create mode 100644 inst/functions/utils.stan create mode 100644 inst/header.stan create mode 100644 man/BaselineHazard.Rd create mode 100644 man/FunctionDraws.Rd create mode 100644 man/JointModel.Rd create mode 100644 man/JointModelFit.Rd create mode 100644 man/StanCodeCreator.Rd create mode 100644 man/StanModel.Rd create mode 100644 man/StanModelFit.Rd create mode 100644 man/TSModel.Rd create mode 100644 man/TSModelFit.Rd create mode 100644 man/TTEModel.Rd create mode 100644 man/TermList.Rd create mode 100644 man/WeibullHazard.Rd create mode 100644 man/add_sff_input.Rd create mode 100644 man/create_jm_grid.Rd create mode 100644 man/create_termlist.Rd create mode 100644 man/dot-FunctionDraws.Rd create mode 100644 man/example.Rd create mode 100644 man/example2.Rd create mode 100644 man/example_data_jm.Rd create mode 100644 man/extend_df.Rd create mode 100644 man/extend_df2.Rd create mode 100644 man/new_draws.Rd create mode 100644 man/pipe.Rd create mode 100644 man/plot_metric.Rd create mode 100644 man/plus-.FunctionDraws.Rd create mode 100644 man/predict_new_subjects.Rd create mode 100644 man/prior_sigma_informed.Rd create mode 100644 man/sample_subjects.Rd create mode 100644 man/sfgp-package.Rd create mode 100644 man/sfsim.Rd create mode 100644 man/stancode_ts.Rd create mode 100644 man/stanmodel_functions.Rd create mode 100644 man/term_to_code.Rd create mode 100644 man/testdata.Rd create mode 100644 man/trajectory_metrics.Rd create mode 100644 man/treatment_effect.Rd create mode 100644 sfgp.Rproj create mode 100644 tests/testthat.R create mode 100644 tests/testthat/test-FormulaSF.R create mode 100644 tests/testthat/test-FormulaTerm.R create mode 100644 tests/testthat/test-OffsetTerm.R create mode 100644 tests/testthat/test-TermList.R create mode 100644 tests/testthat/test-Transform.R create mode 100644 tests/testthat/test-emax.R create mode 100644 tests/testthat/test-models-JointModel.R create mode 100644 tests/testthat/test-models-StanModel.R create mode 100644 tests/testthat/test-models-TSModel.R create mode 100644 tests/testthat/test-sim.R create mode 100644 tests/testthat/test-stan-integrate.R create mode 100644 tests/testthat/test-trajectory_metrics.R create mode 100644 tests/testthat/test-utils-misc.R create mode 100644 tests/testthat/test-utils-rvar.R create mode 100644 tests/testthat/test-utils.R create mode 100644 vignettes/.gitignore create mode 100644 vignettes/math.Rmd create mode 100644 vignettes/models-formula-sf.Rmd create mode 100644 vignettes/models-manycomp-gp.Rmd create mode 100644 vignettes/models-sf-plus-gp.Rmd create mode 100644 vignettes/models-simple-gp.Rmd create mode 100644 vignettes/models-survival-check.Rmd create mode 100644 vignettes/models-survival.Rmd create mode 100644 vignettes/references.bib create mode 100644 vignettes/treatment-effect.Rmd diff --git a/.Rbuildignore b/.Rbuildignore new file mode 100644 index 0000000..ca9ee88 --- /dev/null +++ b/.Rbuildignore @@ -0,0 +1,14 @@ +^.*\.Rproj$ +^\.Rproj\.user$ +^\.travis\.yml$ +^\_config\.yml$ +^\.github$ +^CONTRIBUTING\.md$ +^NEWS\.md$ +^_pkgdown\.yml$ +pkgdown/ +.DS_Store +.Rhistory +.gitignore +^codecov\.yml$ +vignettes/models-survival-check.Rmd diff --git a/.github/.gitignore b/.github/.gitignore new file mode 100644 index 0000000..2d19fc7 --- /dev/null +++ b/.github/.gitignore @@ -0,0 +1 @@ +*.html diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..b73d971 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,96 @@ +# This workflow file is from https://github.com/stan-dev/cmdstanr/blob/master/.github/workflows +# under the BSD 3-Clause License: https://github.com/stan-dev/cmdstanr/blob/master/LICENSE.md. +# +# BSD 3-Clause License +# Copyright (c) 2019, Stan Developers and their Assignees +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +on: + push: + branches: + - main + - develop + +name: check + +jobs: + check: + if: "! contains(github.event.head_commit.message, '[ci skip]')" + runs-on: ${{ matrix.config.os }} + + name: ${{ matrix.config.os }} (${{ matrix.config.r }}) + + strategy: + fail-fast: false + matrix: + config: + - {os: windows-latest, r: 'release', rtools: '42'} + - {os: ubuntu-latest, r: 'release', rtools: ''} + + + env: + R_REMOTES_NO_ERRORS_FROM_WARNINGS: true + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + NOT_CRAN: true + + steps: + - name: cmdstan env vars + run: | + echo "CMDSTAN_PATH=${HOME}/.cmdstan" >> $GITHUB_ENV + shell: bash + + - uses: actions/checkout@v3 + + - name: Install system dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libglpk-dev libglpk40 libcurl4-openssl-dev openmpi-bin openmpi-common libopenmpi-dev || true + + - uses: r-lib/actions/setup-r@v2.2.4 + with: + r-version: ${{ matrix.config.r }} + rtools-version: ${{ matrix.config.rtools }} + + - uses: r-lib/actions/setup-pandoc@v2.2.4 + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::rcmdcheck + needs: check + + - name: Install cmdstan + run: | + cmdstanr::check_cmdstan_toolchain(fix = TRUE) + cmdstanr::install_cmdstan(cores = 2) + shell: Rscript {0} + + - uses: r-lib/actions/check-r-package@v2 + with: + upload-snapshots: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3f2080e --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.Rproj.user +*.Rhistory +*.RData +*.Ruserdata +*.DS_Store +*.exe +*.so +*.o +*.dll +docs/ +*.html +*_cache/ +*_files/ +.Rproj.user +inst/doc diff --git a/DESCRIPTION b/DESCRIPTION new file mode 100644 index 0000000..5abe188 --- /dev/null +++ b/DESCRIPTION @@ -0,0 +1,39 @@ +Package: sfgp +Type: Package +Title: Additive semiparametric modeling +Version: 0.3.0 +Author: Juho Timonen. +Maintainer: Juho Timonen +Description: Additive semiparametric regression modeling with joint longitudinal + and time-to-event models. + The additive components of the longitudinal part can be for example + Stein-Fojo or approximate Gaussian process functions. +Encoding: UTF-8 +LazyData: true +RoxygenNote: 7.3.1 +Depends: + R (>= 3.5.0) +SystemRequirements: CmdStan >= 2.33.0 +Remotes: + stan-dev/cmdstanr, +Imports: + methods, + cmdstanr (>= 0.4.0), + checkmate (>= 2.0.0), + posterior (>= 1.1.0), + R6 (>= 2.4.0), + ggplot2 (>= 3.3.5), + ggdist (>= 3.3.1), + processx (>= 3.5.2), + stringr (>= 1.5.0), + ggpubr (>= 0.6.0), + dplyr (>= 1.1.4), + magrittr(>= 2.0.3) +Suggests: + covr, + knitr, + rmarkdown, + testthat (>= 3.0.0) +VignetteBuilder: knitr +Config/testthat/edition: 3 +License: file LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..149d946 --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2024, Generable + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/NAMESPACE b/NAMESPACE new file mode 100644 index 0000000..3a68999 --- /dev/null +++ b/NAMESPACE @@ -0,0 +1,35 @@ +# Generated by roxygen2: do not edit by hand + +S3method("+",FunctionDraws) +S3method("-",FunctionDraws) +export("%>%") +export(FunctionDraws) +export(JointModel) +export(JointModelFit) +export(StanModelFit) +export(TSModel) +export(TSModelFit) +export(TTEModel) +export(TermList) +export(WeibullHazard) +export(add_sff_input) +export(create_jm_grid) +export(example) +export(example2) +export(extend_df) +export(extend_df2) +export(new_draws) +export(plot_metric) +export(predict_new_subjects) +export(prior_sigma_informed) +export(sample_subjects) +export(sfsim) +export(stancode_ts) +export(term_to_code) +export(trajectory_metrics) +export(treatment_effect) +import(ggplot2) +import(stats) +importFrom(R6,R6Class) +importFrom(magrittr,"%>%") +importFrom(posterior,as_draws_rvars) diff --git a/R/aaa.R b/R/aaa.R new file mode 100644 index 0000000..28c5fc7 --- /dev/null +++ b/R/aaa.R @@ -0,0 +1,80 @@ +# MAIN DOCUMENTATION PAGE ------------------------------------------------- + +#' The 'sfgp' package. +#' +#' @description SF+GP modeling using 'Stan'. +#' @author Juho Timonen (first.last at iki.fi) +#' @keywords tumor GP Stan Bayesian +#' +#' @section Getting started: +#' See the following \code{R6} classes. +#' \itemize{ +#' \item \code{\link{TSModel}}: Main model class. +#' \item \code{\link{TSModelFit}}: Fit class. +#' \item \code{\link{JointModel}}: Main model class. +#' \item \code{\link{JointModelFit}}: Fit class. +#' \item \code{\link{TermList}}: Class describing model terms. +#' \item \code{\link{FunctionDraws}}: Class to hold fitted function +#' distributions. +#' } +#' +#' @section Data: +#' The data that you wish to analyze with 'sfgp' should be in an \R +#' \code{data.frame} where columns correspond to measured variables and rows +#' correspond to observations. Categorical variables should be \code{factor}s +#' and continuous ones should be \code{numeric}. +#' +#' @name sfgp-package +#' @aliases sfgp +#' @import ggplot2 stats +#' @importFrom R6 R6Class +#' @importFrom posterior as_draws_rvars +#' +#' +"_PACKAGE" + + +#' Run an example +#' +#' @description Fits a model to simple simulated data. +#' @export +#' @param num_bf Number of basis functions. +#' @param scale_bf Basis function domain scale. +#' @param formula The model formula. +#' @param ... Other arguments to the \code{$fit()} method of +#' \code{\link{TSModel}}. +#' @return An \code{\link{TSModelFit}} object. +example <- function(num_bf = 32, scale_bf = 1.5, formula = "y ~ gp(x)", + ...) { + form <- stats::as.formula(formula) + m <- TSModel$new(form) + xx <- seq(1, 10, by = 0.15) + ff <- 20 + 5 * sin(xx) + 2 * sin(5 * xx) + xx + yy <- ff + stats::rnorm(n = length(xx), mean = 0, sd = 1) + a <- data.frame(x = xx, y = yy) + tc1 <- list(num_bf = num_bf, scale_bf = scale_bf) + tc <- list(f_gp_x = tc1) + m$fit(data = a, term_confs = tc, ...) +} + +#' Run an example +#' +#' @description Fits a model to \code{testdata}. +#' @export +#' @param num_bf Number of basis functions. +#' @param scale_bf Basis function domain scale. +#' @param formula The model formula. +#' @param ... Other arguments to the \code{$fit()} method of +#' \code{\link{TSModel}}. +#' @return An \code{\link{TSModelFit}} object. +example2 <- function(num_bf = 24, scale_bf = 1.5, + formula = "y ~ gp(time) + gp(time,arm) + gp(time,id)", + ...) { + form <- stats::as.formula(formula) + dat <- testdata + m <- TSModel$new(form) + dat <- add_sff_input(dat, m) + tc1 <- list(num_bf = num_bf, scale_bf = scale_bf) + tc <- list(gp_x = tc1) + m$fit(data = dat, term_confs = tc, ...) +} diff --git a/R/data.R b/R/data.R new file mode 100644 index 0000000..88797ee --- /dev/null +++ b/R/data.R @@ -0,0 +1,25 @@ +#' Simulated longitudinal test data +#' +#' +#' @format +#' A data frame with 176 rows and 7 columns: +#' \describe{ +#' \item{id}{Subject id, factor} +#' \item{arm}{Trial arm, factor} +#' \item{sex}{Sex, factor} +#' \item{time}{Measurement time, numeric} +#' \item{weight}{Weight, numeric} +#' \item{y}{Response variable, numeric} +#' \item{f_true}{True signal} +#' } +#' @family built-in datasets +"testdata" + +#' Simulated joint longitudinal and time-to-event test data +#' +#' +#' @format +#' A list of two data frames \code{lon} (longitudinal data) and +#' \code{tte} (time-to-event data). Generated using \code{simjm}. +#' @family built-in datasets +"example_data_jm" diff --git a/R/fits-JointModelFit.R b/R/fits-JointModelFit.R new file mode 100644 index 0000000..f76272d --- /dev/null +++ b/R/fits-JointModelFit.R @@ -0,0 +1,169 @@ +#' The time-to-event model fit class +#' +#' @export +JointModelFit <- R6::R6Class("JointModelFit", + inherit = TSModelFit, + public = list( + + #' @description Print object description. + print = function() { + cat("An R6 object of class JointModelFit.\n") + }, + + #' @description + #' Get the entire joint model or a particular submodel. + #' + #' @param name Name of the submodel. If \code{NULL} (default), the entire + #' joint model object is returned. + get_model = function(name = NULL) { + if (is.null(name)) { + return(private$model) + } + private$model[[name]] + }, + + #' @description + #' Extract one TTE-related function as a \code{\link{FunctionDraws}} object. + #' + #' @param component Base name of the function in the 'Stan' code. + #' @param dataname Data where the function was evaluated. + function_draws_tte = function(component = "inst_haz", dataname = NULL) { + f_name <- component + model <- self$get_model("tte") + if (!is.null(dataname)) { + if (dataname == "LON") { + model <- self$get_model("lon") + } + f_name <- paste0(f_name, "_", dataname) + } else { + dataname <- "TTE" + } + id_name <- model$id_var + dat <- self$get_data(dataname) + x <- dat[, c(id_name, "t"), drop = FALSE] + f <- self$draws(name = f_name) + FunctionDraws$new(x, f, f_name) + }, + + #' @description + #' Extract baseline hazard as a \code{\link{FunctionDraws}} object. + #' @param dataname Data where the function was evaluated. + h0 = function(dataname = "GRD") { + self$function_draws_tte("h0", dataname) + }, + + #' @description + #' Extract instant hazard as a \code{\link{FunctionDraws}} object. + #' @param dataname Data where the function was evaluated. + instant_hazard = function(dataname = NULL) { + self$function_draws_tte("inst_haz", dataname) + }, + + #' @description + #' Extract cumulative hazard as a \code{\link{FunctionDraws}} object. + #' @param dataname Data where the function was evaluated. + cumulative_hazard = function(dataname = NULL) { + self$function_draws_tte("cum_haz", dataname) + }, + + #' @description + #' Extract survival function as a \code{\link{FunctionDraws}} object. + #' @param dataname Data where the function was evaluated. + survival_function = function(dataname = NULL) { + s <- self$cumulative_hazard(dataname)$scale(a = -1)$exp() + s$rename("survival_function") + s + }, + + #' @description + #' Visualize model fit (instant hazard). + #' + #' @param ... Arguments passed to the plot method of + #' \code{\link{FunctionDraws}}. + #' @param plot_events Should the event occurrences be plotted? + plot_hazard = function(plot_events = TRUE, ...) { + fd <- self$instant_hazard(dataname = "GRD") + plt <- fd$plot(...) + ggtitle("Instant hazard") + dat <- self$get_data("GRD") + dat_tte <- self$get_data("TTE") + id_var <- self$get_model()$lon$id_var + if (plot_events) { + map <- aes(x = t, y = 0, group = !!sym(id_var), pch = event) + plt <- plt + geom_point(dat_tte, mapping = map, inherit.aes = F) + } + plt + }, + + #' Compute predictions at test points + #' + #' @param data_lon Data frame. If \code{NULL}, the data used in fitting + #' is used. + #' @param data_tte Data frame. If \code{NULL}, the data used in fitting + #' is used. + #' @param data_grid Data frame. If \code{NULL}, the data used in fitting + #' is used. + #' @param fitted_params Parameters or model fit. + #' @param ... Other arguments to \code{generate_quantities()}. + #' @return A new \code{\link{JointModelFit}} object. + predict = function(data_lon = NULL, data_tte = NULL, data_grid = NULL, + fitted_params = NULL, ...) { + if (is.null(data_lon)) { + data_lon <- self$get_data("LON") + } + if (is.null(data_tte)) { + data_tte <- self$get_data("TTE") + } + if (is.null(data_grid)) { + data_grid <- self$get_data("GRD") + } + model <- self$get_model() + + # Prepare 'Stan' data + dat <- model$create_standata( + data_lon = data_lon, + data_tte = data_tte, + data_grid = data_grid, + term_confs = self$term_confs, + num_bf = NULL, + scale_bf = NULL, + skip_transform = NULL, + prior_only = FALSE, + set_transforms = FALSE + ) + + # Call 'Stan' + gq <- self$gq(stan_data = dat$stan_data, fitted_params = fitted_params, ...) + + # Original data + data_orig <- list( + LON = dat$data_lon_orig, + TTE = data_tte, + GRD = data_grid + ) + + # Return + JointModelFit$new( + model, gq, data_orig, dat$stan_data, dat$full_term_confs + ) + }, + + #' Compute predictions at given time points + #' + #' @param t_test The test time points + #' @param t_var Name of the time variable + #' @param t_var_copy Names of other continuous covariates to which + #' \code{t_test} should be copied. + #' @param y_test Test y + #' @return A new fit object + predict_time = function(t_test, t_var = "t", y_test = NULL, + t_var_copy = NULL) { + dat_lon <- self$get_data("LON") + y_name <- self$get_model("lon")$y_var + df_test <- create_df_predict_time( + dat_lon, t_test, t_var, t_var_copy, + y_test, y_name + ) + self$predict(data_lon = df_test) + } + ) +) diff --git a/R/fits-StanModelFit.R b/R/fits-StanModelFit.R new file mode 100644 index 0000000..52af3c6 --- /dev/null +++ b/R/fits-StanModelFit.R @@ -0,0 +1,114 @@ +#' The Fit class +#' +#' @export +StanModelFit <- R6::R6Class("StanModelFit", + private = list( + stan_fit = NULL, + stan_data = NULL, + datasets = NULL, + model = NULL + ), + public = list( + + + #' @description + #' Get original data used to fit the model. + #' + #' @param dataname Name of dataset. + get_data = function(dataname = "LON") { + dn <- self$datanames() + checkmate::assert_choice(dataname, dn) + private$datasets[[dataname]] + }, + + #' @description + #' Get names of stored data. + #' + datanames = function() { + names(private$datasets) + }, + + #' @description + #' Get model. + #' + #' @param name Name of model. Only has effect for + #' \code{\link{JointModelFit}} objects. + get_model = function(name) { + private$model + }, + + #' @description + #' Create model fit object + #' + #' @param model A \code{\link{StanModel}} object. + #' @param stan_fit The created 'Stan' fit. + #' @param datasets Original data sets. + #' @param stan_data The created 'Stan' data. Stored mainly for easier + #' debugging. + initialize = function(model, stan_fit, datasets, stan_data) { + checkmate::assert_list(datasets) + checkmate::assert_class(model, "StanModel") + private$model <- model + private$datasets <- datasets + private$stan_fit <- stan_fit + private$stan_data <- stan_data + }, + + #' @description Get the underlying 'Stan' fit object. + get_stan_fit = function() { + private$stan_fit + }, + + #' @description Get the underlying 'Stan' data object (for debugging). + get_stan_data = function() { + private$stan_data + }, + + #' @description Extract draws as \code{rvar}s + #' + #' @param name Param/quantity name + draws = function(name = NULL) { + d <- self$get_stan_fit()$draws(name) + d <- posterior::as_draws_rvars(d) + if (is.null(name)) { + return(d) + } + d[[name]] + }, + + #' @description + #' Full names of parameters that start with 'log_z_'. + log_z_pars = function() { + nams <- names(self$draws()) + match <- grepl(nams, pattern = "log_z_") + nams[which(match)] + }, + + #' @description + #' Extract log likelihood as 'rvars'. + loglik = function() { + self$draws("log_lik") + }, + + #' @description Generate quantities using the fit. + #' + #' @param stan_data Full 'Stan' input list. + #' @param fitted_params Argument to \code{generate_quantities()}. + #' @param ... Other arguments to \code{generate_quantities()}. + gq = function(stan_data = NULL, fitted_params = NULL, ...) { + if (is.null(fitted_params)) { + fitted_params <- self$get_stan_fit() + } + if (inherits(fitted_params, "draws_rvars")) { + fitted_params <- posterior::as_draws_array(fitted_params) + } + + # Call 'Stan' + self$get_model()$get_stanmodel()$generate_quantities( + fitted_params = fitted_params, + data = stan_data, + ... + ) + } + ) +) diff --git a/R/fits-TSModelFit.R b/R/fits-TSModelFit.R new file mode 100644 index 0000000..2149207 --- /dev/null +++ b/R/fits-TSModelFit.R @@ -0,0 +1,273 @@ +#' The 'TSModelFit' class +#' +#' @export +#' @field term_confs The term configurations used. +TSModelFit <- R6::R6Class("TSModelFit", + inherit = StanModelFit, + private = list( + + # Extract one component as 'rvar' + extract_f_comp = function(f_name = "f") { + self$draws(name = f_name) + } + ), + public = list( + term_confs = NULL, + + #' @description + #' Create model fit object + #' + #' @param model A \code{\link{StanModel}} object. + #' @param stan_fit The created 'Stan' fit. + #' @param datasets Original data (list of data frames). + #' @param stan_data The created 'Stan' data. Stored mainly for easier + #' debugging. + #' @param term_confs The term configurations used. + initialize = function(model, stan_fit, datasets, stan_data, term_confs) { + self$term_confs <- term_confs + super$initialize(model, stan_fit, datasets, stan_data) + }, + + #' @description Print object description. + print = function() { + cat("An R6 object of class TSModelFit.\n") + }, + + #' @description + #' Extract one component as a \code{FunctionDraws} object. + #' + #' @param component Name of the component (term). Valid names can + #' be checked using \code{$model$term_names()}. Alternatively, can be + #' an integer that is the component index. + #' @param capped Should a capped version of the draws be taken? This + #' is useful if the fit/prediction explodes to infinity. Only has effect + #' if \code{component="f_sum"} or \code{component="y_log_pred"}. + #' @param data_scale Transform to data scale? Has no effect if + #' extracting a single component. + #' @param dataname name of data set used to evaluate the function + function_draws = function(component = "f_sum", data_scale = TRUE, + capped = FALSE) { + mod <- self$get_model("lon") + dat <- self$get_data("LON") + if (is.numeric(component)) { + checkmate::assert_integerish(component) + component <- mod$term_names()[component] + } + f_name <- component + + if (f_name == "f_sum" || f_name == "y_log_pred") { + if (f_name == "y_log_pred") { + f_name <- paste0(mod$y_var, "_log_pred") + } + # The total sum or predictive + if (capped) { + message( + "Draws capped at max ", mod$log_y_cap, + " on log scale (if they exceed it)" + ) + f_name <- paste0(f_name, "_capped") + } + f <- private$extract_f_comp(f_name) + covs <- mod$term_list$input_vars() + x <- dat[, covs, drop = FALSE] + fd <- FunctionDraws$new(x, f, f_name) + } else { + if (data_scale) { + data_scale <- FALSE + } + # A single component + t <- mod$term_list$get_term(f_name) + covs <- t$input_vars() + x <- dat[, covs, drop = FALSE] + f <- private$extract_f_comp(f_name) + fd <- FunctionDraws$new(x, f, f_name) + } + if (data_scale) { + message("Transforming to data scale") + delta <- mod$get_delta() + fd <- fd$exp()$scale(a = 1, b = -delta) + } + fd + }, + + #' @description + #' Initialize a \code{\link{FunctionDraws}} object from the fit. + #' + #' @param input_vars Column names in the data. + #' @param f_name Name of function in 'Stan' code. + create_functiondraws = function(input_vars, f_name) { + dat <- self$get_data("LON") + x <- dat[, input_vars] + f <- private$extract_f_comp(f_name) + FunctionDraws$new(x, f, f_name) + }, + + #' @description + #' Compute estimate of the measurement error percentage of each observation + #' as 'rvars'. + measurement_error = function() { + f <- self$function_draws(data_scale = TRUE, capped = FALSE) + f_rvars <- f$get_output() + orig_data <- self$get_data("LON") + mod <- self$get_model("lon") + f_diff_rvars <- f_rvars - orig_data[[mod$y_var]] + abs(f_diff_rvars) / abs(f_rvars) + }, + + #' @description + #' Get a data frame that is useful for assessing fit quality. + fit_quality_df = function() { + ll <- self$loglik() + me <- self$measurement_error() + a <- self$get_data("LON") + a$loglik <- stats::median(ll) + a$error_perc <- stats::median(me) + a + }, + + #' @description + #' Get summary statistics useful for assessing fit quality. + #' + #' @param metric Metric used to assess quality. Can be \code{"error_perc"} + #' or \code{"loglik"}. + #' @param by The factor by which to aggregate. If \code{NULL}, the id + #' variable of the model is used. + #' @param fun Function used to aggregate. + fit_quality_summary = function(metric = "error_perc", + by = NULL, fun = stats::median) { + if (is.null(by)) { + by <- self$get_model("lon")$id_var + } + df <- self$fit_quality_df() + form <- paste0(metric, " ~ ", by) + stats::aggregate(as.formula(form), df, FUN = fun) + }, + + #' @description + #' Plot fit or predictive distribution + #' + #' @param f_reference Reference function to plot against the fit. + #' @param plot_y Should y data be plotted? + #' @param capped Should a capped version of the fit be plotted? This + #' is useful if the fit explodes to infinity. + #' @param f_ref_name Name of the reference function. + #' @param predictive Should this plot the predictive distribution for + #' new observations? Otherwise the inferred signal is plotted. + #' @param filter_by Factor to filter the rows by. + #' @param kept_vals Values of the factor that are not filtered out. + #' @param ... Arguments passed to the plot method of + #' \code{\link{FunctionDraws}}. + plot = function(f_reference = NULL, plot_y = TRUE, + capped = TRUE, predictive = TRUE, + f_ref_name = "the true signal", + filter_by = NULL, + kept_vals = NULL, + ...) { + dat <- self$get_data("LON") + yn <- self$get_model("lon")$y_var + if (predictive) { + f_name <- "y_log_pred" + } else { + f_name <- "f_sum" + } + f <- self$function_draws(capped = capped, component = f_name) + plt <- f$plot(filter_by = filter_by, kept_vals = kept_vals, ...) + x_var <- rlang::as_name(plt$mapping$x) + dat <- df_filter_rows(dat, filter_by, kept_vals) + + # Plot true signal as comparison + if (!is.null(f_reference)) { + checkmate::assert_numeric(f_reference, len = nrow(dat)) + dat$f_reference <- f_reference + message("black dashed line is ", f_ref_name) + plt <- plt + geom_line(aes(x = !!sym(x_var), y = f_reference), + color = "black", lty = 2, + data = dat, + inherit.aes = FALSE + ) + } + + # Plot data as dots + if (plot_y) { + plt <- plt + geom_point( + aes(x = !!sym(x_var), y = !!sym(yn)), + color = "black", + data = dat, + inherit.aes = FALSE + ) + } + plt + }, + + #' Compute predictions at test points + #' + #' @param data The input data frame of test points. + #' @param fitted_params Parameters or model fit. + #' @param ... Other arguments to \code{generate_quantities()}. + #' @return A new fit object. + predict = function(data = NULL, fitted_params = NULL, ...) { + # Create input + if (is.null(data)) { + data <- self$get_data("LON") + } + model <- self$get_model() + + # Create Stan input list + dat <- model$create_standata( + data = data, + term_confs = self$term_confs, + num_bf = NULL, + scale_bf = NULL, + skip_transform = NULL, + prior_only = FALSE, + set_transforms = FALSE + ) + + # Call 'Stan' + gq <- self$gq( + stan_data = dat$stan_data, fitted_params = fitted_params, + ... + ) + + # Return + data_orig <- list(LON = dat$orig_data) + TSModelFit$new( + model, gq, data_orig, dat$stan_data, dat$full_term_confs + ) + }, + + #' Compute predictions at given time points + #' + #' @param t_test The test time points + #' @param t_var Name of the time variable + #' @param t_var_copy Names of other continuous covariates to which + #' \code{t_test} should be copied. + #' @param y_test Test y + #' @param ... Arguments passed to \code{$predict()}. + #' @return A new fit object + predict_time = function(t_test, t_var = "t", y_test = NULL, + t_var_copy = NULL, ...) { + dat_lon <- self$get_data("LON") + y_name <- self$get_model()$y_var + df_test <- create_df_predict_time( + dat_lon, t_test, t_var, t_var_copy, + y_test, y_name + ) + self$predict(df_test, ...) + } + ) +) + +# Helper +create_df_predict_time <- function(dat_lon, t_test, t_var, t_var_copy, + y_test, y_name) { + df_test <- extend_df(dat_lon, t_test, t_var) + for (v in t_var_copy) { + df_test[[v]] <- df_test[[t_var]] + } + if (is.null(y_test)) { + y_test <- 1 + } + df_test[[y_name]] <- y_test + df_test +} diff --git a/R/main-sim.R b/R/main-sim.R new file mode 100644 index 0000000..d98a432 --- /dev/null +++ b/R/main-sim.R @@ -0,0 +1,87 @@ +#' Simulate from SF model (3 arms) +#' +#' @export +#' @param n1 number of subjects in arm 1 +#' @param n2 number of subjects in arm 2 +#' @param n3 number of subjects in arm 3 +#' @param ts0_scale initial tumor sizes will be sampled from the uniform +#' distribution \code{ts0_scale * U(1,3)} +#' @param mu_log_ks Mean log \code{k_s} parameters for the three arms. +#' @param mu_log_kg Mean log \code{k_g} parameters for the three arms. +#' @param times Time points where measurements are made. Scatter is added to +#' these. +#' @param kg_sd Jitter added to kg. +#' @param ks_sd Jitter added to ks. +#' @param sigma Noise param value. +#' @param x_effect Magnitude of effect of x on ks. +#' @param log_C_kg offset for log kg +#' @param log_C_ks offset for log ks +#' @param t_sd Jitter added to time points. +#' @return a data frame +sfsim <- function(n1 = 4, n2 = 4, n3 = 4, ts0_scale = 20, + mu_log_ks = c(3, 1.5, -1), + mu_log_kg = c(-2, 0, 2), + times = c(0.5, 3, 6, 12, 24, 48) / 48, + kg_sd = 0.05, + ks_sd = 0.05, + sigma = 0.2, + x_effect = 0, + log_C_kg = -2, + log_C_ks = -1, + t_sd = 0.02) { + df <- sim_id_and_arm(n1, n2, n3) + df_final <- NULL + for (j in 1:nrow(df)) { + t_j <- times + t_j <- sort(t_j + t_sd * rnorm(length(t_j))) + n_j <- length(t_j) + ID <- df$id[j] + ARM <- df$arm[j] + x_j <- stats::runif(1) + 0 * t_j + t_start <- 0.1 + 0.2 * runif(1) + t_end <- 0.6 + 1.6 * runif(1) + x_j <- x_j * (1 / (1 + exp(-30 * (t_j - t_start))) - 1 / (1 + exp(-30 * (t_j - t_end)))) + id_j <- rep(ID, n_j) + arm_j <- rep(ARM, n_j) + ts0 <- ts0_scale * runif(1, 1, 3) + # f_x_ks <- x_effect * (sin(6 * X) + 0.5) + f_x_ks <- sim_emax(x_j, x_effect, 0.35, 9) + f_x_kg <- rep(0, n_j) + kg_vec <- exp(log_C_kg + rnorm(1, mean = mu_log_kg[ARM], sd = kg_sd) + f_x_kg) + ks_vec <- exp(log_C_ks + rnorm(1, mean = mu_log_ks[ARM], sd = ks_sd) + f_x_ks) + f_j <- sfsim_one(ts0, kg_vec, ks_vec, t_j) + y_j <- rlnorm(n = length(f_j), meanlog = log(f_j), sdlog = sigma) + df_j <- data.frame( + id = id_j, arm = arm_j, t = t_j, x = x_j, f_x_ks = f_x_ks, f = f_j, + y = y_j, true_ks = ks_vec, true_kg = kg_vec + ) + df_final <- rbind(df_final, df_j) + } + df_final +} + +# Emax +sim_emax <- function(x, emax, ed50, gamma) { + xg <- x^gamma + (emax * xg) / (ed50^gamma + xg) +} + + +# Simulate one trajectory from SF +sfsim_one <- function(ts0, kg, ks, t) { + ts0 * (exp(kg * t) + exp(-ks * t) - 1.0) +} + + +# Simulate id and arm factors +sim_id_and_arm <- function(n1, n2, n3) { + arm1 <- rep(1, n1) + arm2 <- rep(2, n2) + arm3 <- rep(3, n3) + arm <- c(arm1, arm2, arm3) + id <- c(1:n1, n1 + (1:n2), n1 + n2 + (1:n3)) + df <- data.frame(id, arm) + df$id <- as.factor(df$id) + df$arm <- as.factor(df$arm) + df +} diff --git a/R/main-treatment_effect.R b/R/main-treatment_effect.R new file mode 100644 index 0000000..2bcaf9f --- /dev/null +++ b/R/main-treatment_effect.R @@ -0,0 +1,185 @@ +#' Draw one subjects from each group +#' +#' @export +#' @param data original data frame +#' @param id_var name of the subject identifier variable +#' @param group_var grouping variable +#' @return new data frame +sample_subjects <- function(data, id_var = "id", group_var = "arm") { + N <- 1 # had to be fixed to 1 due to id issues + id_fac <- data[[id_var]] + checkmate::assert_factor(id_fac) + sample_rows_by_group(data, N = N, group_var = group_var) +} + +#' Analyse treatment effect +#' +#' @export +#' @param fit_post Posterior fit. +#' @param fit_prior Prior fit. Needs to have same number of draws as +#' \code{fit_post}. +#' @param t_pred Vector of time points (in days). Chosen based on data range +#' if \code{t_pred=NULL}. Chosen to be an evenly spaced vector from +#' 0 to at most 2 years every 8 weeks if \code{t_pred="auto"}. +#' @param time_var name of the time variable +#' @param group_var grouping variable +#' @param method Used method for analyzing the treatment effect. Can be either +#' \itemize{ +#' \item \code{new_sub}: Simulating new imaginary subjects +#' \item \code{group_est}: Using group-level parameter estimates +#' } +treatment_effect <- function(fit_post, fit_prior, time_var = "t", + t_pred = NULL, group_var = "arm", + method = "new_sub") { + # Validate input + checkmate::assert_choice(method, choices = c("new_sub", "group_est")) + + # "New" subjects + if (method == "new_sub") { + zero_log_z <- FALSE + } else if (method == "group_est") { + zero_log_z <- TRUE + } else { + stop("invalid method") + } + p_new <- predict_new_subjects( + fit_post, fit_prior, time_var, t_pred, group_var, zero_log_z + ) + traj <- p_new$function_draws(data_scale = F)$zero_offset_by_id(group_var) + + # Return + traj_log <- traj + traj <- traj$exp() + list( + traj_log = traj_log, + traj = traj, + metrics = trajectory_metrics(traj, group_var, time_var), + p_new = p_new + ) +} + + +#' Compute trajectory metrics. +#' +#' @export +#' @param trajectories An object of class \code{\link{FunctionDraws}}. +#' @param id_var Name of id variable. +#' @param time_var Name of the time variable. +#' @return The following metrics are computed for each draw for each id. +#' \itemize{ +#' \item \code{depth} = depth of response (RECIST) +#' \item \code{depth_br} = depth of response (best response definition, can +#' be negative) +#' \item \code{duration} = duration of response (RECIST) +#' \item \code{start} = start time of response (RECIST) +#' \item \code{end} = end time of response (RECIST) +#' \item \code{exists} = whether response exists (RECIST) +#' \item \code{endures} = whether response endures whole time interval (RECIST) +#' \item \code{ttb120} = Time to 1.2 x baseline (different duration metric). +#' } +#' See the \code{treatment-effect} vignette for definition of the metrics. +trajectory_metrics <- function(trajectories, id_var, time_var) { + checkmate::assert_class(trajectories, "FunctionDraws") + checkmate::assert_character(id_var, len = 1) + checkmate::assert_character(time_var, len = 1) + df <- trajectories$as_data_frame_long() + df %>% + dplyr::group_by(!!sym(id_var), .draw_idx) %>% + dplyr::summarize( + depth = depth_of_response(value), + depth_br = best_response(value), + duration = response_dur(!!sym(time_var), value), + start = response_start(!!sym(time_var), value), + end = response_end(!!sym(time_var), value), + exists = response_exists(!!sym(time_var), value), + endures = response_endures(!!sym(time_var), value), + ttb120 = ttb120(!!sym(time_var), value), + .groups = "drop" + ) +} + + +#' Generate predictions for new imaginary subjects from each group +#' +#' @export +#' @param fit_post posterior fit +#' @param fit_prior prior fit +#' @param t_pred Vector of time points. Chosen automatically if \code{NULL}. +#' @param time_var time variable +#' @param group_var grouping variable +#' @param zero_log_z Use zero for all \code{log_z_*} parameters. This +#' effectively takes group means of all \code{HierOffsetTerm}s. +#' @param prior_param_names Names of parameters for which prior draws should +#' be used. If \code{NULL} (default), these are attempted be to detected +#' automatically. These are usually all individual-specific parameters whose +#' values we do not know for "new" subjects so we draw them from the prior. +predict_new_subjects <- function(fit_post, fit_prior, + time_var, t_pred = NULL, + group_var = "arm", + zero_log_z = FALSE, + prior_param_names = NULL) { + checkmate::assert_class(fit_post, "TSModelFit") + checkmate::assert_class(fit_prior, "TSModelFit") + t_data <- fit_post$get_data("LON")[[time_var]] + t_pred <- t_pred_auto(t_pred, t_data) + + # Pick one subject from each group + newdat <- group_pred_input(fit_post, group_var, t_pred, time_var) + + # Take draws + if (is.null(prior_param_names)) { + prior_param_names <- id_specific_param_names(fit_post) + } + d <- fit_post$draws() + d_prior <- fit_prior$draws() + for (par in prior_param_names) { + message("using prior draws of ", hl_string(par)) + d[[par]] <- d_prior[[par]] + } + + # Edit draws + if (zero_log_z) { + d <- new_draws(d, fit_post$log_z_pars(), 0) + } + + # Predict using partly posterior and partly prior draws + fit_post$predict(newdat, fitted_params = d) +} + +# Helper +id_specific_param_names <- function(fit) { + id_var <- fit$get_model("LON")$id_var + pn <- names(fit$draws()) + pat <- c( + paste0("^log_z_offset_", id_var), # HierOffsetTerm + paste0("^offset_", id_var), # GroupedOffsetTerm + paste0("^xi_.*", id_var, ".*") # GPTerm + ) + pat <- paste(pat, collapse = "|") + idx <- grepl(pn, pattern = pat) + pn[which(idx)] +} + + +# Helper +group_pred_input <- function(fit, group_var, t_pred, time_var) { + model <- fit$get_model("LON") + id_var <- model$id_var + newsubs <- sample_subjects(fit$get_data("LON"), id_var, group_var) + newdat <- extend_df(newsubs, t_pred, time_var) + newdat[[model$y_var]] <- 1 # value has no effect + newdat +} + +#' Example implementation of plotting trajectory metrics. +#' +#' @export +#' @param df A full long data frame returned by +#' \code{\link{trajectory_metrics}}. +#' @param group_var Name of grouping variable. +#' @param metric Name of the metric. See possible metric names in +#' documentation of \code{\link{trajectory_metrics}}. +plot_metric <- function(df, metric = "depth", group_var = "arm") { + ggplot(df, aes(x = !!sym(group_var), y = !!sym(metric))) + + ggdist::stat_slabinterval() +} diff --git a/R/misc-FormulaParser.R b/R/misc-FormulaParser.R new file mode 100644 index 0000000..21e54d4 --- /dev/null +++ b/R/misc-FormulaParser.R @@ -0,0 +1,150 @@ +# Define the FormulaParser class +FormulaParser <- R6::R6Class("FormulaParser", + private = list( + terms = NULL, + y_name = NULL + ), + public = list( + initialize = function(formula) { + str <- as.character(formula) + private$y_name <- str[2] + private$terms <- create_rhs(str[3]) + }, + num_terms = function() { + length(private$terms) + }, + get_y_name = function() { + private$y_name + }, + parse_term = function(idx) { + checkmate::assert_integerish(idx, lower = 0, upper = self$num_terms()) + t <- private$terms[idx] + pt <- parse_formulaterm(t) + if (pt$type == "gp") { + check_no_hierarchy(pt, "gp") + out <- GPTerm$new(x_name = pt$covariates[1], z_name = pt$covariates[2]) + } else if (pt$type == "offset") { + out <- create_offsetterm(pt$covariates, pt$hierarchy) + } else if (pt$type == "sf") { + out <- create_sfterm(pt$covariates, pt$hierarchy) + } else if (pt$type == "sff") { + out <- create_sffterm(pt$covariates, pt$hierarchy) + } else if (pt$type == "emax") { + out <- EmaxTerm$new( + x_name = pt$covariates[1], + z_name = pt$covariates[2] + ) + } else { + stop("allowed terms are gp(), sf(), sff(), emax() or offset()!") + } + return(out) + }, + parse_terms = function() { + J <- self$num_terms() + terms <- list() + for (j in seq_len(J)) { + terms[[j]] <- self$parse_term(j) + } + gn <- function(x) x$stanname_base() + tn <- sapply(terms, gn) + names(terms) <- tn + terms + } + ) +) + +extract_function_and_argument <- function(input_string) { + # Regular expression to match function name and arguments + regex <- "([^\\(]+)\\((.*)\\)$" + + # Apply regular expression to input string + matches <- regmatches(input_string, regexec(regex, input_string)) + + # Extract matches, if any + if (length(matches[[1]]) > 2) { + func_name <- matches[[1]][2] + args <- matches[[1]][3] + return(list(func_name = func_name, args = args)) + } else { + return(NULL) + } +} + +split_expression <- function(input_string) { + # Initialize variables to keep track of parentheses depth and positions for splitting + paren_depth <- 0 + start_pos <- 1 + parts <- list() + + # Iterate through the string to find top-level plus signs + for (i in 1:nchar(input_string)) { + char <- substr(input_string, i, i) + if (char == "(") { + paren_depth <- paren_depth + 1 + } else if (char == ")") { + paren_depth <- paren_depth - 1 + } else if (char == "+" && paren_depth == 0) { + # Split at top-level plus, excluding the plus sign itself + parts <- c(parts, substr(input_string, start_pos, i - 1)) + start_pos <- i + 1 + } + } + + # Add the last part of the string if there's any remaining + if (start_pos <= nchar(input_string)) { + parts <- c(parts, substr(input_string, start_pos, nchar(input_string))) + } + + return(parts) +} + + +# Parse formula terms +create_rhs <- function(rhs) { + checkmate::assert_character(rhs) + parts <- split_expression(rhs) + trimws(unlist(parts)) +} + +# Parse formula terms +parse_formulaterm <- function(t) { + checkmate::assert_character(t) + a <- extract_function_and_argument(t) + type <- trimws(a$func_name) + rem <- strsplit(a$args, split = "[|]")[[1]] + covs <- trimws(strsplit(rem[1], ",", fixed = TRUE)[[1]]) + if (length(rem) > 2) { + hier <- paste(rem[2:length(rem)], collapse = "|") + } else { + hier <- rem[2] + } + hier <- parse_hier_or_formula(hier) + list( + type = type, + covariates = covs, + hierarchy = hier + ) +} + +# helper +parse_hier_or_formula <- function(hier) { + # split at comma but not inside parentheses + pattern <- ",\\s*(?![^()]*\\))" + hier <- trimws(strsplit(hier, pattern, perl = TRUE)[[1]]) + if (length(hier) < 2) { + if (is.na(hier)) { + hier <- NULL + } + } + hier +} + +# Make sure that error is thrown if hierarchy argument is given +# but will not be used +check_no_hierarchy <- function(parsed_term, type) { + msg <- paste0(type, " terms cannot have hierarchy!") + if (!is.null(parsed_term$hierarchy)) { + stop(msg) + } + invisible(NULL) +} diff --git a/R/misc-FunctionDraws.R b/R/misc-FunctionDraws.R new file mode 100644 index 0000000..ae4c5a4 --- /dev/null +++ b/R/misc-FunctionDraws.R @@ -0,0 +1,439 @@ +#' The 'FunctionDraws' class +#' +#' @export +FunctionDraws <- R6::R6Class("FunctionDraws", + private = list( + input = NULL, + output = NULL, + name = NULL, + var_types = function() { + sapply(private$input, class) + }, + cont_vars = function() { + idx_cont <- which(private$var_types() == "numeric") + if (length(idx_cont) == 0) { + return(NULL) + } + colnames(private$input)[idx_cont] + }, + categ_vars = function() { + idx_cat <- which(private$var_types() == "factor") + if (length(idx_cat) == 0) { + return(NULL) + } + colnames(private$input)[idx_cat] + }, + + # Create data frame for ggplot + quantiles_df_full = function(ci_inner, ci_outer) { + get_q <- function(x, p) { + f <- as.vector(stats::quantile(x, probs = p)) + } + p_in_low <- (1 - ci_inner) / 2 + p_in_up <- 1 - p_in_low + p_out_low <- (1 - ci_outer) / 2 + p_out_up <- 1 - p_out_low + f_draws <- private$output + out_low <- get_q(f_draws, p_out_low) + in_low <- get_q(f_draws, p_in_low) + med <- get_q(f_draws, 0.5) + in_up <- get_q(f_draws, p_in_up) + out_up <- get_q(f_draws, p_out_up) + f_dist <- cbind(out_low, in_low, med, in_up, out_up) + cbind(private$input, data.frame(f_dist)) + } + ), + public = list( + + #' @description Create the object. + #' @param input The data frame used as function input. + #' @param output An \code{rvar} containing draws of the function values + #' at \code{input} points. + #' @param name Function name. + initialize = function(input, output, name) { + checkmate::assert_character(name, min.chars = 1) + checkmate::assert_class(input, "data.frame") + checkmate::assert_class(output, "rvar") + checkmate::assert_true(length(dim(input)) == 2) + checkmate::assert_true(length(dim(output)) == 1) + checkmate::assert_true(nrow(input) == length(output)) + private$input <- input + private$output <- output # rvar + private$name <- name + }, + + #' @description + #' Get number of function draws + num_draws = function() { + posterior::ndraws(private$output) + }, + + #' @description + #' Get number of input points. + num_points = function() { + length(private$output) + }, + + #' @description + #' Get the function name. + get_name = function() { + private$name + }, + + #' @description + #' Rename the function. + #' + #' @param name new name + rename = function(name) { + checkmate::assert_character(name, len = 1) + private$name <- name + }, + + #' @description + #' Get the input data frame. + get_input = function() { + private$input + }, + + #' @description + #' Get the output \code{rvar}. + #' + #' @param as_matrix Transform the \code{rvar} to a draws matrix + #' (see the \code{posterior} package). + get_output = function(as_matrix = FALSE) { + out <- private$output + if (as_matrix) { + out <- posterior::as_draws_matrix(out) + } + out + }, + + #' @description + #' Get the input and output as \code{data.frame} with an \code{rvar} + #' column. + as_data_frame = function() { + value <- self$get_output() + cbind(self$get_input(), value) + }, + + #' @description + #' Get the input and output as a long \code{data.frame} with each draw + #' stacked on top of each other. + as_data_frame_long = function() { + S <- self$num_draws() + N <- self$num_points() + row_idx <- rep(1:N, each = S) + draw_idx <- rep(1:S, times = N) + df <- self$get_input()[row_idx, ] + df[[".draw_idx"]] <- as.factor(draw_idx) + df$value <- as.vector(self$get_output(as_matrix = TRUE)) + df + }, + + #' @description + #' Create (a possibly filtered) data frame for ggplot. + #' @param ci_inner inner credible interval + #' @param ci_outer outer credible interval + #' @param filter_by filtering variable + #' @param kept_vals value of the filter variable that are kept + quantiles_df = function(ci_inner = 0.5, ci_outer = 0.8, + filter_by = NULL, kept_vals = NULL) { + df <- private$quantiles_df_full(ci_inner = ci_inner, ci_outer = ci_outer) + df_filter_rows(df, filter_by, kept_vals) + }, + + #' @description Print object description. + print = function() { + S <- self$num_draws() + P <- self$num_points() + cat("An R6 object of class FunctionDraws (", + number_string(S), " draws, ", number_string(P), " points). \n", + sep = "" + ) + v1 <- private$cont_vars() + s1 <- paste(v1, collapse = ", ") + cat(" - Name:", private$name, "\n") + if (length(v1) > 0) { + cat(" - Continuous inputs:", hl_string(s1), "\n") + } + v2 <- private$categ_vars() + s2 <- paste(v2, collapse = ", ") + if (length(v2) > 0) { + cat(" - Categorical inputs:", hl_string(s2), "\n") + } + }, + + + #' @description + #' Plot using automatically selected aesthetics by default. + #' + #' @param x_var Name of the x-axis variable. + #' @param group_by Name of the factor used as group aesthetic. + #' @param color_by Name of the factor used as color and fill aesthetics. + #' @param ribbon_alpha Opacity of ribbon. + #' @param facet_by Faceting factor. + #' @param ci_inner Inner credible interval (ribbon). + #' @param ci_outer Outer credible interval (ribbon). + #' @param max_levels Maximum number of levels to facet by. + #' @param filter_by Factor to filter the rows by. + #' @param kept_vals Values of the factor that are not filtered out. + plot = function(x_var = "auto", + group_by = "auto", + color_by = "auto", + facet_by = "auto", + ribbon_alpha = 0.3, + ci_inner = 0.5, + ci_outer = 0.8, + max_levels = 40, + filter_by = NULL, + kept_vals = NULL) { + checkmate::assert_number(ci_inner, lower = 0, upper = 1) + checkmate::assert_number(ci_outer, lower = 0, upper = 1) + checkmate::assert_true(ci_outer >= ci_outer) + checkmate::assert_number(ribbon_alpha, lower = 0, upper = 1) + df <- self$quantiles_df( + ci_inner = ci_inner, ci_outer = ci_outer, + filter_by = filter_by, kept_vals = kept_vals + ) + df$Function <- as.factor(rep(private$name, nrow(df))) + z <- private$categ_vars()[1] + is_disabled <- function(x) { + if (is.null(x) || is.na(x) || base::isFALSE(x)) { + return(TRUE) + } + FALSE + } + is_auto <- function(x) { + if (is_disabled(x)) { + return(FALSE) + } + x == "auto" + } + + # Handle auto + if (is_auto(x_var)) { + x_var <- private$cont_vars()[1] + } + if (is_auto(group_by)) { + if (is.null(z)) { + group_by <- NULL + } else { + group_by <- z + } + } + if (is_auto(color_by)) { + if (is.null(z)) { + color_by <- NULL + } else { + color_by <- z + } + } + if (is_auto(facet_by)) { + if (is.null(z)) { + facet_by <- NULL + } else { + facet_by <- z + } + } + + # Avoid slow plots + num_z_facet <- length(unique(df[, facet_by])) + num_z_color <- length(unique(df[, color_by])) + max_levs_plot <- max(c(num_z_color, num_z_facet)) + if (max_levs_plot > max_levels) { + message( + "more than ", max_levels, + " (max_levels) levels to color or facet by,", + " disabling color, faceting, and ribbon" + ) + color_by <- NULL + facet_by <- NULL + ribbon_alpha <- 0 + } + + # Dummy factor if NULL + if (is_disabled(group_by)) { + group_by <- "Function" + } + if (is_disabled(color_by)) { + color_by <- "Function" + } + + # Mapping + mapping <- aes( + x = !!sym(x_var), y = med, + group = !!sym(group_by), color = !!sym(color_by), + fill = !!sym(color_by) + ) + + # Create plot + plt <- ggplot(df, mapping) + if (ribbon_alpha > 0) { + plt <- plt + geom_ribbon( + alpha = ribbon_alpha, mapping = aes(ymin = out_low, ymax = out_up) + ) + + geom_ribbon( + alpha = ribbon_alpha, mapping = aes(ymin = in_low, ymax = in_up) + ) + } + st <- paste0("Median") + if (ribbon_alpha > 0) { + ci <- paste0("{", 100 * ci_inner, ", ", 100 * ci_outer, "}") + st <- paste0(st, " and ", ci, "% Credible Intervals") + } + plt <- plt + geom_line() + ggtitle(private$name, subtitle = st) + ylab("") + if (!is_disabled(facet_by)) { + plt <- plt + facet_wrap(stats::as.formula(paste0(".~", facet_by)), + labeller = label_both + ) + } + plt + }, + + #' @description + #' Scale the draws as \code{f_new = f*a + b} + #' @param a multiplier + #' @param b added constant + scale = function(a = 1, b = 0) { + checkmate::assert_number(a) + checkmate::assert_number(b) + f <- private$output * a + b # arithmetic on rvar + new_name <- paste0(private$name, "_scaled") + FunctionDraws$new(private$input, f, new_name) + }, + + #' @description + #' Add to another \code{FunctionDraws}. You can also use \code{f1 + f2}. + #' @param f A \code{FunctionDraws} object. + add = function(f) { + checkmate::assert_class(f, "FunctionDraws") + x <- df_union(private$input, f$get_input()) + f_sum <- private$output + f$get_output() # sum of rvars + nam <- paste0(private$name, " + ", f$get_name()) + FunctionDraws$new(x, f_sum, nam) + }, + + #' @description + #' Subtract another \code{FunctionDraws}. You can also use \code{f1 - f2}. + #' @param f A \code{FunctionDraws} object. + subtract = function(f) { + checkmate::assert_class(f, "FunctionDraws") + x <- df_union(private$input, f$get_input()) + f_sub <- private$output - f$get_output() # arithmetic of rvars + nam <- paste0(private$name, " - ", f$get_name()) + FunctionDraws$new(x, f_sub, nam) + }, + + #' @description + #' Take log of function draws. + log = function() { + new_name <- paste0("log(", private$name, ")") + FunctionDraws$new(private$input, log(private$output), new_name) + }, + + #' @description + #' Exponentiate function draws. + exp = function() { + new_name <- paste0("exp(", private$name, ")") + FunctionDraws$new(private$input, exp(private$output), new_name) + }, + + #' @description + #' Offset every function draw so that they start from zero + #' @return a new object of class \code{\link{FunctionDraws}} + zero_offset = function() { + new_out <- zero_offset_fd_rvar(self$get_output()) + FunctionDraws$new(private$input, new_out, private$name) + }, + + #' @description + #' Split to groups + #' @return a list of objects of class \code{\link{FunctionDraws}}, + #' one for each group/id + #' @param id_var Name of the subject identifier factor. + split_by_id = function(id_var) { + split_to_subjects(self, id_var) + }, + + #' @description + #' Offset every function draw for each subject so that they start from zero + #' @return a new object of class \code{\link{FunctionDraws}} + #' @param id_var Name of the subject identifier factor. + zero_offset_by_id = function(id_var) { + checkmate::assert_character(id_var, min.chars = 1) + splits <- self$split_by_id(id_var) + out <- splits[[1]]$zero_offset()$get_output() + inp <- splits[[1]]$get_input() + id_new <- rep(names(splits)[1], nrow(inp)) + L <- length(splits) + if (L > 1) { + for (j in 2:L) { + out_j <- splits[[j]]$zero_offset()$get_output() + inp_j <- splits[[j]]$get_input() + id_new_j <- rep(names(splits)[j], nrow(inp_j)) + out <- c(out, out_j) + inp <- rbind(inp, inp_j) + id_new <- c(id_new, id_new_j) + } + } + inp[[id_var]] <- as.factor(id_new) + FunctionDraws$new(inp, out, self$get_name()) + } + ) +) + +#' Add +#' @export +#' @param f1 component 1 +#' @param f2 component 2 +#' @return the sum \code{f1 + f2} +`+.FunctionDraws` <- function(f1, f2) { + f1$add(f2) +} + +#' Subtract +#' @export +#' @param f1 component 1 +#' @param f2 component 2 +#' @return the difference \code{f1 - f2} +`-.FunctionDraws` <- function(f1, f2) { + f1$subtract(f2) +} + +# Split function draws to groups by id +split_to_subjects <- function(fd, id_var) { + inp <- fd$get_input() + out <- fd$get_output() + levs <- unique(inp[[id_var]]) + ret <- list() + j <- 0 + ids_found <- c() + for (id in levs) { + idx_rows <- which(inp[[id_var]] == id) + + if (length(idx_rows) > 0) { + j <- j + 1 + ids_found <- c(ids_found, id) + input <- inp[idx_rows, , drop = FALSE] + output <- out[idx_rows] + name <- paste0("(", fd$get_name(), ")_", id) + ret[[j]] <- FunctionDraws$new(input = input, output = output, name = name) + } + } + names(ret) <- ids_found + ret +} + +# make every function draw start from zero +zero_offset_fd_rvar <- function(fd) { + fd <- posterior::as_draws_array(fd) # dim n_draws x n_chains x n_points + S <- dim(fd)[1] + C <- dim(fd)[2] + for (s in seq_len(S)) { + for (j in seq_len(C)) { + x <- as.numeric(fd[s, j, ]) + fd[s, j, ] <- x - x[1] + } + } + posterior::as_draws_rvars(fd)$x +} diff --git a/R/misc-Transform.R b/R/misc-Transform.R new file mode 100644 index 0000000..8695779 --- /dev/null +++ b/R/misc-Transform.R @@ -0,0 +1,113 @@ +# Define the purely abstract base class Transform +Transform <- R6::R6Class("Transform", + lock_class = TRUE, + private = list( + suffix = NULL + ), + public = list( + print = function() { + cat("An R6 object of class ", class(self)[1], ".\n", sep = "") + }, + forward = function(x) { + stop("'forward()' should be implemented by inheriting class") + }, + backward = function(x) { + stop("'backward()' should be implemented by inheriting class") + }, + add_suffix = function(str) { + checkmate::assert_character(str) + if (is.null(private$suffix)) { + return(str) + } + paste0(str, "_", private$suffix) + } + ) +) + +# Define the LinearTransform (shifts and scales data linearly) +LinearTransform <- R6::R6Class("LinearTransform", + lock_class = TRUE, + inherit = Transform, + private = list( + multiplier = 1.0, + offset = 0.0, + suffix = "shifted_and_scaled" + ), + public = list( + initialize = function(multiplier = 1.0, offset = 0.0) { + private$multiplier <- multiplier + private$offset <- offset + }, + forward = function(x) { + (x + private$offset) * private$multiplier + }, + backward = function(x) { + x / private$multiplier - private$offset + } + ) +) + +# Define the IdentityTransform (doesn't do anything) +IdentityTransform <- R6::R6Class("IdentityTransform", + lock_class = TRUE, + inherit = LinearTransform, + private = list( + suffix = NULL + ), + public = list( + initialize = function() { + super$initialize(multiplier = 1.0, offset = 0.0) + } + ) +) + + +# Define the ScaleTransform (scales data linearly) +ScaleTransform <- R6::R6Class("ScaleTransform", + lock_class = TRUE, + inherit = LinearTransform, + private = list( + suffix = "scaled" + ), + public = list( + initialize = function(multiplier = 1.0) { + super$initialize(multiplier = multiplier) + } + ) +) + +# Define the MaxScaleTransform (like ScaleTransform but can be set_using_data) +MaxScaleTransform <- R6::R6Class("MaxScaleTransform", + lock_class = TRUE, + inherit = ScaleTransform, + public = list( + set_using_data = function(x_data) { + checkmate::assert_numeric(x_data) + max_x <- max(x_data) + checkmate::assert_number(max_x, lower = 1e-12) + mult <- 1.0 / max_x + MaxScaleTransform$new(multiplier = mult) + } + ) +) + + +# Define the UnitScaleTransform (like ScaleTransform but can be set_using_data) +UnitScaleTransform <- R6::R6Class("UnitScaleTransform", + lock_class = TRUE, + inherit = LinearTransform, + private = list( + suffix = "unit" + ), + public = list( + # set so that x_data will map to interval [-1, 1] + set_using_data = function(x_data) { + checkmate::assert_numeric(x_data) + x1 <- max(x_data) + x2 <- min(x_data) + ofs <- -(x1 + x2) / 2 + mult <- 2 / (x1 - x2) + UnitScaleTransform$new(multiplier = mult, offset = ofs) + } + ) +) diff --git a/R/models-BaselineHazard.R b/R/models-BaselineHazard.R new file mode 100644 index 0000000..747132e --- /dev/null +++ b/R/models-BaselineHazard.R @@ -0,0 +1,73 @@ +#' Hazard function (R6 class) +BaselineHazard <- R6::R6Class("BaselineHazard", + inherit = StanCodeCreator, + private = list( + t_name = "t_hazard", + stancode_data_impl = function(datanames) { + paste0(" vector[n_", datanames, "]", self$stanname_t(datanames), ";", + collapse = "\n" + ) + } + ), + public = list( + + + #' @description + #' Get name of the hazard time variable in 'Stan' code + #' + #' @param datanames names of data sets + stanname_t = function(datanames) { + paste0(private$t_name, "_", datanames) + } + ) +) + + +#' Weibull hazard function +#' +#' @export +WeibullHazard <- R6::R6Class("WeibullHazard", + inherit = BaselineHazard, + private = list( + prior_lambda = NULL, + prior_gamma = NULL, + stanfiles_functions_impl = function() { + c("hazard/weibull") + }, + stancode_pars_impl = function() { + paste( + " real h0_lambda; // hazard fun param (scale)", + " real h0_gamma; // hazard fun param (shape)", + sep = "\n" + ) + }, + stancode_tpars_impl = function(datanames) { + c1 <- paste0( + " vector[n_", datanames, "] h0_", datanames, " = weibull_haz(", + self$stanname_t(datanames), ", h0_lambda, h0_gamma);" + ) + paste(c1, collapse = "\n") + }, + stancode_model_impl = function() { + l1 <- paste0(" h0_lambda ~ ", private$prior_lambda, "; // weibull param") + l2 <- paste0(" h0_gamma ~ ", private$prior_gamma, "; // weibull param") + paste(l1, l2, sep = "\n") + } + ), + + # PUBLIC + public = list( + + + #' @description + #' Create hazard + #' + #' @param prior_lambda prior of Weibull scale param + #' @param prior_gamma prior of Weibull shape param + initialize = function(prior_lambda = "gamma(5, 5)", + prior_gamma = "inv_gamma(3, 6)") { + private$prior_lambda <- prior_lambda + private$prior_gamma <- prior_gamma + } + ) +) diff --git a/R/models-JointModel.R b/R/models-JointModel.R new file mode 100644 index 0000000..5e1d87b --- /dev/null +++ b/R/models-JointModel.R @@ -0,0 +1,211 @@ +#' The joint model class (R6 class) +#' +#' @export +#' @field tte The time-to-event model, has class \code{\link{TTEModel}}. +#' @field lon The longitudinal model, has class \code{\link{TSModel}}. +JointModel <- R6::R6Class("JointModel", + inherit = StanModel, + private = list( + loglik_suffix = "joint", + stanfiles_functions_impl = function() { + c( + self$tte$stanfiles_functions(), + self$lon$stanfiles_functions() + ) + }, + stancode_data_impl = function(datanames) { + dn <- c("LON", "TTE", "GRD") + c1 <- self$lon$stancode_data(dn) + c2 <- self$tte$stancode_data(dn) + paste(c1, c2, sep = "\n") + }, + stancode_tdata_impl = function(datanames) { + dn <- c("LON", "TTE", "GRD") + c1 <- self$tte$stancode_tdata(dn) + c2 <- self$lon$stancode_tdata(dn) + paste(c1, c2, sep = "\n") + }, + stancode_pars_impl = function() { + c1 <- self$tte$stancode_pars() + c2 <- self$lon$stancode_pars() + paste(c1, c2, sep = "\n") + }, + stancode_tpars_impl = function(datanames) { + dn <- c("TTE", "GRD") + c1 <- self$lon$stancode_tpars(c("LON", dn)) + c2 <- self$tte$stancode_tpars(dn) + paste(c1, c2, sep = "\n") + }, + stancode_model_impl = function() { + c1 <- self$lon$stancode_model() + c2 <- self$tte$stancode_model() + paste(c1, c2, sep = "\n") + }, + stancode_gq_impl = function() { + c1 <- self$lon$stancode_gq() + c2 <- self$tte$stancode_gq() + paste(c1, c2, sep = "\n") + } + ), + # PUBLIC + public = list( + tte = NULL, + lon = NULL, + + #' @description + #' Create model + #' + #' @param lon An object of class \code{\link{StanModel}}. + #' @param h0 An object of class \code{\link{BaselineHazard}}. + #' @param compile Should the 'Stan' model code be created and compiled. + initialize = function(lon, h0 = NULL, compile = TRUE) { + if (is.null(h0)) { + h0 <- WeibullHazard$new() + } + checkmate::assert_class(h0, "BaselineHazard") + checkmate::assert_class(lon, "StanModel") + tte <- TTEModel$new(h0 = h0, link_name = "f_sum") + tte$id_var <- lon$id_var + self$tte <- tte + self$lon <- lon + super$initialize(compile) + }, + + #' @description + #' The model description as a string + string = function() { + str <- paste0( + class_name_hl(self), ":\n ", + "* Longitudinal submodel = ", self$lon$string(), "\n ", + "* Time-to-event submodel = ", self$tte$string(), "\n" + ) + str + }, + + #' @description + #' Create the 'Stan' data list from data frames. + #' + #' @param data_lon Longitudinal data, a data frame. + #' @param data_tte The events data, a data frame. + #' @param data_grid The integration grid data, a data frame. + #' @param term_confs A list that specifies configuration of model terms. + #' If name of any term is not found from the list, \code{$default_conf()} + #' of that \code{FormulaTerm} is used. + #' @param num_bf If not \code{NULL}, configurations of all + #' \code{GPTerm}s are updated with this value. + #' @param scale_bf If not \code{NULL}, configurations of all + #' \code{GPTerm}s are updated with this value. + #' @param skip_transform Term names whose input transform should be + #' skipped. + #' @param set_transforms If longitudinal data transforms should be set + #' based on the given \code{data_lon}. This should be \code{TRUE} when + #' fitting a model, and \code{FALSE} when computing predictions using GQ. + #' @param prior_only Sample from prior only? + #' @return A list. + create_standata = function(data_lon, + data_tte, + data_grid = NULL, + term_confs = NULL, + num_bf = NULL, + scale_bf = NULL, + skip_transform = NULL, + prior_only = FALSE, + set_transforms = TRUE) { + # Stan input of longitudinal model at longitudinal observations + dl_lon <- self$lon$create_standata( + data_lon, term_confs, num_bf, scale_bf, + skip_transform, prior_only, + set_transforms + ) + + # Stan input of longitudinal model at event observations + full_term_confs <- dl_lon$full_term_confs + dl_tte <- self$lon$term_list$create_standata( + data_tte, "TTE", full_term_confs + ) + + # Stan input of longitudinal model at integration grid + dl_grid <- self$lon$term_list$create_standata( + data_grid, "GRD", full_term_confs + ) + + # Stan input of hazard function and TTE model + dl_tte_haz <- list( + t_hazard_TTE = get_x_from_data(data_tte, "t"), + t_hazard_GRD = get_x_from_data(data_grid, "t") + ) + dl_tte_events <- self$tte$create_standata(data_tte$event) + + + # Combine and take unique fields + sd <- c( + dl_lon$stan_data, dl_tte, + dl_grid, dl_tte_haz, dl_tte_events + ) + sd <- sd[unique(names(sd))] + + # Return + list( + stan_data = sd, + full_term_confs = full_term_confs, + data_lon_orig = dl_lon$orig_data + ) + }, + + + #' @description + #' Fit the model. + #' + #' @param data_lon Longitudinal data, a data frame. + #' @param data_tte The events data, a data frame. + #' @param data_grid The integration grid data, a data frame. Can be + #' created using \code{\link{create_jm_grid}}. + #' @param term_confs A list that specifies configuration of model terms. + #' If name of any term is not found from the list, \code{$default_conf()} + #' of that \code{FormulaTerm} is used. + #' @param num_bf If not \code{NULL}, configurations of all + #' \code{GPTerm}s are updated with this value. + #' @param scale_bf If not \code{NULL}, configurations of all + #' \code{GPTerm}s are updated with this value. + #' @param skip_transform Term names whose input transform should be + #' skipped. + #' @param prior_only Sample from prior only. + #' @param ... Arguments passed to \code{sample} method of the + #' 'CmdStanR' model. + #' @return A \code{\link{JointModelFit}} object. + fit = function(data_lon, + data_tte, + data_grid, + term_confs = NULL, + num_bf = NULL, + scale_bf = NULL, + skip_transform = NULL, + prior_only = FALSE, + ...) { + # Get Stan model object + stan_model <- self$get_stanmodel() + if (is.null(stan_model)) { + stop("Stan model does not exist, you need to call compile()!") + } + + # Create Stan input list + dat <- self$create_standata( + data_lon, data_tte, data_grid, term_confs, num_bf, scale_bf, + skip_transform, prior_only, TRUE + ) + + # Call 'Stan' + stan_fit <- stan_model$sample(data = dat$stan_data, ...) + + # Create the fit object + data_orig <- list( + LON = dat$data_lon_orig, + TTE = data_tte, + GRD = data_grid + ) + JointModelFit$new( + self, stan_fit, data_orig, dat$stan_data, dat$full_term_confs + ) + } + ) +) diff --git a/R/models-StanModel.R b/R/models-StanModel.R new file mode 100644 index 0000000..6c93156 --- /dev/null +++ b/R/models-StanModel.R @@ -0,0 +1,203 @@ +#' Class for objects that can create parts of 'Stan' code +#' +StanCodeCreator <- R6::R6Class("StanCodeCreator", + private = list( + finalize_code = function(code) { + if (nchar(trimws(code)) == 0) { + return("") + } + checkmate::assert_character(code, len = 1) + paste0("\n // ", class_name(self), "\n", code, "\n") + }, + stanfiles_functions_impl = function() { + NULL + }, + stancode_data_impl = function(datanames) { + "" + }, + stancode_tdata_impl = function(datanames) { + "" + }, + stancode_pars_impl = function() { + "" + }, + stancode_tpars_impl = function(datanames) { + "" + }, + stancode_model_impl = function() { + "" + }, + stancode_gq_impl = function() { + "" + } + ), + public = list( + + #' @description + #' Description as a string. + string = function() { + paste0(class_name_hl(self)) + }, + + #' @description + #' Print info. + #' + #' @return Nothing. + print = function() { + cat(self$string(), "\n", sep = "") + }, + + #' @description Stan files where to get the functions. + stanfiles_functions = function() { + files <- c("utils", private$stanfiles_functions_impl()) + unique(files) + }, + + #' @description Generate 'Stan' code for the functions block. + stancode_functions = function() { + files <- self$stanfiles_functions() + code <- "" + for (fn in files) { + code <- paste0(code, read_stan_function(fn), sep = "\n") + } + code + }, + + #' @description Generate 'Stan' code for the data block. + #' @param datanames Names of input data sets. + stancode_data = function(datanames = NULL) { + private$finalize_code(private$stancode_data_impl(datanames)) + }, + + #' @description Generate 'Stan' code for the transformed data block. + #' @param datanames Names of input data sets. + stancode_tdata = function(datanames = NULL) { + private$finalize_code(private$stancode_tdata_impl(datanames)) + }, + + #' @description Generate 'Stan' code for the parameters block. + stancode_pars = function() { + private$finalize_code(private$stancode_pars_impl()) + }, + + #' @description Generate 'Stan' code for the transformed parameters block. + #' @param datanames Names of input data sets. + stancode_tpars = function(datanames = NULL) { + private$finalize_code(private$stancode_tpars_impl(datanames)) + }, + + #' @description Generate 'Stan' code for the model block. + stancode_model = function() { + private$finalize_code(private$stancode_model_impl()) + }, + + #' @description Generate 'Stan' code for the generated quantities block. + stancode_gq = function() { + private$finalize_code(private$stancode_gq_impl()) + } + ) +) + + +#' Abstract for objects that can generate entire 'Stan' models +#' +StanModel <- R6::R6Class("StanModel", + inherit = StanCodeCreator, + + # PRIVATE + private = list( + stan_model = NULL, + stancode_tpars_impl = function(datanames) { + paste0(" real log_lik_empty = 0.0;") + } + ), + + # PUBLIC + public = list( + + #' @description + #' Create model + #' + #' @param compile Should the 'Stan' model code be created and compiled. + initialize = function(compile = TRUE) { + if (compile) { + self$compile() + } + }, + + #' @description Get the underlying 'Stan' model. + get_stanmodel = function() { + private$stan_model + }, + + #' @description + #' Create the 'Stan' model code for the model. + #' + #' @param autoformat Should automatic formatting be attempted? + #' @return A string. + create_stancode = function(autoformat = TRUE) { + checkmate::check_logical(autoformat) + sc_data <- paste( + " int prior_only;\n", + self$stancode_data() + ) + code <- paste( + stancode_block("functions", self$stancode_functions()), + stancode_block("data", sc_data), + stancode_block("transformed data", self$stancode_tdata()), + stancode_block("parameters", self$stancode_pars()), + stancode_block("transformed parameters", self$stancode_tpars()), + stancode_block("model", self$stancode_model()), + stancode_block("generated quantities", self$stancode_gq()), + sep = "\n" + ) + + if (autoformat) { + tryCatch( + { + code <- autoformat_stancode(code) + }, + error = function(e) { + cat(code) + stop(e) + } + ) + } + code + }, + + #' @description + #' Create and compile the 'Stan' model. + #' + #' @param dir Path to directory where to store the \code{.stan} file. + #' @return A 'CmdStanR' model. + create_stanmodel = function(dir = tempdir()) { + code <- self$create_stancode(autoformat = FALSE) + a <- cmdstanr::write_stan_file(code = code, dir = dir) + + # silence compile warnings from cmdstan + utils::capture.output( + { + mod <- cmdstanr::cmdstan_model(a) + }, + type = "message" + ) + mod + }, + + #' @description + #' Create and compile the 'Stan' model. + #' + #' @param ... Arguments passed to \code{create_stanmodel()}. + #' @return The updated model object (invisibly). + compile = function(...) { + private$stan_model <- self$create_stanmodel(...) + invisible(self) + } + ) +) + +# Helper +stancode_block <- function(name, code) { + paste0(name, "{\n", code, "\n}\n") +} diff --git a/R/models-TSModel.R b/R/models-TSModel.R new file mode 100644 index 0000000..fcfe22b --- /dev/null +++ b/R/models-TSModel.R @@ -0,0 +1,372 @@ +#' Can be used to just generate Stan code for a TS model +# given a formula +#' +#' @export +#' @param formula model formula +#' @param print Should the code be printed? +#' @param ... Arguments passed to the \code{$create_stancode()} method +#' of \code{\link{TSModel}}. +#' @return Stan code as character string (invisibly) +stancode_ts <- function(formula, print = TRUE, ...) { + checkmate::assert_class(formula, "formula") + m <- TSModel$new(formula, compile = F) + code <- m$create_stancode(...) + if (print) { + cat(code) + } + invisible(code) +} + +#' Time series (longitudinal) model class (R6 class) +#' +#' @export +#' @field term_list The additive model terms. +#' @field y_var Name of the y variable. +#' @field id_var Name of the subject identifier variable. +#' @field prior_sigma Prior for the noise parameter. +#' @field sigma_upper Upper bound for the noise parameter. +#' @field sigma_lower Lower for the noise parameter. +#' @field log_y_cap Upper bound on log scale for creating a capped +#' predicted signal or predicted observations. +TSModel <- R6::R6Class("TSModel", + inherit = StanModel, + + # PRIVATE + private = list( + loglik_suffix = "lon", + default_dataname = function(datanames) { + if (is.null(datanames)) { + datanames <- "LON" + } + datanames + }, + stanfiles_functions_impl = function() { + self$term_list$stanfiles_functions() + }, + stancode_data_impl = function(datanames) { + datanames <- private$default_dataname(datanames) + code <- paste0( + " int n_", datanames, ";", + " // number of input points (", datanames, ")" + ) + paste( + paste(code, collapse = "\n"), + self$term_list$stancode_data(datanames), + stancode_ts_data(self$stanname_y(), "LON"), + sep = "\n" + ) + }, + stancode_tdata_impl = function(datanames) { + datanames <- private$default_dataname(datanames) + dn_def <- private$default_dataname(NULL) + c1 <- stancode_ts_tdata(self$stanname_y(), dn_def) + c2 <- self$term_list$stancode_tdata(datanames) + paste(c1, c2, sep = "\n") + }, + stancode_pars_impl = function() { + scb_sigma <- stancode_bounds(self$sigma_lower, upper = self$sigma_upper) + c1 <- paste0(" real", scb_sigma, " sigma; // noise magnitude \n") + c2 <- self$term_list$stancode_pars() + paste(c1, c2, sep = "\n") + }, + stancode_tpars_impl = function(datanames) { + datanames <- private$default_dataname(datanames) + code <- self$term_list$stancode_tpars(datanames) + dn_def <- private$default_dataname(NULL) + paste(code, + stancode_ts_likelihood(self$stanname_y(), dn_def), + sep = "\n" + ) + }, + stancode_model_impl = function() { + c1 <- paste0(" sigma ~ ", self$prior_sigma, ";\n") + c2 <- self$term_list$stancode_model() + c3 <- stancode_loglik(private$loglik_suffix) + paste(c1, c2, c3, sep = "\n") + }, + stancode_gq_impl = function() { + terms_gq <- self$term_list$stancode_gq() + stancode_ts_gq(self, self$stanname_y(), terms_gq) + }, + delta = NULL + ), + + # PUBLIC + public = list( + id_var = NULL, + term_list = NULL, + y_var = NULL, + prior_sigma = "normal(0, 2)", + sigma_upper = 3, + sigma_lower = 0, + log_y_cap = 7, + + #' @description + #' Create model + #' + #' @param formula The model formula determining the terms and the y + #' variable (longitudinal observation). + #' @param id_var Name of the subject identifier variable. + #' @param compile Should the 'Stan' model code be created and compiled. + #' @param delta Offset for log transform (\code{y_log = log(y + delta)}). + #' @param prior_sigma Prior for sigma + #' @param prior_terms A list with names equal to a subset of the + #' names of the model terms. Can be used to edit priors of term parameters. + #' @param prior_baseline Prior for the baseline term. + #' @param sigma_upper Upper bound for sigma + #' @param sigma_lower Lower bound for sigma + #' @param baseline Baseline term definition. Created automatically based + #' on \code{id_var} if \code{NULL} (default). + initialize = function(formula, id_var = "id", compile = TRUE, delta = 0, + baseline = NULL, + prior_baseline = NULL, + prior_terms = NULL, + prior_sigma = "normal(0, 2)", + sigma_upper = 3, + sigma_lower = 0) { + checkmate::assert_character(id_var, min.chars = 1) + checkmate::assert_class(formula, "formula") + + # Handle adding the baseline term + complete <- complete_formula_ts(formula, id_var, baseline) + formula <- complete$formula + baseline <- complete$baseline + prior_terms <- complete_prior_terms(prior_terms, prior_baseline, baseline) + + # Set fields + self$prior_sigma <- prior_sigma + self$sigma_upper <- sigma_upper + self$sigma_lower <- sigma_lower + self$term_list <- create_termlist(formula, prior_terms) + self$y_var <- FormulaParser$new(formula)$get_y_name() + self$id_var <- id_var + private$delta <- delta + + # Compile + super$initialize(compile) + }, + + #' @description + #' Get name of y variable in Stan code. + stanname_y = function() { + paste0("dat_", self$y_var) + }, + + #' @description + #' The model description as a string + string = function() { + str <- paste0( + class_name_hl(self), ":\n ", + "log(", self$y_var, "+", self$get_delta(), ") ~ N(f, sigma^2)" + ) + tls <- self$term_list$string() + paste0(str, ", where \n", " f = ", hl_string(tls)) + }, + + #' @description Get value of \code{delta}. + get_delta = function() { + private$delta + }, + + #' @description Get term names in Stan code. + #' @return A character vector with length equal to number of terms. + term_names = function() { + self$term_list$stan_names() + }, + + #' @description + #' Create the 'Stan' data list from a data frame. Performs normalization + #' on continuous variables that are input to GP terms. + #' + #' @param data A data frame. + #' @param term_confs A list that specifies configuration of model terms. + #' If name of any term is not found from the list, \code{$default_conf()} + #' of that \code{FormulaTerm} is used. + #' @param num_bf If not \code{NULL}, configurations of all + #' \code{GPTerm}s are updated with this value. + #' @param scale_bf If not \code{NULL}, configurations of all + #' \code{GPTerm}s are updated with this value. + #' @param skip_transform Term names whose input transform should be + #' skipped. + #' @param prior_only Sample from prior only? + #' @param set_transforms If data transforms should be set based on the given + #' \code{data}. This should be \code{TRUE} when fitting a model, and + #' \code{FALSE} when computing predictions using GQ. + #' @param dataname Name of dataset. + #' @return A list. + create_standata = function(data, + term_confs = NULL, + num_bf = NULL, + scale_bf = NULL, + skip_transform = NULL, + prior_only = FALSE, + set_transforms = TRUE, + dataname = "LON") { + checkmate::assert_data_frame(data, col.names = "named") + checkmate::assert_logical(set_transforms) + checkmate::assert_character(dataname) + + # Fill missing term configurations + full_term_confs <- self$term_list$fill_term_confs( + term_confs, + num_bf = num_bf, + scale_bf = scale_bf + ) + + # Set variable transforms + if (set_transforms) { + self$term_list$set_transforms(data, skip_transform) + } + + # Prepare 'Stan' data + data <- ensure_id_var_exists(data, self$id_var) + + # Create final data list + stan_data <- standata_ts( + data, dataname, self$term_list, full_term_confs, self$y_var, + private$delta + ) + + # Return + list( + orig_data = data, + stan_data = finalize_stan_data(stan_data, prior_only), + full_term_confs = full_term_confs + ) + }, + + + #' @description + #' Fit the model. + #' + #' @param data A data frame. + #' @param term_confs A list that specifies configuration of model terms. + #' If name of any term is not found from the list, \code{$default_conf()} + #' of that \code{FormulaTerm} is used. + #' @param num_bf If not \code{NULL}, configurations of all + #' \code{GPTerm}s are updated with this value. + #' @param scale_bf If not \code{NULL}, configurations of all + #' \code{GPTerm}s are updated with this value. + #' @param skip_transform Term names whose input transform should be + #' skipped. + #' @param prior_only Sample from prior only. + #' @param ... Arguments passed to \code{sample} method of the + #' 'CmdStanR' model. + #' @return An \code{\link{TSModelFit}} object. + fit = function(data, + term_confs = NULL, + num_bf = NULL, + scale_bf = NULL, + skip_transform = NULL, + prior_only = FALSE, + ...) { + # Get Stan model object + stan_model <- self$get_stanmodel() + if (is.null(stan_model)) { + stop("Stan model does not exist, you need to call compile()!") + } + + # Create Stan input list + d <- self$create_standata( + data, term_confs, num_bf, scale_bf, + skip_transform, prior_only, TRUE + ) + + # Call 'Stan' + stan_fit <- stan_model$sample(data = d$stan_data, ...) + + # Return + dat_list <- list(LON = d$orig_data) + TSModelFit$new(self, stan_fit, dat_list, d$stan_data, d$full_term_confs) + } + ) +) + + +# Stan data for TS model +standata_ts <- function(data, dataname, term_list, term_confs, y_name, delta) { + sd_y <- standata_ts_y(data, y_name, delta) + sd_x <- term_list$create_standata(data, dataname, term_confs) + sd <- c(sd_x, sd_y) + sd$prior_only <- 0 + sd +} + +# Stan data specific to TS model +standata_ts_y <- function(data, y_name, delta) { + checkmate::assert_character(y_name) + y <- data[[y_name]] + checkmate::assert_numeric(y) + out <- list(y, delta) + names(out) <- c(paste0("dat_", y_name), "delta") + out +} + +# Ensure that data has id specifier +ensure_id_var_exists <- function(data, id_var) { + id_var_given <- id_var %in% colnames(data) + msg <- paste0(id_var, " not found in data, assuming data are from same id") + if (!(id_var_given)) { + message(msg) + data[[id_var]] <- as.factor(rep(1, nrow(data))) + } + data +} + +# Complete the formula for TS model +complete_formula_ts <- function(formula, id_var, baseline) { + ff <- as.character(formula) + if (is.null(baseline)) { + baseline <- paste0("offset(", id_var, ")") + message("baseline was NULL, setting baseline = ", hl_string(baseline)) + } + checkmate::assert_character(baseline, min.chars = 1) + if (ff[3] == ".") { + formula <- as.formula(paste0(ff[2], " ~ ", baseline)) + } else { + dpf <- deparse(formula) + dpf <- paste(dpf, collapse = "") + formula <- as.formula(paste(dpf, "+", baseline)) + } + list( + formula = formula, + baseline = baseline + ) +} + +# Complete the prior list for terms, handle baseline separately +complete_prior_terms <- function(prior_terms, prior_baseline, baseline) { + baseline_stan <- term_to_code(baseline) + if (baseline_stan %in% names(prior_terms)) { + stop( + "prior for baseline should not be given in prior_terms, instead use ", + "prior_baseline" + ) + } + prior_terms[[baseline_stan]] <- prior_baseline + prior_terms +} + +# Helper +sigma_star_lognormal <- function(p) { + 0.5 * (log(1 + p) - log(1 - p)) +} + +#' Create a prior of noise parameter based on information about expected +#' amount of measurement error +#' +#' @export +#' @param p A priori the most likely value of measurement error percentage +#' @param p_sd Prior std in measurement error percentage +prior_sigma_informed <- function(p = 0.065, p_sd = 0.015) { + checkmate::assert_number(p, lower = 0, upper = 1) + checkmate::assert_number(p_sd, lower = 0, upper = 1) + sigma_mean <- sigma_star_lognormal(p) + ss_up <- sigma_star_lognormal(p + p_sd) + ss_low <- sigma_star_lognormal(p - p_sd) + diffs <- c(sigma_mean - ss_up, sigma_mean - ss_low) + sigma_sd <- mean(abs(diffs)) + m <- round(sigma_mean, 5) + s <- round(sigma_sd, 5) + paste0("normal(", m, ", ", s, ")") +} diff --git a/R/models-TTEModel.R b/R/models-TTEModel.R new file mode 100644 index 0000000..0d36b7b --- /dev/null +++ b/R/models-TTEModel.R @@ -0,0 +1,134 @@ +#' The time-to-event model class (R6 class) +#' +#' @export +#' @field h0 An object of class \code{\link{BaselineHazard}}. +#' @field id_var name of id variable +TTEModel <- R6::R6Class("TTEModel", + inherit = StanModel, + private = list( + link_name = NULL, + loglik_suffix = "tte", + stanfiles_functions_impl = function() { + c( + self$h0$stanfiles_functions(), + "hazard/integrate", + "hazard/inst_hazard" + ) + }, + stancode_data_impl = function(datanames) { + datanames <- c("TTE", "GRD") + c1 <- self$h0$stancode_data(datanames) + c2a <- " // Events" + c2 <- paste0(" int num_events;") + c3 <- paste0(" array[num_events] int event_ids;") + paste(c1, c2a, c2, c3, sep = "\n") + }, + stancode_tdata_impl = function(datanames) { + dn <- c("GRD", "TTE") + c1 <- self$h0$stancode_tdata(dn) + c2 <- " // Indices (TTEModel)" + id_var <- self$id_var + c3 <- paste0( + " array[G_", id_var, ", 2] int inds_", dn, " = inds_array(", + self$stanname_id(dn), ", G_", id_var, "); ", + collapse = "\n" + ) + paste(c1, c2, c3, sep = "\n") + }, + stancode_pars_impl = function() { + paste( + self$h0$stancode_pars(), + " real assoc; // association parameter", + sep = "\n" + ) + }, + stancode_tpars_impl = function(datanames) { + dn <- c("TTE", "GRD") + c0 <- self$h0$stancode_tpars(dn) + + c1a <- " // Instant and cumulative hazard" + t_out <- self$h0$stanname_t(dn[1]) + t_grid <- self$h0$stanname_t(dn[2]) + y_grid <- paste0("inst_haz_", dn[2]) + inds_out <- paste0("inds_", dn[1]) + inds_grid <- paste0("inds_", dn[2]) + c1 <- stancall_inst_haz(dn, private$link_name) + c2 <- paste0( + " vector[n_", dn[1], "] cum_haz_", dn[1], + " = integrate_1d_many(", t_out, ",", t_grid, ", ", y_grid, ", ", + inds_out, ", ", inds_grid, ");" + ) + c3 <- "\n // Log likelihood" + c4 <- paste0( + " real log_lik_", private$loglik_suffix, + " = sum(-cum_haz_TTE) + ", + "sum(log(inst_haz_TTE[event_ids]));" + ) + paste(c0, c1a, c1, c2, c3, c4, sep = "\n") + }, + stancode_model_impl = function() { + c1 <- self$h0$stancode_model() + c2 <- " assoc ~ normal(0, 1);" + c3 <- stancode_loglik(private$loglik_suffix) + paste(c1, c2, c3, sep = "\n") + }, + stancode_gq_impl = function() { + dn <- "TTE" + c1 <- self$h0$stancode_gq() + c2 <- paste0(" vector[n_", dn, "] inst_haz = inst_haz_", dn, ";") + c3 <- paste0(" vector[n_", dn, "] cum_haz = cum_haz_", dn, ";") + paste(c1, c2, c3, sep = "\n") + } + ), + # PUBLIC + public = list( + h0 = NULL, + id_var = "id", + + #' @description Get name of id variable in 'Stan' code. + #' @param datanames names of data sets + stanname_id = function(datanames) { + paste0("dat_", self$id_var, "_", datanames) + }, + + #' @description + #' Create model + #' + #' @param h0 An object of class \code{\link{BaselineHazard}}. + #' @param link_name Base name of the link variable in 'Stan' code. + initialize = function(h0 = NULL, link_name = "f_sum") { + private$link_name <- link_name + if (is.null(h0)) { + h0 <- WeibullHazard$new() + } + checkmate::assert_class(h0, "BaselineHazard") + self$h0 <- h0 + super$initialize(compile = FALSE) + }, + + #' @description + #' Create the 'Stan' data list from a data vector. + #' + #' @param event a logical vector + #' @return A list. + create_standata = function(event) { + checkmate::assert_logical(event) + + # Return + list( + event_ids = which(event), + num_events = sum(event) + ) + } + ) +) + + +# Stan code line that computes instant hazard +stancall_inst_haz <- function(dn, link_name) { + paste0(" vector[n_", dn, "] inst_haz_", dn, + " = compute_inst_hazard(h0_", dn, ", assoc, ", + link_name, "_", dn, ", y_loc, y_scale, delta);", + collapse = "\n" + ) +} diff --git a/R/terms-EmaxTerm.R b/R/terms-EmaxTerm.R new file mode 100644 index 0000000..a9e0ef2 --- /dev/null +++ b/R/terms-EmaxTerm.R @@ -0,0 +1,63 @@ +# Define the EmaxTerm class +EmaxTerm <- R6::R6Class("EmaxTerm", + inherit = FormulaTerm, + lock_class = TRUE, + private = list( + latex_type = "EM", + latex_param_names = function() { + c("\text{ED}_{50}", "E_{\text{MAX}}", "gamma") + }, + stanfiles_functions_impl = function() { + c("emax") + } + ), + public = list( + prior_log_ed50 = "student_t(4, 5, 2)", + prior_gamma = "inv_gamma(5, 2)", + prior_emax = "student_t(4, 0, 1)", + initialize = function(x_name, z_name) { + checkmate::assert_character(x_name, any.missing = FALSE) + checkmate::assert_character(z_name, any.missing = FALSE) + private$typename <- "emax" + private$suffix <- paste0(x_name, "X", z_name) + self$x_name <- x_name + self$z_name <- z_name + }, + stancode_data = function(used_names, datanames) { + x <- self$stanlines_data_x(datanames) + z <- self$stanlines_data_z(datanames) + lines <- c(x$lines, z$lines) + names <- c(x$stannames, z$stannames) + build_stancode_data(lines, names, used_names) + }, + stancode_pars = function() { + gn <- paste0("G_", self$z_name) + line1 <- paste0(" vector[", gn, "] log_ED50;\n") + line2 <- paste0(" real Emax", "; // max effect") + line3 <- paste0(" real gamma", "; // shape") + paste0(line1, "\n", line2, "\n", line3, "\n") + }, + stancode_tpars = function(datanames) { + sn <- self$stanname(datanames) + sx <- self$stanname_x(datanames) + nn <- paste0("n_", datanames) + dnz <- self$stanname_z(datanames) + fc <- paste0("emax(", sx, ", log_ED50[", dnz, "], Emax, gamma)") + c1 <- paste0(" vector[", nn, "] ", sn, " = ", fc, ";") + paste(c1, collapse = "\n") + }, + stancode_model = function() { + line1 <- paste0(" log_ED50 ~ ", self$prior_log_ed50, ";") + line2 <- paste0(" Emax ~ ", self$prior_emax, ";") + line3 <- paste0(" gamma ~ ", self$prior_gamma, ";") + paste(line1, line2, line3, sep = "\n") + }, + standata = function(datasets, conf) { + conf <- self$ensure_conf(conf) + c( + self$create_standata_x(datasets), + self$create_standata_z(datasets) + ) + } + ) +) diff --git a/R/terms-FormulaTerm.R b/R/terms-FormulaTerm.R new file mode 100644 index 0000000..26940ee --- /dev/null +++ b/R/terms-FormulaTerm.R @@ -0,0 +1,279 @@ +# Define the purely abstract base class FormulaTerm +FormulaTerm <- R6::R6Class("FormulaTerm", + inherit = StanCodeCreator, + lock_class = TRUE, + private = list( + typename = NULL, + suffix = NULL, + latex_type = "UNKNOWN", + latex_param_names = function() { + NULL + }, + latex_param_subscript = NULL, + latex_params = function() { + pn <- private$latex_param_names() + sub <- private$latex_param_subscript + if (is.null(sub)) { + codes <- pn + } else { + codes <- sapply(pn, function(x) { + suffix <- paste0("_{", sub, "}") + if (x == "\\ldots") { + return(x) + } else { + return(paste0(x, suffix)) + } + }) + } + paste(codes, collapse = ", ") + } + ), + public = list( + x_name = NULL, + z_name = NULL, + h_name = NULL, # hierarchy + x_transform = IdentityTransform$new(), + has_x = function() { + !is.null(self$x_name) + }, + has_z = function() { + !is.null(self$z_name) + }, + has_hierarchy = function() { + !is.null(self$h_name) + }, + input_vars = function() { + unique(c(self$x_name, self$z_name, self$h_name)) + }, + finalize_code = function(code) { + checkmate::assert_character(code, len = 1) + code <- trimws(code) + if (nchar(code) == 0) { + return("") + } + tn_long <- self$name_long() + paste0(" // Term ", tn_long, "\n ", code, "\n") + }, + stanname_x = function(datanames) { + x_name <- paste0("dat_", self$x_name) + paste0(self$x_transform$add_suffix(x_name), "_", datanames) + }, + stanname_z = function(datanames) { + paste0("dat_", self$z_name, "_", datanames) + }, + stanname_h = function(datanames) { + paste0("dat_", self$h_name, "_", datanames) + }, + stanname_base = function() { + paste0("f_", private$typename, "_", private$suffix) + }, + stanname = function(datanames) { + paste0(self$stanname_base(), "_", datanames) + }, + name_long = function() { + paste0(self$stanname_base(), " (", class_name(self), ")") + }, + latex = function() { + cov_names <- c(self$x_name, self$z_name, self$h_name) + vars <- paste(paste0("\\text{", cov_names, "}"), collapse = ", ") + pars <- private$latex_params() + typ <- private$latex_type + paste0("f^{\\text{", typ, "}} \\left(", vars, " \\mid ", pars, "\\right)") + }, + stancode_data = function(used_names, datanames) { + list(code = "", stannames = character(0)) + }, + stancode_gq = function() { + dataname <- "LON" + tn <- self$stanname_base() + tn_tpar <- self$stanname(dataname) + nn <- paste0("n_", dataname) + paste0(" vector[", nn, "] ", tn, " = ", tn_tpar, ";") + }, + print = function() { + cat(class_name(self), "(base name in Stan code = ", + hl_string(self$stanname_base()), ").\n", + sep = "" + ) + }, + standata = function(datasets, conf) { + list() + }, + stanlines_data_x = function(datanames) { + dnx <- self$stanname_x(datanames) + nn <- paste0("n_", datanames) + lines <- as.list( + paste0(" vector[", nn, "] ", dnx, "; // continuous input") + ) + list(lines = lines, stannames = c(dnx)) + }, + stanlines_data_z = function(datanames) { + dnz <- self$stanname_z(datanames) + dnG <- paste0("G_", self$z_name) + nn <- paste0("n_", datanames) + zlines <- paste0(" array[", nn, "] int ", dnz, "; // categ input") + lines <- c( + as.list(zlines), + list(paste0(" int ", dnG, "; // num of groups")) + ) + list(lines = lines, stannames = c(dnz, dnG)) + }, + stanlines_data_h = function(datanames) { + dnh <- self$stanname_h(datanames) + dnG <- paste0("G_", self$h_name) + nn <- paste0("n_", datanames) + hlines <- paste0(" array[", nn, "] int ", dnh, "; // categ input") + lines <- c( + as.list(hlines), + list(paste0(" int ", dnG, "; // num of groups")) + ) + list(lines = lines, stannames = c(dnh, dnG)) + }, + create_standata_x = function(datasets) { + checkmate::assert_list(datasets, min.len = 1) + datanames <- names(datasets) + j <- 0 + OUT <- list() + for (df in datasets) { + j <- j + 1 + checkmate::assert_data_frame(df, col.names = "named") + x <- get_x_from_data(df, self$x_name) + x <- self$x_transform$forward(x) # always give transformed data to Stan + out <- list(x) + snx <- self$stanname_x(datanames[j]) + names(out) <- c(snx) + OUT <- c(OUT, out) + } + OUT + }, + create_standata_z = function(datasets) { + checkmate::assert_list(datasets, min.len = 1) + nn <- names(datasets) + create_standata_categorical(datasets, self$z_name, self$stanname_z(nn)) + }, + create_standata_h = function(datasets) { + checkmate::assert_list(datasets, min.len = 1) + nn <- names(datasets) + create_standata_categorical(datasets, self$h_name, self$stanname_h(nn)) + }, + default_conf = function() { + list() + }, + required_conf_names = function() { + c() + }, + + # this should not be overridden by inheriting class + ensure_conf = function(conf) { + str <- hl_string(self$stanname_base()) + if (length(conf) == 0) { + message( + "Configuration not supplied for term ", + str, ", using default configuration" + ) + conf <- self$default_conf() + } else { + message("Configuration found for term ", str, "!") + } + checkmate::assert_list(conf, names = "named") + cn <- self$required_conf_names() + for (nam in cn) { + if (!(nam %in% names(conf))) { + stop(nam, " not found in conf list!") + } + } + conf + } + ) +) + +# Util +name_does_not_exist <- function(name, names = NULL) { + checkmate::assert_character(name) + if (!is.null(names)) { + checkmate::assert_character(names) + } + !(name %in% names) +} + +# Appends line to Stan code if variable name is not already in use +append_to_datablock <- function(code, line, var_name, used_names) { + if (name_does_not_exist(var_name, used_names)) { + code <- paste0(code, line, "\n") + } + code +} + +# To be called in the end of stancode_data methods of FormulaTerms +build_stancode_data <- function(lines, stannames, used_names) { + code <- "" + J <- length(stannames) + if (length(lines) != J) { + stop( + "length(lines) != length(stannames) in build_stancode_data(), ", + "please report a bug." + ) + } + for (j in seq_len(J)) { + code <- append_to_datablock(code, lines[[j]], stannames[j], used_names) + used_names <- c(used_names, stannames[j]) + } + list( + code = code, + stannames = stannames + ) +} + +# Get continuous variable named 'xn' from data frame +get_x_from_data <- function(data, xn) { + x <- data[[xn]] + if (is.null(x)) { + stop(xn, " not found in data!") + } + checkmate::assert_numeric(x) + x +} + +# Helper +create_standata_categorical <- function(datasets, name, stannames) { + zn <- name + j <- 0 + OUT <- list() + for (df in datasets) { + j <- j + 1 + checkmate::assert_data_frame(df, col.names = "named") + z <- df[[zn]] + if (is.null(z)) { + stop(zn, " not found in data!") + } + checkmate::assert_factor(z) + G <- length(levels(z)) + out <- list( + as.numeric(z), + G + ) + gn <- paste0("G_", zn) + nams <- c(stannames[j], gn) + names(out) <- nams + OUT <- c(OUT, out) + } + OUT +} + +# Stan code variable name suffix for interaction term +stancode_suffix_interact <- function(x_name, z_name) { + ts <- x_name + if (!is.null(z_name)) { + ts <- paste0(ts, "X", z_name) + } + ts +} + +# Latex subscript for interaction term +latex_subscript_interact <- function(x_name, z_name) { + ts <- paste0("\\text{", x_name, "}") + if (!is.null(z_name)) { + ts <- paste0(ts, " \\times ", paste0("\\text{", z_name, "}")) + } + ts +} diff --git a/R/terms-GPTerm.R b/R/terms-GPTerm.R new file mode 100644 index 0000000..57e6e0d --- /dev/null +++ b/R/terms-GPTerm.R @@ -0,0 +1,224 @@ +# Define the GPTerm class +GPTerm <- R6::R6Class("GPTerm", + inherit = FormulaTerm, + lock_class = TRUE, + private = list( + stanfiles_functions_impl = function() { + c("gp/basisfun", "gp/shared", "gp/group") + }, + latex_xi = function(z_name) { + if (is.null(z_name)) { + out <- "\\mathbf{\\xi}" + } else { + G <- paste0("G_{", z_name, "}") + out <- c( + "\\mathbf{\\xi}^{(1)}", "\\ldots", + paste0("\\mathbf{\\xi}^{(", G, ")}") + ) + } + return(out) + }, + latex_type = "HSGP", + latex_param_names = function() { + c(private$latex_xi(self$z_name), "\\alpha", "\\ell", "B", "L") + } + ), + public = list( + x_transform = UnitScaleTransform$new(), + initialize = function(x_name, z_name = NULL) { + if (is.na(z_name)) { + z_name <- NULL + } + checkmate::assert_character(x_name, any.missing = FALSE) + if (!is.null(z_name)) { + checkmate::assert_character(z_name, any.missing = FALSE) + } + self$x_name <- x_name + self$z_name <- z_name + private$typename <- "gp" + private$suffix <- stancode_suffix_interact(x_name, z_name) + private$latex_param_subscript <- latex_subscript_interact(x_name, z_name) + }, + default_conf = function() { + list(scale_bf = 1.5, num_bf = 30) + }, + required_conf_names = function() { + c("scale_bf", "num_bf") + }, + stancode_data = function(used_names, datanames) { + x <- self$stanlines_data_x(datanames) + gp <- stanlines_data_hsgp(private$suffix) + if (self$has_z()) { + z <- self$stanlines_data_z(datanames) + } else { + z <- list(lines = NULL, stannames = NULL) + } + lines <- c(x$lines, gp$lines, z$lines) + names <- c(x$stannames, gp$stannames, z$stannames) + build_stancode_data(lines, names, used_names) + }, + stancode_tdata = function(datanames) { + tn <- private$suffix + xn <- self$stanname_x(datanames) + bn <- paste0("B_", tn) + mn <- paste0("mat_B_", tn, "_", datanames) + sn <- paste0("seq_B_", tn) + c1 <- paste0(" vector[", bn, "] ", sn, " = seq_len(", bn, ");\n") + c2 <- paste0(" ", stancode_B_matrix(datanames, bn, mn, sn), ";\n") + c3 <- paste0(" ", stancode_PHI_matrix(datanames, bn, mn, tn, xn), ";\n") + paste0(c(c1, c2, c3), collapse = "") + }, + stancode_pars = function() { + tn <- private$suffix + gn <- paste0("G_", self$z_name) + code <- paste0("\n real alpha_", tn, "; // magnitude") + code <- paste0(code, "\n real ell_", tn, "; // lengthscale") + if (!self$has_z()) { + code <- paste0(code, "\n vector[B_", tn, "] xi_", tn, "; // auxiliary") + } else { + code <- paste0(code, "\n matrix[", gn, ", B_", tn, "] xi_", tn, ";") + } + paste0(code, "\n") + }, + stancode_tpars = function(datanames) { + sfx <- private$suffix + sn <- self$stanname(datanames) + zn <- self$stanname_z(datanames) + c1 <- stancall_bf(sfx) + if (self$has_z()) { + c2 <- stancall_gp_group(datanames, sn, sfx, zn) + } else { + c2 <- stancall_gp_shared(datanames, sn, sfx) + } + paste0(c(c1, c2), collapse = "") + }, + stancode_model = function() { + tn <- private$suffix + code <- paste0("\n alpha_", tn, " ~ student_t(20, 0, 1);") + code <- paste0(code, "\n ell_", tn, " ~ lognormal(0, 1);") + code <- paste0(code, "\n to_vector(xi_", tn, ") ~ normal(0, 1);") + paste0(code, "\n") + }, + standata = function(datasets, conf) { + datanames <- names(datasets) + conf <- self$ensure_conf(conf) + ts <- private$suffix + + # Stan data for continuous variable + xn <- self$x_name + dat <- self$create_standata_x(datasets) + sn_x <- self$stanname_x(datanames) + + # HSGP Stan data + term_names <- self$stanname(datanames) + j <- 0 + for (tn in term_names) { + j <- j + 1 + dat_x_unit <- dat[[sn_x[j]]] + dat_hsgp_j <- create_standata_hsgp(ts, conf, dat_x_unit, xn, tn) + dat <- c(dat, dat_hsgp_j) + } + + # Stan data for categorical variable + if (self$has_z()) { + dat_z <- self$create_standata_z(datasets) + } else { + dat_z <- NULL + } + + # Return + c(dat, dat_z) + } + ) +) + +# Helper +stancode_B_matrix <- function(datasets, bn, mn, sn) { + nn <- paste0("n_", datasets) + paste0( + "matrix[", nn, ", ", bn, "] ", mn, " = transpose(rep_matrix(", + sn, ", ", nn, "))" + ) +} + +# Helper +stancode_PHI_matrix <- function(datasets, bn, mn, tn, xn) { + nn <- paste0("n_", datasets) + tn_sfx <- paste0(tn, "_", datasets) + paste0( + "matrix[", nn, ", ", bn, "] PHI_", tn_sfx, + " = bf_eq(", xn, ", ", mn, ", L_", tn, ")" + ) +} + +# Prevent improper use +ensure_gp_approx_is_valid <- function(dat_x_unit, x_name, L, term_name) { + max_abs_x <- max(abs(dat_x_unit)) + x_name <- paste0(x_name, " (transformed to unit scale)") + if (max_abs_x > L) { + msg <- paste0("Error in term '", term_name, "':\n") + msg <- paste0( + msg, + " max absolute value of ", x_name, " is larger than L = ", L, + ".\n GP approximation not valid for this input." + ) + stop(msg) + } +} + +# Stan data for HSGP approximation +create_standata_hsgp <- function(suffix, conf, dat_x_unit, x_name, term_name) { + checkmate::assert_number(conf$scale_bf, lower = 0) + checkmate::assert_integerish(conf$num_bf, lower = 1) + L <- conf$scale_bf + ensure_gp_approx_is_valid(dat_x_unit, x_name, L, term_name) + out <- list(L, conf$num_bf) + names(out) <- paste0(c("L_", "B_"), suffix) + out +} + +# Helper +stanlines_data_hsgp <- function(suffix) { + dn <- list( + L = paste0("L_", suffix), + B = paste0("B_", suffix) + ) + lines <- list( + paste0(" real ", dn$L, "; // domain size"), + paste0(" int ", dn$B, "; // num of basis funs") + ) + list( + lines = lines, + stannames = unlist(dn) + ) +} + +# Basis function multipliers +stancall_bf <- function(sfx) { + paste0( + " vector[B_", sfx, "] s_", sfx, + " = bf_eq_multips(alpha_", sfx, ", ell_", sfx, + ", seq_B_", sfx, ", L_", sfx, ");\n" + ) +} + +# Shared GP term +stancall_gp_shared <- function(datanames, sn, sfx) { + nn <- paste0("n_", datanames) + sfx_full <- paste0(sfx, "_", datanames) + paste0( + " vector[", nn, "] ", sn, + " = compute_f_shared(xi_", sfx, ", PHI_", sfx_full, ", s_", sfx, ");\n" + ) +} + +# Group-specific GP term +stancall_gp_group <- function(datanames, sn, sfx, zn) { + nn <- paste0("n_", datanames) + sfx_full <- paste0(sfx, "_", datanames) + paste0( + " vector[", nn, "] ", sn, + " = compute_f_group(xi_", sfx, ", PHI_", sfx_full, ", s_", sfx, + ", ", zn, ");\n" + ) +} diff --git a/R/terms-OffsetTerm.R b/R/terms-OffsetTerm.R new file mode 100644 index 0000000..cc1dec0 --- /dev/null +++ b/R/terms-OffsetTerm.R @@ -0,0 +1,154 @@ +# Used by FormulaParser +create_offsetterm <- function(covariates, hierarchy) { + z_name <- covariates[1] + h_name <- hierarchy + if (is.null(h_name)) { + out <- GroupedOffsetTerm$new(z_name) + } else { + out <- HierOffsetTerm$new(z_name = z_name, h_name = h_name) + } + out +} + + +# Define the OffsetTerm class +# Abstract class which should not be instantiated +OffsetTerm <- R6::R6Class("OffsetTerm", + inherit = FormulaTerm, + lock_class = TRUE, + private = list( + latex_type = "BAS" + ), + public = list( + prior_intercept = "student_t(4, 0, 5)", + initialize = function() { + private$suffix <- "0" + private$typename <- "baseline" + }, + param_name = function() { + ts <- private$suffix + paste0("offset_", ts) + }, + stancode_model = function() { + paste0(self$param_name(), " ~ ", self$prior_intercept, ";\n") + } + ) +) + +# Define the grouped OffsetTerm class +GroupedOffsetTerm <- R6::R6Class("GroupedOffsetTerm", + inherit = OffsetTerm, + lock_class = TRUE, + private = list( + latex_param_names = function() { + "\\mathbf{c}" + } + ), + public = list( + initialize = function(z_name) { + super$initialize() + checkmate::assert_character(z_name) + self$z_name <- z_name + private$suffix <- z_name + private$latex_param_subscript <- "0" + }, + stancode_data = function(used_names, datanames) { + z <- self$stanlines_data_z(datanames) + build_stancode_data(z$lines, z$stannames, used_names) + }, + stancode_pars = function() { + pn <- self$param_name() + gn <- paste0("G_", self$z_name) + paste0(" vector[", gn, "] ", pn, ";\n") + }, + stancode_tpars = function(datanames) { + tn <- self$stanname(datanames) + pn <- self$param_name() + dnz <- self$stanname_z(datanames) + code <- paste0( + " vector[n_", datanames, "] ", tn, " = ", pn, "[", dnz, "];" + ) + paste(code, collapse = "\n") + }, + standata = function(datasets, conf) { + conf <- self$ensure_conf(conf) + self$create_standata_z(datasets) + } + ) +) + + +# Define the grouped OffsetTerm class +HierOffsetTerm <- R6::R6Class("HierOffsetTerm", + inherit = OffsetTerm, + lock_class = TRUE, + private = list( + latex_param_names = function() { + "\\mathbf{c}" + } + ), + public = list( + prior_log_z = "std_normal()", + prior_log_mu = "std_normal()", + prior_log_sigma = "normal(0, 5)", + initialize = function(z_name, h_name) { + super$initialize() + checkmate::assert_character(z_name) + checkmate::assert_character(h_name) + self$z_name <- z_name + self$h_name <- h_name + private$suffix <- z_name + private$latex_param_subscript <- "0" + }, + stancode_data = function(used_names, datanames) { + z <- self$stanlines_data_z(datanames) + h <- self$stanlines_data_h(datanames) + lines <- c(z$lines, h$lines) + sn <- c(z$stannames, h$stannames) + build_stancode_data(lines, sn, used_names) + }, + stancode_pars = function() { + gz <- paste0("G_", self$z_name) + gh <- paste0("G_", self$h_name) + raw_params_stancode(self$param_name(), gh, gz) + }, + stancode_tpars = function(datanames) { + zn <- self$stanname_z(datanames) + hn <- self$stanname_h(datanames) + pn <- self$param_name() + fc <- stancall_offset_grouped(pn, hn, zn) + sn <- self$stanname(datanames) + code <- paste0( + " vector[n_", datanames, "] ", sn, " = ", fc, ";" + ) + paste(code, collapse = "\n") + }, + stancode_model = function() { + prior <- list( + log_z = self$prior_log_z, + log_mu = self$prior_log_mu, + log_sigma = self$prior_log_sigma + ) + hier_prior_stancode(self$param_name(), prior) + }, + standata = function(datasets, conf) { + conf <- self$ensure_conf(conf) + sd_z <- self$create_standata_z(datasets) + sd_h <- self$create_standata_h(datasets) + c(sd_z, sd_h) + } + ) +) + +# Parameter to natural scale (in transformed parameters) +stancall_offset_grouped <- function(parname, h_name, z_name) { + codes <- c() + J <- length(h_name) + for (j in seq_len(J)) { + ofs <- param_vecs_log_hier(parname, h_name[j], z_name[j]) + in_par <- paste(ofs, collapse = ", ") + code_j <- paste0("to_log_natural_scale(", in_par, ")") + codes <- c(codes, code_j) + } + codes +} diff --git a/R/terms-SFTerm.R b/R/terms-SFTerm.R new file mode 100644 index 0000000..121da9b --- /dev/null +++ b/R/terms-SFTerm.R @@ -0,0 +1,252 @@ +# Used by FormulaParser +create_sfterm <- function(covariates, hierarchy) { + x_name <- covariates[1] + z_name <- covariates[2] + h_name <- hierarchy + if (is.na(z_name)) { + out <- SFTerm$new(x_name = x_name) + } else { + formula_def <- c( + paste0("kg ~ offset(", z_name, "_kg)"), + paste0("ks ~ offset(", z_name, "_ks)") + ) + if (!is.null(h_name)) { + formula_def <- c( + paste0("kg ~ offset(", z_name, "_kg | ", h_name, "_kg)"), + paste0("ks ~ offset(", z_name, "_ks | ", h_name, "_ks)") + ) + } + out <- create_sffterm(covariates, formula_def) + } + out +} + +# Used by FormulaParser +create_sffterm <- function(covariates, formula_def) { + if (length(formula_def) != 2) { + stop("error parsing formula definition for sf parameters") + } + x_name <- covariates[1] + for (f in formula_def) { + ff <- as.formula(f) + y_var <- FormulaParser$new(ff)$get_y_name() + if (y_var == "ks") { + form_ks <- ff + } else if (y_var == "kg") { + form_kg <- ff + } else { + stop("sff formula must have either kg or ks on the RHS") + } + } + FormulaSFTerm$new( + x_name = x_name, formula_kg = form_kg, formula_ks = form_ks + ) +} + +# Define the SFTerm class +SFTerm <- R6::R6Class("SFTerm", + inherit = FormulaTerm, + lock_class = TRUE, + private = list( + latex_type = "log-SF", + latex_param_names = function() { + c("k_{g}", "k_{s}") + }, + stanfiles_functions_impl = function() { + c("sf") + } + ), + public = list( + prior_kg = "student_t(4, 0, 1)", + prior_ks = "student_t(4, 0, 1)", + x_transform = MaxScaleTransform$new(), + initialize = function(x_name) { + checkmate::assert_character(x_name, any.missing = FALSE) + if (inherits(self, "FormulaSFTerm")) { + private$typename <- "log_sff" + } else { + private$typename <- "log_sf" + } + private$suffix <- x_name + self$x_name <- x_name + }, + stancode_data = function(used_names, datanames) { + x <- self$stanlines_data_x(datanames) + build_stancode_data(x$lines, x$stannames, used_names) + }, + stancode_pars = function() { + scb <- stancode_bounds(lower = 0, upper = NULL) + line1 <- paste0(" real", scb, " kg", "; // growth rate") + line2 <- paste0(" real", scb, " ks", "; // shrinkage rate") + paste0(line1, "\n", line2, "\n") + }, + stancode_tpars = function(datanames) { + sn <- self$stanname(datanames) + sx <- self$stanname_x(datanames) + nn <- paste0("n_", datanames) + fc <- paste0("log_sf_norm(", sx, ", kg, ks)") + c1 <- paste0(" vector[", nn, "] ", sn, " = ", fc, ";") + paste(c1, collapse = "\n") + }, + stancode_model = function() { + line1 <- paste0(" kg ~ ", self$prior_kg, ";") + line2 <- paste0(" ks ~ ", self$prior_ks, ";") + paste(line1, line2, sep = "\n") + }, + standata = function(datasets, conf) { + conf <- self$ensure_conf(conf) + self$create_standata_x(datasets) + } + ) +) + +# Define the FormulaSFTerm class +FormulaSFTerm <- R6::R6Class("FormulaSFTerm", + inherit = SFTerm, + lock_class = TRUE, + private = list( + latex_param_names = function() { + c("\\mathbf{k}_{g}", "\\mathbf{k}_{s}") + }, + stanfiles_functions_impl = function() { + f1 <- self$term_list_kg$stanfiles_functions() + f2 <- self$term_list_ks$stanfiles_functions() + c("sf", f1, f2) + } + ), + public = list( + term_list_kg = NULL, + term_list_ks = NULL, + prior_log_C_kg = "student_t(4, -2, 4)", + prior_log_C_ks = "student_t(4, 0, 10)", + prior_kg = NULL, + prior_ks = NULL, + initialize = function(x_name, formula_kg, formula_ks) { + self$term_list_kg <- create_termlist(formula_kg, self$prior_kg) + self$term_list_ks <- create_termlist(formula_ks, self$prior_ks) + self$term_list_kg$fsum_name <- "log_kg" + self$term_list_ks$fsum_name <- "log_ks" + super$initialize(x_name) + }, + input_vars = function() { + v1 <- super$input_vars() + vars_ks <- as.vector(self$term_list_ks$input_vars()) + vars_kg <- as.vector(self$term_list_kg$input_vars()) + vars_no_ks <- replicate_without_suffix(vars_ks, "ks") + vars_no_kg <- replicate_without_suffix(vars_kg, "kg") + unique(c(v1, vars_ks, vars_kg, vars_no_kg, vars_no_ks)) + }, + stancode_data = function(used_names, datanames) { + x <- self$stanlines_data_x(datanames) + lines <- c(x$lines) + dn <- c(x$stannames) + out <- build_stancode_data(lines, dn, used_names) + sc1 <- self$term_list_kg$stancode_data(datanames) + sc2 <- self$term_list_ks$stancode_data(datanames) + list( + code = paste(out$code, sc1, sc2, sep = "\n"), + stannames = out$stannames + ) + }, + stancode_tdata = function(datanames) { + paste( + self$term_list_kg$stancode_tdata(datanames), + self$term_list_ks$stancode_tdata(datanames), + sep = "\n" + ) + }, + stancode_pars = function() { + code_C <- paste0(" real log_C_kg;\n real log_C_ks;") + paste( + self$term_list_kg$stancode_pars(), + self$term_list_ks$stancode_pars(), + code_C, + sep = "\n" + ) + }, + stancode_tpars = function(datanames) { + code <- paste( + self$term_list_kg$stancode_tpars(datanames), + self$term_list_ks$stancode_tpars(datanames), + sep = "\n" + ) + head <- " // FormulaSFTerm" + l1 <- par_to_nat_scale(self$term_list_kg$fsum_name, "kg", datanames) + l2 <- par_to_nat_scale(self$term_list_ks$fsum_name, "ks", datanames) + sn <- self$stanname(datanames) + xn <- self$stanname_x(datanames) + l3 <- paste0(" vector[n_", datanames, "] ", sn, " = ", + "log_sf_norm_vec(", xn, ", kg_", datanames, ", ks_", + datanames, ");", + collapse = "\n" + ) + paste(code, head, l1, l2, l3, sep = "\n") + }, + stancode_model = function() { + code1 <- self$term_list_kg$stancode_model() + code2 <- self$term_list_ks$stancode_model() + code3 <- paste0(" log_C_kg ~ ", self$prior_log_C_kg, ";") + code4 <- paste0(" log_C_ks ~ ", self$prior_log_C_ks, ";") + paste0(code1, code2, code3, code4) + }, + stancode_gq = function() { + code1 <- self$term_list_kg$stancode_gq() + code2 <- self$term_list_ks$stancode_gq() + code3 <- super$stancode_gq() + code4 <- paste0("\n // SF parameters") + code5 <- paste0(" vector[n_LON] kg = kg_LON;") + code6 <- paste0(" vector[n_LON] ks = ks_LON;") + paste(code1, code2, code3, code4, code5, code6, sep = "\n") + }, + standata = function(datasets, conf) { + if (length(datasets) != 1) { + stop("standata() for multiple datasets not implemntd for FormulaSFTerm") + } + data <- datasets[[1]] + dataname <- names(datasets)[1] + conf <- self$ensure_conf(conf) + sd_x <- self$create_standata_x(datasets) + sd_1 <- self$term_list_kg$create_standata(data, dataname, conf$kg) + sd_2 <- self$term_list_ks$create_standata(data, dataname, conf$ks) + c(sd_x, sd_1, sd_2) + } + ) +) + +# Stan code for raw parameters +raw_params_stancode <- function(param_name, G_hier, G_id) { + line1 <- paste0(" vector[", G_hier, "] log_mu_", param_name, ";") + line2 <- paste0(" vector[", G_hier, "] log_sigma_", param_name, ";") + line3 <- paste0(" vector[", G_id, "] log_z_", param_name, ";") + paste(line1, line2, line3, "\n", sep = "\n") +} + +# Hierarchical parameters to vectors with length equal to data +param_vecs_log_hier <- function(param_name, h_name, z_name) { + pn_z <- param_name + list( + z = paste0("log_z_", pn_z, "[", z_name, "]"), + mu = paste0("log_mu_", param_name, "[", h_name, "]"), + sigma = paste0("log_sigma_", param_name, "[", h_name, "]") + ) +} + +# Hierarchical prior on log scale +hier_prior_stancode <- function(param_name, prior) { + z <- paste0("log_z_", param_name) + m <- paste0("log_mu_", param_name) + s <- paste0("log_sigma_", param_name) + line1 <- paste0(" ", z, " ~ ", prior$log_z, ";") + line2 <- paste0(" ", m, " ~ ", prior$log_mu, ";") + line3 <- paste0(" ", s, " ~ ", prior$log_sigma, ";") + paste(line1, line2, line3, "\n", sep = "\n") +} + +# kg/ks parameter on natural scale +par_to_nat_scale <- function(f_sum_name, pn, datanames) { + paste0(" vector[n_", datanames, "] ", pn, "_", + datanames, " = exp(log_C_", pn, " + ", + f_sum_name, "_", datanames, ");", + collapse = "\n" + ) +} diff --git a/R/terms-TermList.R b/R/terms-TermList.R new file mode 100644 index 0000000..3ca5daa --- /dev/null +++ b/R/terms-TermList.R @@ -0,0 +1,347 @@ +#' Create additive terms +#' +#' @param formula the model formula +#' @param priors Prior configurations. +#' @return An object of \code{R6} class \code{TermList}. +create_termlist <- function(formula, priors) { + checkmate::assert_class(formula, "formula") + fp <- FormulaParser$new(formula) + terms <- fp$parse_terms() + list <- TermList$new(terms) + pr_names <- names(priors) + for (pr in pr_names) { + t <- list$get_term(pr) + message("Editing term ", hl_string(pr)) + prior <- priors[[pr]] + checkmate::assert_list(prior) + for (nam in names(prior)) { + message(" * Editing field ", hl_string(nam)) + t[[nam]] <- prior[[nam]] + } + } + list +} + +#' Additive terms (R6 class) +#' +#' @export +#' @field terms A list of \code{FormulaTerm} objects, defining the +#' function components. +#' @field fsum_name Base name of the sum of the terms variable in 'Stan' code. +#' @description A semi-parametric additive model (R6 class). +TermList <- R6::R6Class("TermList", + inherit = StanCodeCreator, + + # PRIVATE + private = list( + stan_model = NULL, + finalize_code = function(code) { + if (nchar(trimws(code)) == 0) { + return("") + } + checkmate::assert_character(code, len = 1) + paste0( + "\n // ", class_name(self), " for ", self$fsum_name, "\n", + code, "\n" + ) + }, + stanfiles_functions_impl = function() { + gtr <- function(x) x$stanfiles_functions() + unlist(sapply(self$terms, gtr)) + }, + stancode_data_impl = function(datanames) { + stancode_termlist_data(self$terms, datanames)$code + }, + stancode_tdata_impl = function(datanames) { + f <- function(x) x$finalize_code(x$stancode_tdata(datanames)) + paste(sapply(self$terms, f), collapse = "\n") + }, + stancode_pars_impl = function() { + f <- function(x) x$finalize_code(x$stancode_pars()) + paste(sapply(self$terms, f), collapse = "\n") + }, + stancode_tpars_impl = function(datanames) { + # Terms + f1 <- function(x) x$finalize_code(x$stancode_tpars(datanames)) + sc_tpars <- paste(sapply(self$terms, f1), collapse = "\n") + + # Sum + sc_sum <- stancode_f_sum(datanames, self$terms, self$fsum_name) + paste(sc_tpars, sc_sum, sep = "\n") + }, + stancode_model_impl = function() { + f <- function(x) x$finalize_code(x$stancode_model()) + paste(sapply(self$terms, f), collapse = "\n") + }, + stancode_gq_impl = function() { + dataname <- "LON" + fsum_name <- self$fsum_name + nn <- paste0("n_", dataname) + f <- function(x) x$finalize_code(x$stancode_gq()) + sc_gq <- paste(sapply(self$terms, f), collapse = "\n") + paste0( + sc_gq, "\n // Sum of terms\n vector[", nn, "] ", fsum_name, + " = ", fsum_name, "_", dataname, ";\n" + ) + } + ), + + # PUBLIC + public = list( + terms = NULL, + fsum_name = "f_sum", + + #' @description + #' Create term list + #' + #' @param terms The list of model terms. + initialize = function(terms) { + checkmate::assert_class(terms, "list") + self$terms <- terms + }, + + #' @description + #' The term list as a string + string = function() { + gn <- function(x) x$stanname_base() + paste0(sapply(self$terms, gn), collapse = " + ") + }, + + #' @description + #' Get names of all input variables + input_vars = function() { + getter <- function(x) x$input_vars() + inputs_list <- sapply(self$terms, getter) + vars <- unique(unlist(inputs_list)) + }, + + #' @description List names of all terms in Stan code. + stan_names = function() { + gtr <- function(x) x$stanname_base() + out <- sapply(self$terms, gtr) + names(out) <- NULL + out + }, + + #' @description Get term object based on term name in Stan' + #' @param f_name_stan Name of term in 'Stan'. Valid names + #' can be checked using the `$stan_names()` method. + get_term = function(f_name_stan) { + sn <- self$stan_names() + idx <- find_term_index(f_name_stan, sn, TRUE) + if (length(idx) != 1) { + stop("Internal error. Please report a bug.") + } + self$terms[[idx]] + }, + + #' @description + #' Latex code, mathematical notation for the terms + latex = function() { + J <- length(self$terms) + fs <- paste0("f^{(", seq_len(J), ")}(\\mathbf{x})") + math <- sapply(self$terms, function(x) x$latex()) + latex_terms <- paste0(fs, " &= ", math) + paste( + "\\begin{align}\n ", + paste(latex_terms, collapse = "\\\\ \n "), + "\\end{align}", + sep = "\n" + ) + }, + + #' @description List length + length = function() { + length(self$terms) + }, + + #' @description + #' Create the 'Stan' data list from a data frame. Performs transforms + #' on continuous variables that are input to GP terms. + #' @param data A data frame. + #' @param dataname Name of data set. + #' @param term_confs A list that specifies configuration of model terms. + #' If name of any term is not found from the list, \code{$default_conf()} + #' of that \code{FormulaTerm} is used. + #' @return A list. + create_standata = function(data, dataname, term_confs = NULL) { + term_names <- names(self$terms) + sd <- list(nrow(data)) + names(sd) <- paste0("n_", dataname) # dimension + datasets <- list(data) + names(datasets) <- dataname + for (tn in term_names) { + sd_term <- self$terms[[tn]]$standata(datasets, term_confs[[tn]]) + sd <- c(sd, sd_term) + } + sd[unique(names(sd))] # remove duplicates + }, + + #' @description + #' Get term configuration of all terms, some of which can be given by user. + #' + #' @param term_confs A list that specifies configuration of model terms. + #' If name of any term is not found from the list, \code{$default_conf()} + #' of that \code{FormulaTerm} is used. + #' @param num_bf If not \code{NULL}, configurations of all + #' \code{GPTerm}s are updated with this value. + #' @param scale_bf If not \code{NULL}, configurations of all + #' \code{GPTerm}s are updated with this value. + #' @return The updated model object (invisibly). + fill_term_confs = function(term_confs = NULL, + num_bf = NULL, scale_bf = NULL) { + stannames <- names(self$terms) + confs <- list() + j <- 0 + for (tn in stannames) { + j <- j + 1 + term <- self$terms[[tn]] + tc <- term$ensure_conf(term_confs[[tn]]) + if (is(term, "FormulaSFTerm")) { + tc$kg <- term$term_list_kg$fill_term_confs(tc, num_bf, scale_bf) + tc$ks <- term$term_list_ks$fill_term_confs(tc, num_bf, scale_bf) + } + if (is(term, "GPTerm")) { + tc <- fill_term_conf_gpterm(tc, num_bf, scale_bf) + } + confs[[j]] <- tc + } + names(confs) <- stannames + confs + }, + + #' @description + #' Set variable transforms using data. + #' + #' @param data A data frame. + #' @param names_skip Names of terms whose input transform should be + #' skipped. + set_transforms = function(data, names_skip = NULL) { + settable <- c("Normalizing", "MaxScale", "UnitScale") + settable <- paste0(settable, "Transform") + ts <- self$terms + for (tn in names(ts)) { + term <- ts[[tn]] + transf <- term$x_transform + msg <- paste0("Setting transform for ", hl_string(tn), "... ") + if (tn %in% names_skip) { + msg <- paste0(msg, "skipped!") + } else { + if (inherits(transf, settable)) { + x_data <- get_x_from_data(data, term$x_name) + term$x_transform <- transf$set_using_data(x_data) + transf_name <- class(transf)[1] + msg <- paste0(msg, "done (", hl_string(transf_name), ")!") + } else { + msg <- paste0(msg, "not needed.") + } + } + message(msg) + + # Handle FormulaSFTerm + if (inherits(term, "FormulaSFTerm")) { + message("Setting transforms for the terms inside of FormulaSFTerm") + term$term_list_kg$set_transforms(data, names_skip) + term$term_list_ks$set_transforms(data, names_skip) + } + } + }, + + + #' @description + #' Check how data transforms using the current model transforms. + #' + #' @param data A data frame. + check_transforms = function(data) { + settable <- c("Normalizing", "MaxScale", "UnitScale") + settable <- paste0(settable, "Transform") + ts <- self$terms + for (tn in names(ts)) { + term <- ts[[tn]] + transf <- term$x_transform + msg <- paste0("Checking transform for ", hl_string(tn), "... ") + if (inherits(transf, settable)) { + x_data <- get_x_from_data(data, term$x_name) + r1 <- range(x_data) + r2 <- range(transf$forward(x_data)) + transf_name <- class(transf)[1] + msg <- paste0(msg, "(", hl_string(transf_name), ")!") + message(msg) + message("Data range is [", r1[1], ", ", r1[2], "]") + message("Transformed range is [", r2[1], ", ", r2[2], "]") + } else { + msg <- paste0(msg, "no transform.") + message(msg) + } + } + } + ) +) + +# Term configuration of GPTerm +fill_term_conf_gpterm <- function(tc, num_bf, scale_bf) { + if (!is.null(num_bf)) { + tc$num_bf <- num_bf + } + if (!is.null(scale_bf)) { + tc$scale_bf <- scale_bf + } + tc +} + +# Find index of model term based on Stan variable name +find_term_index <- function(f_name_stan, term_names_stan, strict) { + sn <- term_names_stan + tn_str <- paste(sn, collapse = ", ") + if (!(f_name_stan %in% sn) && strict) { + msg <- paste0( + hl_string(f_name_stan), " is not a Stan variable name of any", + " term in this model. Valid names are {", hl_string(tn_str), "}." + ) + stop(msg) + } + which(sn == f_name_stan) +} + + +# Generate data block +stancode_termlist_data <- function(terms, datanames, code = NULL, + used_data_names = NULL) { + for (t in terms) { + scd <- t$stancode_data(used_names = used_data_names, datanames) + used_data_names <- unique(c(used_data_names, scd$stannames)) + code_add <- t$finalize_code(scd$code) + code <- paste(code, code_add, sep = "\n") + } + list(code = code, used_names = used_data_names) +} + +# Helper +stancode_f_sum <- function(datanames, terms, fsum_name) { + fun <- function(x) x$stanname(datanames) + f_list <- lapply(terms, fun) # gives list with length = num terms + J <- length(datanames) + code <- " // Sum the terms" + for (j in seq_len(J)) { + nn <- paste0("n_", datanames[j]) + gtr <- function(x) x[j] + f_names <- paste(lapply(f_list, gtr), collapse = " + ") + code_j <- paste0( + " vector[", nn, "] ", fsum_name, "_", datanames[j], " = ", f_names, ";" + ) + code <- paste(code, code_j, sep = "\n") + } + code +} + + +# Get sff term of a model +get_sff_term <- function(model) { + terms <- model$term_list$terms + cl <- sapply(terms, function(x) class(x)[1] == "FormulaSFTerm") + idx <- which(cl) + if (length(idx) != 1) { + stop("The model should have one FormulaSFTerm") + } + terms[[which(cl)]] +} diff --git a/R/utils-data.R b/R/utils-data.R new file mode 100644 index 0000000..64c85ec --- /dev/null +++ b/R/utils-data.R @@ -0,0 +1,225 @@ +#' Extend data frame +#' +#' @export +#' @param df original data frame +#' @param t vector of new time values +#' @param time_var name of time variable +#' @return new data frame +extend_df <- function(df, t, time_var) { + u <- df_unique_factor_rows(df) + df <- df_replicate_rows(u, length(t)) + df[[time_var]] <- rep(t, nrow(u)) + df +} + +#' Extend data frame and add a constant continuous variable +#' +#' @export +#' @param df original data frame +#' @param t vector of new time values +#' @param time_var name of time variable +#' @param x value of other continuous variable (will be constant) +#' @param x_var name of other continuous variable +#' @param id_var name of id variable +#' @return new data frame +extend_df2 <- function(df, t, time_var, x, x_var, id_var) { + checkmate::assert_number(x) + test_dat <- df + t_test <- t + N <- length(t_test) + test_dat[[x_var]] <- x + split_data <- split(test_dat, test_dat[[id_var]]) + first_rows <- lapply(split_data, function(x) x[1, ]) + test_dat <- do.call(rbind, first_rows) + R <- nrow(test_dat) + test_dat <- test_dat[rep(1:R, each = N), ] + test_dat[[time_var]] <- rep(t_test, R) + test_dat +} + +#' Add input for 'FormulaSFTerm' +#' +#' @description +#' Duplicates original data columns, adding new columns with the \code{_kg} and +#' \code{_ks} suffixes if these are missing. +#' +#' @export +#' @param df the data frame +#' @param model an object of class \code{\link{TSModel}} +#' @param debug print debugging messages? +add_sff_input <- function(df, model, debug = FALSE) { + t <- get_sff_term(model) + vars_ks <- as.vector(t$term_list_ks$input_vars()) + vars_kg <- as.vector(t$term_list_kg$input_vars()) + vars <- c(vars_kg, vars_ks) + for (v in vars) { + if (v %in% colnames(df)) { + message("column ", hl_string(v), " already exists in the data frame") + } else { + parts <- strsplit(v, split = "_", fixed = TRUE)[[1]] + v_base <- paste(parts[1:(length(parts) - 1)], sep = "_") + new_col <- df[[v_base]] + if (is.null(new_col)) { + stop(hl_string(v_base), " not found in the data frame") + } + msg <- paste0( + "copying column ", hl_string(v_base), " to new column ", + hl_string(v) + ) + if (debug) { + message(msg) + } + df[[v]] <- new_col + } + } + df +} + +#' Create a dense grid until event time for each subject (for 'JointModel') +#' +#' @export +#' @param df_tte time-to-event data frame +#' @param df_lon the longitudinal data frame +#' @param id_var id variable +#' @param time_var time variable +#' @param num_points number of grid points +#' @param even_spacing space grid points evenly? +create_jm_grid <- function(df_tte, df_lon, num_points = 30, id_var = "id", + time_var = "t", even_spacing = FALSE) { + t_min <- 1e-6 # because hazard might not be defined at t = 0 + id <- df_tte[[id_var]] + time <- df_tte[[time_var]] + checkmate::assert_factor(id) + checkmate::assert_numeric(time) + ids <- as.numeric(levels(id)) + df_grid <- NULL + t_max <- max(time) + if (even_spacing) { + t_seq <- seq(0, t_max, length.out = num_points) + } else { + t_seq <- exp(seq(0, 3, length.out = num_points)) - 1 + t_seq <- t_max / max(t_seq) * t_seq + t_seq[t_seq <= t_min] <- t_min + } + for (lev in ids) { + idx_tte <- which(id == lev) + idx_tte <- idx_tte[length(idx_tte)] + idx_lon <- which(df_lon[[id_var]] == lev)[1] + row_tte <- df_tte[idx_tte, , drop = FALSE] # take last row of the subject + row_lon <- df_lon[idx_lon, , drop = FALSE] # take first row of the subject + row_lon[[time_var]] <- NULL # remove old time variable + row <- cbind(row_tte, row_lon) + df_j <- row[rep(1, num_points), ] # repeat row max_num_points times + df_j[[time_var]] <- t_seq + df_grid <- rbind(df_grid, df_j) + } + df_grid +} + + + +# Union of data frames +df_union <- function(x, y) { + checkmate::assert_class(x, "data.frame") + checkmate::assert_class(y, "data.frame") + checkmate::assert_true(nrow(x) == nrow(y)) + common_names <- intersect(names(x), names(y)) + for (name in common_names) { + if (!all(x[[name]] == y[[name]], na.rm = TRUE)) { + stop(paste0("Columns '", name, "' are not identical!")) + } + } + rem <- !(names(y) %in% common_names) + rem_names <- colnames(y)[rem] + all_names <- c(colnames(x), rem_names) + combined_df <- cbind(x, y[, rem]) + colnames(combined_df) <- all_names + return(combined_df) +} + +# Indices of data frame columns that are factors +df_factor_cols <- function(df) { + which(sapply(df, inherits, "factor")) +} + +# Only unique rows of the factors of a data frame +df_unique_factor_rows <- function(df) { + col_inds <- df_factor_cols(df) + if (is.null(col_inds)) { + stop("no factor columns found in data frame") + } + df <- unique(df[, col_inds, drop = FALSE]) + rownames(df) <- NULL + df +} + +# Replicate df rows +df_replicate_rows <- function(df, n) { + return(df[rep(seq_len(nrow(df)), each = n), , drop = FALSE]) +} + +# Base R data frame row filter +df_filter_rows <- function(df, filter_by = NULL, kept_vals = NULL) { + if (!is.null(filter_by)) { + rows_keep <- which(df[[filter_by]] %in% kept_vals) + df <- df[rows_keep, , drop = FALSE] + } + df +} + +# Sample N data rows from each group +sample_rows_by_group <- function(data, N = 1, group_var = "arm") { + checkmate::assert_integerish(N, lower = 1) + group_fac <- data[[group_var]] + checkmate::assert_factor(group_fac) + levs <- unique(levels(group_fac)) + new_dat <- NULL + for (lev in levs) { + inds <- which(group_fac == lev) + new_rows <- pick_random_rows(data[inds, , drop = F], size = N) + new_dat <- rbind(new_dat, new_rows) + } + new_dat +} + + +# Pick random row of a data frame +pick_random_rows <- function(df, size = 1) { + idx <- sample(nrow(df), size = size, replace = FALSE) + df[idx, , drop = FALSE] +} + +# automatic selection of prediction time points +t_pred_auto <- function(t_pred = NULL, t_data = NULL) { + if (!is.null(t_pred)) { + if (length(t_pred) == 1) { + if (t_pred == "auto") { + t_max <- min(2 * 365, max(t_data)) + message( + "t_pred was 'auto', setting it to 10 evenly spaced points with", + " t_max = ", t_max + ) + t_pred <- seq(0, t_max, length.out = 10) + } else { + stop("invalid t_pred argument") + } + } else { + checkmate::assert_numeric(t_pred, min.len = 2) + } + } else { + message("t_pred was NULL, setting it based on data") + t_range <- range(t_data) + t_min <- min(0, t_range[1]) + t_max <- 1.1 * t_range[2] + t_pred <- seq(t_min, t_max, length.out = 40) + } + t_pred +} + +# Finalizer +finalize_stan_data <- function(stan_data, prior_only) { + stan_data <- stan_data[unique(names(stan_data))] # remove duplicates + checkmate::assert_logical(prior_only) + stan_data$prior_only <- as.numeric(prior_only) + stan_data +} diff --git a/R/utils-misc.R b/R/utils-misc.R new file mode 100644 index 0000000..2902890 --- /dev/null +++ b/R/utils-misc.R @@ -0,0 +1,23 @@ +#' Name of a term in the formula syntax to its Stan code name +#' +#' @export +#' @param term A string. +term_to_code <- function(term) { + checkmate::assert_character(term) + form <- as.formula(paste0("y~", term)) + a <- create_termlist(form, NULL) + a$terms[[1]]$stanname_base() +} + +#' Create a 'Stan' model with only given functions in it +#' +#' @param stan_file Path to the Stan file relative to the directory +#' \code{inst/functions}, without the \code{.stan} suffix. +#' @param ... Optional arguments to \code{cmdstanr::cmdstan_model()}. +stanmodel_functions <- function(stan_file, ...) { + checkmate::assert_character(stan_file) + sc <- read_stan_function(stan_file) + sc <- paste("functions {", sc, "}", sep = "\n") + sf <- cmdstanr::write_stan_file(sc) + cmdstanr::cmdstan_model(sf, ...) +} diff --git a/R/utils-pipe.R b/R/utils-pipe.R new file mode 100644 index 0000000..fd0b1d1 --- /dev/null +++ b/R/utils-pipe.R @@ -0,0 +1,14 @@ +#' Pipe operator +#' +#' See \code{magrittr::\link[magrittr:pipe]{\%>\%}} for details. +#' +#' @name %>% +#' @rdname pipe +#' @keywords internal +#' @export +#' @importFrom magrittr %>% +#' @usage lhs \%>\% rhs +#' @param lhs A value or the magrittr placeholder. +#' @param rhs A function call using the magrittr semantics. +#' @return The result of calling `rhs(lhs)`. +NULL diff --git a/R/utils-rvar.R b/R/utils-rvar.R new file mode 100644 index 0000000..7da89aa --- /dev/null +++ b/R/utils-rvar.R @@ -0,0 +1,34 @@ +# Create a new rvar with same shape as rv +rvar_set <- function(rv, value) { + dims <- dim(posterior::as_draws_array(rv)) + posterior::rvar(x = array(value, dim = dims), dim = dim(rv)) +} + +# Create a new rvar with draws from standard normal, same shape as rv +rvar_std_normal <- function(rv) { + dims <- dim(posterior::as_draws_array(rv)) + x <- stats::rnorm(n = prod(dims)) + posterior::rvar(x = array(x, dim = dims), dim = dim(rv)) +} + +#' Reset values of given parameters in 'rvars' list to a constant value +#' +#' @export +#' @param rvars An object of class \code{draws_rvars}. +#' @param names A character vector of parameter names. +#' @param value Value for the parameters. +#' @return An updated object of class \code{draws_rvars}. +new_draws <- function(rvars, names, value = 0) { + checkmate::assert_character(names) + checkmate::assert_class(rvars, "draws_rvars") + checkmate::assert_number(value) + for (name in names) { + rv_orig <- rvars[[name]] + if (is.null(rv_orig)) { + stop("invalid parameter name '", name, "'") + } + message("setting ", hl_string(name), " to ", value) + rvars[[name]] <- rvar_set(rv_orig, value) + } + rvars +} diff --git a/R/utils-stancode.R b/R/utils-stancode.R new file mode 100644 index 0000000..3b935e0 --- /dev/null +++ b/R/utils-stancode.R @@ -0,0 +1,110 @@ +# Autoformat a 'Stan' code string +autoformat_stancode <- function(code) { + tryCatch( + { + file <- cmdstanr::write_stan_file(code) + model <- cmdstanr::cmdstan_model(file, compile = FALSE) + res <- processx::run( + file.path(cmdstanr::cmdstan_path(), "bin", "stanc"), + args = c(model$stan_file(), "--auto-format") + ) + return(res$stdout) + }, + error = function(e) { + stop(e$stderr) + } + ) +} + +# Read stan code from inst/functions dir +read_stan_function <- function(name) { + checkmate::assert_character(name, min.chars = 1) + fname <- file.path("functions", paste0(name, ".stan")) + filepath <- system.file(fname, package = "sfgp") + code <- read_file_lines(filepath) + paste0(" // --------------- ", name, " ---------------\n", code, "\n") +} + +# Read lines from file +read_file_lines <- function(file) { + a <- readLines(file) + paste(a, collapse = "\n") +} + +# Bounds +stancode_bounds <- function(lower = NULL, upper = NULL) { + if (is.null(lower) && is.null(upper)) { + return("") + } + if (is.null(lower) && !is.null(upper)) { + return(paste0("")) + } + if (!is.null(lower) && is.null(upper)) { + return(paste0("")) + } + if (!is.null(lower) && !is.null(upper)) { + return(paste0("")) + } +} + +# Likelihood in model block +stancode_ts_likelihood <- function(stanname_y, dataname) { + code <- " // TS model likelihood\n" + paste0( + code, " real log_lik_lon = normal_lpdf(", stanname_y, "_log | f_sum_", + dataname, ", sigma);" + ) +} + +# Log likelihood in generated quantities +stancode_ts_gq <- function(mod, stanname_y, sc_terms_gq) { + cap_val <- mod$log_y_cap + y_var <- mod$y_var + def_ll <- paste0(" vector[n_LON] log_lik;") + sylp <- paste0(y_var, "_log_pred") + def_yp <- paste0(" vector[n_LON] ", sylp, ";") + def_fcap <- paste0( + " vector[n_LON] f_sum_capped = fmin(f_sum, ", cap_val, ");" + ) + def_ycap <- paste0(" vector[n_LON] ", y_var, "_log_pred_capped;") + line_yp <- paste0( + " ", sylp, "[i] = normal_rng(", "f_sum[i], sigma);" + ) + loop <- paste0(" for(i in 1:n_LON) {\n", line_yp, "\n }") + line_ycap <- paste0( + " ", y_var, "_log_pred_capped = fmin(", sylp, ", ", + cap_val, ");" + ) + paste(sc_terms_gq, "\n // Other generated quantities", + def_ll, def_yp, + def_fcap, def_ycap, loop, line_ycap, + sep = "\n" + ) +} + +# Data block +stancode_ts_data <- function(stanname_y, dataname) { + line0 <- " // Observation model" + line1 <- " real delta; // Offset for log transform of y" + y_decl <- paste0(" vector[n_", dataname, "] ") + line2 <- paste0(y_decl, stanname_y, "; // TS model observations") + paste(line0, line1, line2, sep = "\n") +} + +# Transformed data block +stancode_ts_tdata <- function(stanname_y, dataname) { + c1 <- paste0( + " vector[n_", dataname, "] ", stanname_y, "_log = log(", stanname_y, + " + delta); // log-scale TS observations\n" + ) + c2 <- paste0(" real y_loc = mean(", stanname_y, ");") + c3 <- paste0(" real y_scale = sd(", stanname_y, ");") + paste(c1, c2, c3, sep = "\n") +} + +# Stan code always in the model block +stancode_loglik <- function(model_suffixes) { + ll <- paste0("log_lik_", model_suffixes, collapse = " + ") + code <- "\n // Likelihood\n" + paste0(code, " if(prior_only == 0){\n target += ", ll, ";\n }") +} diff --git a/R/utils-string.R b/R/utils-string.R new file mode 100644 index 0000000..b224e58 --- /dev/null +++ b/R/utils-string.R @@ -0,0 +1,46 @@ +# Remove certain suffixes from strings +replicate_without_suffix <- function(strings, sfx) { + regex <- paste0("_", sfx, "$") + # Identify strings that end with suffix + has_suffix <- grepl(regex, strings) + + # Remove suffix and replicate the string if it has the suffix, else + # return original string + ifelse(has_suffix, gsub(regex, "", strings), strings) +} + +# Colorize string +colorize_string <- function(x, col) { + if (interactive()) { + x <- paste0(col, x, "\u001b[0m") + } + x +} + +# Number string +number_string <- function(x) { + col <- "\u001b[34;1m" # bold blue + colorize_string(x, col) +} + +# Highlight string +hl_string <- function(x) { + col <- "\u001b[33m" # orange + colorize_string(x, col) +} + +# Highlight class +hl_class <- function(x) { + col <- "\u001b[31m" # red + colorize_string(x, col) +} + +# Main class name +class_name <- function(x) { + class(x)[1] +} + +# Main class name +class_name_hl <- function(x) { + hl_class(class_name(x)) +} diff --git a/R/utils-trajectory_metrics.R b/R/utils-trajectory_metrics.R new file mode 100644 index 0000000..3af77d2 --- /dev/null +++ b/R/utils-trajectory_metrics.R @@ -0,0 +1,127 @@ +# Best response after baseline +best_response <- function(y) { + checkmate::assert_numeric(y, lower = 0) + checkmate::assert_vector(y, min.len = 2) + baseline <- y[1] + y_after_baseline <- y[2:length(y)] + A <- baseline + C <- baseline - min(y_after_baseline) + C / A +} + +# Time to 120% of baseline +ttb120 <- function(t, y) { + checkmate::assert_numeric(y, lower = 0) + inds <- which(y >= 1.2 * y[1]) + if (length(inds) == 0) { + return(max(t)) + } + t_idx <- min(inds) + t[t_idx] +} + +# Depth of response from a single trajectory +depth_of_response <- function(y) { + checkmate::assert_numeric(y, lower = 0) + baseline <- y[1] + A <- baseline + C <- baseline - min(y) + C / A +} + +# Duration +response_dur <- function(t, y) { + d <- duration_of_response(t, y) + d$end - d$start +} + +# Duration +response_end <- function(t, y) { + duration_of_response(t, y)$end +} + +# Duration +response_start <- function(t, y) { + duration_of_response(t, y)$start +} + +# Duration +response_exists <- function(t, y) { + duration_of_response(t, y)$response_exists +} + +# Duration +response_endures <- function(t, y) { + duration_of_response(t, y)$response_endures +} + +# Duration of response from a single trajectory +duration_of_response <- function(t, y) { + L <- length(t) + checkmate::assert_numeric(t, len = L) + checkmate::assert_numeric(y, len = L) + baseline <- y[1] + during_response <- 0 + response_start <- 0 + response_end <- 0 + response_exists <- FALSE + min_to_date <- baseline + for (it in seq_len(L)) { + # update min to date with each step + if (y[it] < min_to_date) { + min_to_date <- y[it] + } + # response starts at SLD <= 30% below baseline value + if (y[it] <= 0.7 * baseline) { + if (during_response == 0 && response_start == 0) { + response_start <- t[it] + response_exists <- TRUE + during_response <- 1 + } + } + if (during_response == 1) { + # response ends at SLD >= 20% above baseline value + if (y[it] >= 1.2 * min_to_date) { + response_end <- t[it] + during_response <- 0 + } + } + if (it == L) { + # trajectory ends with subject still in response + if (during_response == 1) { + response_end <- t[it] + } + } + } + + # return + list( + start = response_start, + end = response_end, + response_endures = as.logical(during_response), + response_exists = response_exists + ) +} + +# Plot trajectory and indicate response start and end +trajectory_plot <- function(t, y) { + dur <- duration_of_response(t, y) + TTB <- ttb120(t, y) + dor <- depth_of_response(y) + duration <- dur$end - dur$start + plt <- ggplot(data.frame(t = t, y = y), aes(t, y)) + + geom_line() + + geom_point() + + ggtitle(paste0( + "Depth = ", round(dor, 3), + ", duration = ", round(duration, 3), + ", resp. exists = ", dur$response_exists, + ", resp. endures = ", dur$response_endures, + ", TTB120 = ", round(TTB, 3) + )) + plt + + geom_hline(yintercept = y[1], color = "gray20", lty = 2) + + geom_vline(xintercept = dur$start, color = "steelblue3") + + geom_vline(xintercept = dur$end, color = "firebrick") + + geom_vline(xintercept = TTB, color = "orange") +} diff --git a/R/zzz.R b/R/zzz.R new file mode 100644 index 0000000..d900cde --- /dev/null +++ b/R/zzz.R @@ -0,0 +1,28 @@ +.onAttach <- function(...) { + msg <- create_startup_message() + packageStartupMessage(msg) +} + +# Create package startup message +create_startup_message <- function() { + v_pkg <- create_desc("sfgp") + msg <- paste0( + "Attached sfgp", v_pkg, + ". Type ?sfgp to get started." + ) + return(msg) +} + +# Create package description +create_desc <- function(pkg_name) { + Lib <- dirname(system.file(package = pkg_name)) + pkgdesc <- suppressWarnings( + utils::packageDescription(pkg_name, lib.loc = Lib) + ) + if (length(pkgdesc) > 1) { + out <- paste0(" ", pkgdesc$Version) + } else { + out <- "" + } + return(out) +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..8321a95 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# The 'sfgp' R package + +SF + GP modeling. + +## Authentication + +You need to set the `GITHUB_PAT` environment variable, which should be your GitHub [personal +access token](https://github.com/settings/tokens?type=beta). + +## Installation + +* Install `cmdstanr` following the instructions [here](https://mc-stan.org/cmdstanr/). +* Install `sfgp` using + +```r +remotes::install_github("generable/sfgp", ref = "main", build_vignettes = TRUE) +``` + +Building the vignettes can take rather long, so consider using `build_vignettes = FALSE`. + +## Getting started + + More info is in documentation, that you can view with +```r +library(sfgp) +?sfgp +``` diff --git a/data/example_data_jm.rda b/data/example_data_jm.rda new file mode 100644 index 0000000000000000000000000000000000000000..937556cec62b7da4feaf6a83573c4b9c44ec3775 GIT binary patch literal 3793 zcmajX`9Bj5z`*g18N(bym?P)b92MrCz8jj&3^{U_yUpDxDvjjGxjEvovbh+h4w`F6;F>9av~!${vJ;HSjxI^FJMo`x#f1xU*Vlq0d3hp$? z)v1Zb`v-+P&6Y65n-Cc)fmz5Z^gdGu0H6R_{m`mWSvW6_WrmNXv*2g|lovbtdbD4W zg+>D?{=!(bbApuQRd7;oEZa;xfyUIOW{w6{a;W7JWUwk(7EWcCOGqHdaJ2teMM5k) za{@~NfN9_~`2V(r4Vf38CJXZhXXHj^?07%Qi4G<2<0gqJ-rb~3`pZ;WTfPz%DY-Kc5PfvRS8s{>YzU1(KZdF*qK z>B>GYC*0oCQ}xbyL&3IB>H}N(n%-KMrHll9j$~Wz1=M6#o?ijou-nV)$~8ByS8Ll7U< zQ8MZk&`Fm5vnko&>Li92Y!C)?YeQNmsEu+k*Y$X((+)Zy85|oW`+yR5Nfi&|sNh)Y z-cl<%xib^TvP+gcBjwHoj*$Yqvb-)FM^Klq0Bhu3Q+3g;-a-(VXq7Q-2Q_^{^lZVN(C2>b zPNZ3ehL0{*%@S*mkvO=+-C>FY>?&nFE(wR$`NFctu*wQ)7tV@{-F}K8iuE^Ob!h_( zDN*C9cTc}s+;g~?Mu*0$0wjR)x3$WTYR4Z2q8f!l1aDE*P5@spnv za`rBN1@5wg3@6%O9Ti9jL-zKwSFckK0?p$KJOdA6-`PCck8oVy)d|nkCaYS^W|j~WZSOdJ8K!v|zxH;^hZlpdXojnUeE7==mpJ2HP&unBH(x9Wu_*XN zh|o%3qn^zZwDXJ?{Jr~bovjLn4Ny ze#R1R+vD8E2<2(d=i&^5ZfZmpMu&U0S#7`n7h1wFT#pYp`D~RXr;U!bKikPeX80FA z*pB-}nq7nRiA#@jbb3LTT5VJzz(;4BfG<7TW5zKWKvmki-*AP0g4bkeGBCNU+e$_q zTPxEpvB#{Ke&(OgI6cv%*Zm^i5_3%b$y=i` zT;zU@Z+>5EQKE`S(=<$ZyR7$ey{Hp9{(@!NIote7VRiY^{j|WSA0aY85P~S-A{IyU zS{W3{sOQ>?;qtTOXMra**#(=4PSR(|H^_hGmJQFsLVv}wM~Yj+B;zOjG<756LwC+l zCUw2Sj&)27=~++ef(1b+CGccR7vWWHr+8c3WDfWqv`{Wr7GIpM(s8h_%O02uyCU9) zIqK#MORnU}s2TP>h*1FQjR0b@+`$tWvj0q4KmuZYAY*ndpx@*aO~E6S z%$DP{QmRkrl}?JPGo8Y|bwGF3&sGk9HLBXgd;X`TUZY**n1ANGnKM@l(b{l)eQ8us zt*Fzh>;XhwD^}oYg5Q?DQ{~{=$>_Ye*ct!JBSz@G7%*cuuqUsnVS1eUO-Wf)iea3R zSDj$=SNPMzMD|Aa4{f9lSJGh;dWb&dg~-*t@0^|OYy?zDzd96Soz$7!ZcI*w)h*_{ zbWxZnkCZ<{=H1kD`JQNKTnB7(*Us553jh3a-MgmgYUxQ?WdLX(3z7P;`t$tE;QJNa znRiP&#N*Q`2KZ_7g}va3p%{1ba5}~Ut|%jeTz>)cjNo{0teuL_z{@ay)RALM(?VT3 zvS~9zk-(MDYwmxAHY-cg^nM!O|KTUMxpl?D8T(XRHM#VMi$1vQMXp(`*@x$)#;IY9 zT*WW8%Ku0xUAOxQN)|ckrV<%1INkYvYW9np%4Fg7iSTV_r}~~=HS?I;Ex(L&M`F`U zd2Q5o5Qbj(T5XK1V9*Io*$Zfx9re_ezmSXjwB@_$fqe)xp!RSxwCuR%(xu-q4W0dkCo$`9if#N*inY!6oiLYLbdUcW@*B?&<)<{Mq& zb$p5eR+{X?^Tzp;B3i}D8UwYtf>*`Vo+&|~QKGb#n4`mM$t{BeEAKb&i0~nUkAAv( z1_y&}!X6?I9KFa~%h8(xa?Ro+A?1?Sx%PV!%cKxnsT*Y>^s3l&FVw-))U#V;@vv0` zYnjF`whV>-8f3mHHk~p&@{&sr!WbVSGEY9F_E+h9DsWo(2P?`oA;Q<&sx1edZQ)i}^dt(`^h%u73FFwIZg&3Uu89_M)7;`($S z@*|)qzU_)7%}sZGaJp3laC$RiYd%yyq~p-5p5H>MkE;HpMfh9#Gf@BJ=3Osvr2mNV zk4v=|eO6J^K_v0TDnUj7?%2f^H4ns@j zp4rgO-}U?odOZM%CM`d1L+l34)h)t?^BUYX^4$_5B6kYOOIknuO0e&KGSx+rf`^j6 zeG8SBzW?*}Gh_YN38(Ko0omgEEoIxTgFpI)N!S=J1HPyE0|h>xeY~JWEW^64FNK1} zFn8)rpYL2e9(djZHf~Tv9}djq?|YzRC6-_e=61GtZI?F~G6>flfKO@5GXyTf zQmjAU)?X9DhX`%a{=RJ2n`i&2K-y!gEs}kF6}xeA<919D`KTb*OcT44Er$K`=n8+a zW;5?@72IxQR7|W6q#879b>1Z`F7-@8JT3Kf_pzrdFMSRsc&oihJ6asmSYsak9TPOn z0UZ@1Y^#TkFHjhKd(768cy!V4n%Vn%HYgQuqTEb3FN_b}ax{E(-k2krANvcS0V$gM z9Cbzyh0b)^`()va*J#k&(1G!VI5^paC@6R~cVaU-p(-bgj$b%vM?9lk%Bie~MIC|NIm9q}A-3LoYyRpG>5 zq10T*{S^s8|F&7#JNqc{MMHTbe>RGDJ^kPlB6x$nb#C2^oPd1N5rS^VEF0L>(vK*obG_hS(C1X)7cww7G1PpS-rR4% zWt5F*%-@1Yz8O|*OW3ql5X*>~0Y{N?d=bwL)zX!t?3j+Bi+2YvQFoK>1>medH+mY= z6mZjclY1neYlel~RsLJw8j;)BiQeuh`&R>%Y>=vUWt@23lBNoCE)w_zrjXNm#L+B1 z4!nYUc%xw+Uf0vKOf<6ors1DhL3cs&hhs2jr#SoPxwpB9Y?8r$4TMtV*HOuzt-OGp z=O5l$JH3-D8`64oP~afgBO7MTuFBtD4xH0WuoOGZo4@T!a6L8eC`&hR zcAzC`t@jT@yf4G2(yFFbwioCMElFPT^XJ38mh)B3KY9 zgo=u&H<=m?jY^7?1~iaf6e^J<65dqbbIy6|`o43W@4Jpa-tRikb^WgUxA(sHz1O|& zwbs4X-VevdeV&ZFjEIPcn25NDsEC+^kcvnyVj0XeoGT(CE-E~lB)lRmY$tE<4d1ZZ zdxN*IGeLMPC2Zw|?fJj=`S&3@0fq4DyZe4GRBt^&e*^8T=%3f0B7W$^4(h@Fy|)NsNCIlb^)&Pa-ecn)!K~ zf@HWBoC*D?asZqlv0K!OG2j}Recm@v4}sO;t;L0{7z{p8Cv%v~$}J;5R!#!RQo zNU=At)cZy~I=CE*HuW`7tF2&9$T6D#P!-N$cdF-p_Xca)N}Jj0@zY0vaud7}i@LQf)>;||uj)FGWZ`$znUc4 zV%kh=^5W80FmmBlrJ508^pJXfV2&T~%<68slMZYGygS_#55s|_G5c*wGn^H&JCh#Y z11mp+_c`MWIPs>j?kgsN`}G+4K#w{EA%VvY%xa*?=rH|#;3D*QTu;}zuLI+C_ho*( zw8X-7lswhVj z2L$1`9lr0w%0a4_^QJe?9dq84-F1|k4>QdRl>{|eST>ytu}PZ(Th48(qazeJP#b6M z8ES{aYLbpf^=Giux7%d@Fa~?+)3;IF)8L+crqSjm4gQsu){SK;m{ys2+4YKW-NNtL z+F$VfGHlldY&LnJ2#1Qh`TBRRV{wq}xmBC5 z!gb|Am#XR$VEYd?4V8I;8*{#Ac1;*~&6VHsx^uz2d*Q>j_%x=$zTWwQXJ9#;?!D_z?4^SxB8XdNnFDsinn`E$zku8KacAy3!C<~nLy93VOwgNY) z?9OQI7B#; z6VE&9NCkiK(YB!B?cklts5xX(0B)6vI8$i_IOVt7n&ooA?&x~+Oj)?jGdq{he|QV5 z?02DFEPy3A@ir=E4p{u)jdt%}3+-&Z?eIelur-s0Q=L1({@pn+y153NvxS_#-)4au zw{@>5>H6vxEBYu^UJlyB#w$C==Jig*#S)4?5bmnu574BX}ATUNSxfb-_`ijkHPaFXl3 zn9-cTS)->`%xnS2zm<^tVGx`nIgFz|9;}E9o5uCnsHt zDkHS3v--COe1zi_=)GURKN{Sn4emWnPT(Hgs2SE$W-pYJtNG=7F$6nuH{J?427$gK^<-8c_(iXSd_Akca)`W{=*|I)mt|8L zrYGEIBGksNQo-s|3fG(=ObH^_Pe^4DAN@L4<|j z3t?PxJ6pPDtR1|&ABw^iy20M^%H>rv6>K;C@3YSe^P{4LXG;RwzzOKfGieq2N0769 zs&x~1JJ+VAAMpf#Q)Ua@`({%hg-8=tSu ze6twrnt6>&BJP5{!PhE~lm)g^dX?D=C1D#U7+^0Io;M^9M(qQ;x?8z4Nm_WH^ESQx z7r?pwQmu%-7F_QYJr6#H2>m>)CS`xxf2&<5Qe#e}cAZE~JP{ctGVXsuUH+e-hreoW zf0aE<0Dmh3MSqC=qw*8`zpB;0cNP2D^SRNY$th=ALI1oaZ9J|W%#z7Vk7Rs+MpRGdG+HIpm7?`$M5cf#CSPyJ znGK>_E&H`~J(Rqcy34yP1i9W`H^%H3$d5V~b(eO6zIrXm+aMYA7{$}eKHh{{jhlcJ zW)BA4?nyw^JSeyAyc3|81tppLyPIVegXl_iklUIAYWU6N&03kDWS?V>I`u@^ z9Wr1Xpv~y-sRTWDX4sC+;h;D^Dw-ZF0aCeJQ^0yT(2AcNzOQWy!X;{ixos^dMrrHo zYs#Ri8$Y->N*8Lzu^tC!F90LR{kh)zc+ei#ZMbA=2eNc#-iy~Fpx15Ansdhuj7PQ| ztM*TaYC?hBjH~(}JX_e(xV#6_@rsW$MdQGT$Jp_SHmYT>%)>zOcK(jhT9F znW*2n>tH-Sa{7*>KGX>kCa1?0!MG6qee6dK7%3%X0I|xAyMaai4P(9 zhFO(5GZ+#g5}PL3se>rf;rYS$DX61&Wh~BJ1v$lpZb+O%hA$edU?KZa!${N27$aic7 zrE9&{iBTdb#~$1{s8t8$k+^U=e>o_9?fF%b+d)*vu%?$G=+lZO>nzLxh50$`q}(-7 z*2}0otuq31Zl;p-LPJotWPMPNphDH5KBg-yO$F| zKb2bj&f+oDWIx&~h#iCKwrwTE)^i{@1$bvZ_#IRHwB=VqOF9;UQ!CeCTf zhFWBjb1JtJD&Im~H79vP&P#4*si8S!YcEgUKT{h@hL)QC@--l@pRTcFy%9)$BTsj& z+b`7bBF^G!5`Q5f}Gv3^rpH5C>|G|NIz@`)j?2Y`p^|jLQzmb zmlqiG>=LF;`v8XOq;*;9C7?Y^vdMd_0@~!BM3*{cY{!2XoAvslN)9(2obiLa7SXBhXlvR&4 zVtql=G))=Yz6fOX;hPd}As|06sJ}G$5Oi@fAJzC1VAvKPdoC&qwN@K)>gn~MCO2k2 z_tXW=`c{Cp`d%mL)}LbYNHY8z5) zFNX>3ck*eSu-;8zEcJ@KAEgP3*n^cDY9oYpWX6#h*#KIhQ}Hg^7m!_311+Wr=doqd znEZP=P%7WN+udvn;`gkh>uy?to?U;+h7kG^H6%k-85#M4rQ~~LzLMLtzM5@kH^3!I}S-cmuq70BV)?T>&aSBMe8R41BtU;b> z*;3bL4vP7K?@#*+K_5}fZ^$eIy?Lbpo0kJ-BGIqtlo2QvgU7eZ{07>-uPtVe5}hJVxs7m>&cc-aC*fgjS8T$k`j};`#s3J zHXK>Fe=4XQ=Q3*U$bb|#D3T~Yf+Q*}CZ~DVkAV(y2xp~k*-6dw& zx%Vrmr6>K}_!o2=&}y zzkOMt&WQ2Yr(FZ8#Jh|8&jf-xVjy{+zZbL*3%>Mq=zt-sxyZt)9JCwiC$km>fHtez zat}Wbl!SM(bE0a6_BlYAYIqVf(Rhu-P=7FI_H7GW5fA3A`YSW<-GQpdNN2o?5$Ku6 zFDBfm1vTB=$;QDLw62|BjwES-Ijcr~HA!f{B{p#yXLfc5$061LK%$h?Cz2Q-?3+^H9vTJ8 zQch~=NC2pt??&i_tAnPP#``X}8;mJ$8Wdc~pl>9Hj&##OyI5rsYS|0gg>2>Z!Z=46 znb&&xek;gQq0*T?eBu7Fl7E13L%2WcH`IAJLUmH2a?)r&=ntw^?wPLuI=8^&nXnM0 zmw$5%$Q}Zbu&uONh6G|@RgSjPW6*-%*>{h7fNAnMBgS|p=xy7nE4;EmE7vQI>q!Sy zJa_dnQZcAzd{e@EB{1(F-tHVc3Z`y$h(WYazm9B!lz{}u#Qc$JzRwxbVea#8SvWxH z;fjOzRw;mZfEAkk^b06~2ZmNl?gD9z+0fN$btv!JS=i`)5rmmm+cxf(1nF48jeY%L zpt<(0r2ChG{$AgWym1HUS;^PM)oEaQg;!Y46y`0>y=FFyVK9$xsqV8%1Kq0n&H1pE zpfxfs9**h;P42q$r#QASZt0EqJ(U8Dbw+=By&kB>{L?#1KY~0bXP?n{Ayf|bn~0F( zL0H%jGo-x*#2s}#yHcD$Og@wl^#MZvXwr0!<$+r6!#nkL8OT153w5oZfvPqy{>+pd zsMg=P>@vgwL+x5ltyi^B?)607m3bfsx63=<+7H5~Ns+FB#vm<@PAV~*0g76);M{#1 zP&LG*O7G-?`n2A1Uzt$fv}<|m+%|#P&8nYjO9t60z@)}+FX$T4(NW97!Hi3}{^Mgk z7$y8OEyPMtwU?DDeV@gu;jdBpXX*E-bq9A=T*j+PYBWMC<)0=G+ zFidXzHg!}7l(l?r-iAXU^>sTHZgd7E_E7P)eZ!#tX6Ly5;twz?_K4>z?F3WQ*Ia7# zHZXe1zTR}(1)8nS?kPP@ARE-Yjy%`}Qh=#)#)?lMgm=ci>=foXaivCL8N$4!d|j}r zXfu?qT#k@Pu?8W_DNxVB6f}a#nduKxKs#*ivpGqaCs}%q#E!lPX}X8JM{_jDy(KT{ zzs~~IZ^`bA@N=N0ouBGa(S<2lN0w2tOQ3eU&{F&AOHf~_93qzmg3@7b*nU*+pT9ST zcnACb{N5<_^Rr~Y>OYzIhJaw-pGoy5-vB@V4Sybqd4~o6q~X4se`e&7zcTO@8^SjF z{?pT>!$LRd{(R!!N>tJ9B7gs#|6dY$nZNx{L3d4<@RmQ5`S1S)_mUFCs`YA3 literal 0 HcmV?d00001 diff --git a/inst/functions/emax.stan b/inst/functions/emax.stan new file mode 100644 index 0000000..7b07930 --- /dev/null +++ b/inst/functions/emax.stan @@ -0,0 +1,7 @@ + + // EMAX (without E0) + vector emax(vector x, vector log_ED50, real Emax, real gamma) { + int N = num_elements(x); + vector[N] xg = x^gamma; + return((Emax * xg) ./ (exp(log_ED50)^gamma + xg)); + } diff --git a/inst/functions/gp/basisfun.stan b/inst/functions/gp/basisfun.stan new file mode 100644 index 0000000..e1227d0 --- /dev/null +++ b/inst/functions/gp/basisfun.stan @@ -0,0 +1,22 @@ + + // Basis function matrix (EQ kernel) + matrix bf_eq(vector x, data matrix mat_B, data real L) { + int N = num_elements(x); + int B = cols(mat_B); + matrix[N, B] mat_X = rep_matrix(x+L, B); + matrix[N, B] PHI = 1.0/sqrt(L)*sin(0.5*pi()/L * mat_X .* mat_B); + return(PHI); + } + + // Compute spectral density of EQ kernel + vector log_spd_eq(real alpha, real ell, vector omega){ + return 2*log(alpha)+log(ell)+0.5*log(2*pi())-0.5*ell^2*omega .* omega; + } + + // Compute the multipliers s_b + vector bf_eq_multips(real alpha, real ell, data vector seq_B, + data real L) + { + return exp(0.5*log_spd_eq(alpha, ell, 0.5*pi()*seq_B/L)); + } + diff --git a/inst/functions/gp/group.stan b/inst/functions/gp/group.stan new file mode 100644 index 0000000..d3d2882 --- /dev/null +++ b/inst/functions/gp/group.stan @@ -0,0 +1,14 @@ + + // Compute a group-specific GP effect (interaction) + // - xi = the basis function coefficient parameters (size num_groups x num_bf) + // - PHI = the evaluated basis functions (size num_obs x num_bf) + // - s = sqrts. of eigenvalues of the basis functions (size num_bf) + // - group = group assignment of each data point (size num_obs) + vector compute_f_group(matrix xi, matrix PHI, vector s, array[] int group) { + int num_obs = rows(PHI); + vector[num_obs] f; + for(n in 1:num_obs){ + f[n] = sum(to_vector(PHI[n,:]) .* s .* to_vector(xi[group[n], :])); + } + return(f); + } diff --git a/inst/functions/gp/shared.stan b/inst/functions/gp/shared.stan new file mode 100644 index 0000000..2e061c6 --- /dev/null +++ b/inst/functions/gp/shared.stan @@ -0,0 +1,8 @@ + + // Compute a shared GP effect + // - xi = the basis function coefficient parameters (size num_bf) + // - PHI = the evaluated basis functions (size num_obs x num_bf) + // - s = sqrts. of eigenvalues of the basis functions (size num_bf) + vector compute_f_shared(vector xi, matrix PHI, vector s) { + return(PHI * (s .* xi)); + } diff --git a/inst/functions/hazard/inst_hazard.stan b/inst/functions/hazard/inst_hazard.stan new file mode 100644 index 0000000..5f76754 --- /dev/null +++ b/inst/functions/hazard/inst_hazard.stan @@ -0,0 +1,13 @@ + + // new instant hazard function + vector compute_inst_hazard(vector h0, real assoc, vector link, + real loc, real scale, real delta) { + int n = num_elements(h0); + vector[n] link_norm = (exp(link) + delta - loc) / scale; + return(h0 .* exp(assoc * link_norm)); + } + + // old instant hazard function + //vector compute_inst_hazard(vector h0, real assoc, vector link) { + // return(h0 .* exp(assoc * link)); + //} diff --git a/inst/functions/hazard/integrate.stan b/inst/functions/hazard/integrate.stan new file mode 100644 index 0000000..be928e5 --- /dev/null +++ b/inst/functions/hazard/integrate.stan @@ -0,0 +1,69 @@ + + // Linear interpolation + // - y = vector to interpolate, length N_in + // - x = incresing vector, length N_in + // - x_out = increasing vector, length N_out, values must be in + // (min(x),max(x)] + // Returns a vector, length N_out, corresponding to interpolated + // values y_out + vector interp_1d_linear(vector y, vector x, vector x_out) { + int left = 1; + int right = 1; + real w = 1.0; + real dx = 0.0; + int N_in = num_elements(x); + int N_out = num_elements(x_out); + vector[N_out] y_out = rep_vector(y[1], N_out); + for (j in 1 : N_out) { + while (x[right] < x_out[j]) { + right = right + 1; + } + while (x[left + 1] < x_out[j]) { + left = left + 1; + } + dx = x[right] - x[left]; + if(dx <= 0){ + y_out[j] = y[left]; + } else { + w = (x[right] - x_out[j]) / dx; + y_out[j] = w * y[left] + (1 - w) * y[right]; + } + } + return y_out; + } + + // Trapezoidal rule + vector trapezoid(vector x, vector y) { + int N = num_elements(x); + vector[N-1] dx = x[2:N] - x[1:(N-1)]; + vector[N-1] ym = 0.5*(y[2:N] + y[1:(N-1)]); + return(cumulative_sum(dx .* ym)); + } + + // Integrate from 0 to all t_out + // Using the trapezoidal rule + // Assumes that v has equidistant evaluations of the hazard at t = 0, ... , T + vector integrate_1d(vector t_out, vector t_grid, vector y_grid) { + int N_grid = num_elements(t_grid); + vector[N_grid] Y_grid = rep_vector(0.0, N_grid); + Y_grid[2:N_grid] = trapezoid(t_grid, y_grid); + return(interp_1d_linear(Y_grid, t_grid, t_out)); + } + + // Compute numeric integral for each individual + vector integrate_1d_many(vector t_out, vector t_grid, vector y_grid, + array[,] int inds_out, array[,] int inds_grid) { + int G = size(inds_out); + int N_out = num_elements(t_out); + vector[N_out] Y_int; + for(g in 1:G){ + int iso = inds_out[g, 1]; + int ieo = inds_out[g, 2]; + int isg = inds_grid[g, 1]; + int ieg = inds_grid[g, 2]; + Y_int[iso:ieo] = integrate_1d( + t_out[iso:ieo], t_grid[isg:ieg], y_grid[isg:ieg] + ); + } + return(Y_int); + } diff --git a/inst/functions/hazard/misc.stan b/inst/functions/hazard/misc.stan new file mode 100644 index 0000000..8123f19 --- /dev/null +++ b/inst/functions/hazard/misc.stan @@ -0,0 +1,27 @@ + + // Cumulative mean of vector + vector cumulative_mean(vector x) { + int n = num_elements(x); + return(cumulative_sum(x) ./ seq_len(n)); + } + + // Linear 1d interpolation + vector interp_1d(vector y, vector x, vector x_out) { + int left = 1; + int right = 1; + real w = 1.0; + int N_in = num_elements(x); + int N_out = num_elements(x_out); + vector[N_out] y_out; + for (j in 1 : N_out) { + while (x[right] < x_out[j]) { + right = right + 1; + } + while (x[left + 1] < x_out[j]) { + left = left + 1; + } + w = (x[right] - x_out[j]) / (x[right] - x[left]); + y_out[j] = w * y[left] + (1 - w) * y[right]; + } + return(y_out); + } diff --git a/inst/functions/hazard/weibull.stan b/inst/functions/hazard/weibull.stan new file mode 100644 index 0000000..1ad3f54 --- /dev/null +++ b/inst/functions/hazard/weibull.stan @@ -0,0 +1,5 @@ + + // revised Weibull hazard function + vector weibull_haz(vector x, real lambda, real gamma) { + return(gamma * lambda * x ^ (gamma - 1.0)); + } diff --git a/inst/functions/sf.stan b/inst/functions/sf.stan new file mode 100644 index 0000000..19879b1 --- /dev/null +++ b/inst/functions/sf.stan @@ -0,0 +1,11 @@ + + // Log Stein Fojo (without ts0) + vector log_sf_norm(vector t, real kg, real ks) { + return(log_diff_exp(log_sum_exp(kg .* t, -ks .* t), 0.0)); + } + + // Vectorized log Stein Fojo (without ts0) + // - all arguments should have the same length + vector log_sf_norm_vec(vector t, vector kg, vector ks) { + return(log_diff_exp(log_sum_exp(kg .* t, -ks .* t), 0.0)); + } diff --git a/inst/functions/utils.stan b/inst/functions/utils.stan new file mode 100644 index 0000000..af96f79 --- /dev/null +++ b/inst/functions/utils.stan @@ -0,0 +1,88 @@ + + // Create vector with elements 1, ..., N + vector seq_len(data int N) { + vector[N] v = rep_vector(1.0, N); + for(n in 2:N) v[n] = n; + return(v); + } + + // Create integer array with elements 1, ..., N + array[] int seq_len_int(data int N) { + array[N] int v = rep_array(1, N); + for(n in 2:N) v[n] = n; + return(v); + } + + // Same as rep(x, times=N) in R + vector rep_vector_times(vector x, data int N) { + return to_vector(rep_matrix(x, N)); + } + + // Repeat a matrix horizontally + matrix rep_matrix_times(data matrix x, data int times) { + int N = rows(x); + int M = cols(x); + matrix[N, M*times] out; + for(j in 1:times) { + out[:, (1+(j-1)*M):(j*M)] = x; + } + return(out); + } + + // Find indices of val + array[] int which(array[] int vals, int val){ + int n = size(vals); + array[n] int out; + int J = 0; + for(i in 1:n){ + if(i==val){ + out[n] = i; + J = J + 1; + } + } + return(out[1:J]); + } + + // Find first occurence of value in array + int first(array[] int arr, int val){ + int J = size(arr); + for(j in 1:J){ + if(arr[j]==val){ + return(j); + } + } + return(0); + } + + // Find last occurence of value in array + int last(array[] int arr, int val){ + int J = size(arr); + array[J] int r_arr = reverse(arr); + for(j in 1:J){ + if(r_arr[j]==val){ + return(J-j+1); + } + } + return(0); + } + + // Create an index array where first column is start indices and second has + // the end indices + // Assumes that arr is sorted + array[,] int inds_array(array[] int arr, int G){ + array[G, 2] int out; + for(g in 1:G){ + out[g, 1] = first(arr, g); + out[g, 2] = last(arr, g); + } + print("Created index array:"); + print(out); + return(out); + } + + // Transform parameter to log natural scale (vectorized) + // - all vector arguments should have the same length + vector to_log_natural_scale(vector log_z, vector log_mu, vector log_sigma) { + return(log_sum_exp(log_z + log_sigma, log_mu)); + } + diff --git a/inst/header.stan b/inst/header.stan new file mode 100644 index 0000000..ceb95ed --- /dev/null +++ b/inst/header.stan @@ -0,0 +1,3 @@ + + // - This Stan model is meant to be used both to fit a model and for + // computing predictios using standalone generated quantities diff --git a/man/BaselineHazard.Rd b/man/BaselineHazard.Rd new file mode 100644 index 0000000..481fc41 --- /dev/null +++ b/man/BaselineHazard.Rd @@ -0,0 +1,71 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/models-BaselineHazard.R +\name{BaselineHazard} +\alias{BaselineHazard} +\title{Hazard function (R6 class)} +\description{ +Hazard function (R6 class) + +Hazard function (R6 class) +} +\section{Super class}{ +\code{\link[sfgp:StanCodeCreator]{sfgp::StanCodeCreator}} -> \code{BaselineHazard} +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-BaselineHazard-stanname_t}{\code{BaselineHazard$stanname_t()}} +\item \href{#method-BaselineHazard-clone}{\code{BaselineHazard$clone()}} +} +} +\if{html}{\out{ +
Inherited methods + +
+}} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-BaselineHazard-stanname_t}{}}} +\subsection{Method \code{stanname_t()}}{ +Get name of the hazard time variable in 'Stan' code +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{BaselineHazard$stanname_t(datanames)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{datanames}}{names of data sets} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-BaselineHazard-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{BaselineHazard$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} +} diff --git a/man/FunctionDraws.Rd b/man/FunctionDraws.Rd new file mode 100644 index 0000000..f15ac91 --- /dev/null +++ b/man/FunctionDraws.Rd @@ -0,0 +1,384 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/misc-FunctionDraws.R +\name{FunctionDraws} +\alias{FunctionDraws} +\title{The 'FunctionDraws' class} +\description{ +The 'FunctionDraws' class + +The 'FunctionDraws' class +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-FunctionDraws-new}{\code{FunctionDraws$new()}} +\item \href{#method-FunctionDraws-num_draws}{\code{FunctionDraws$num_draws()}} +\item \href{#method-FunctionDraws-num_points}{\code{FunctionDraws$num_points()}} +\item \href{#method-FunctionDraws-get_name}{\code{FunctionDraws$get_name()}} +\item \href{#method-FunctionDraws-rename}{\code{FunctionDraws$rename()}} +\item \href{#method-FunctionDraws-get_input}{\code{FunctionDraws$get_input()}} +\item \href{#method-FunctionDraws-get_output}{\code{FunctionDraws$get_output()}} +\item \href{#method-FunctionDraws-as_data_frame}{\code{FunctionDraws$as_data_frame()}} +\item \href{#method-FunctionDraws-as_data_frame_long}{\code{FunctionDraws$as_data_frame_long()}} +\item \href{#method-FunctionDraws-quantiles_df}{\code{FunctionDraws$quantiles_df()}} +\item \href{#method-FunctionDraws-print}{\code{FunctionDraws$print()}} +\item \href{#method-FunctionDraws-plot}{\code{FunctionDraws$plot()}} +\item \href{#method-FunctionDraws-scale}{\code{FunctionDraws$scale()}} +\item \href{#method-FunctionDraws-add}{\code{FunctionDraws$add()}} +\item \href{#method-FunctionDraws-subtract}{\code{FunctionDraws$subtract()}} +\item \href{#method-FunctionDraws-log}{\code{FunctionDraws$log()}} +\item \href{#method-FunctionDraws-exp}{\code{FunctionDraws$exp()}} +\item \href{#method-FunctionDraws-zero_offset}{\code{FunctionDraws$zero_offset()}} +\item \href{#method-FunctionDraws-split_by_id}{\code{FunctionDraws$split_by_id()}} +\item \href{#method-FunctionDraws-zero_offset_by_id}{\code{FunctionDraws$zero_offset_by_id()}} +\item \href{#method-FunctionDraws-clone}{\code{FunctionDraws$clone()}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-FunctionDraws-new}{}}} +\subsection{Method \code{new()}}{ +Create the object. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{FunctionDraws$new(input, output, name)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{input}}{The data frame used as function input.} + +\item{\code{output}}{An \code{rvar} containing draws of the function values +at \code{input} points.} + +\item{\code{name}}{Function name.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-FunctionDraws-num_draws}{}}} +\subsection{Method \code{num_draws()}}{ +Get number of function draws +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{FunctionDraws$num_draws()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-FunctionDraws-num_points}{}}} +\subsection{Method \code{num_points()}}{ +Get number of input points. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{FunctionDraws$num_points()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-FunctionDraws-get_name}{}}} +\subsection{Method \code{get_name()}}{ +Get the function name. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{FunctionDraws$get_name()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-FunctionDraws-rename}{}}} +\subsection{Method \code{rename()}}{ +Rename the function. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{FunctionDraws$rename(name)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{name}}{new name} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-FunctionDraws-get_input}{}}} +\subsection{Method \code{get_input()}}{ +Get the input data frame. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{FunctionDraws$get_input()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-FunctionDraws-get_output}{}}} +\subsection{Method \code{get_output()}}{ +Get the output \code{rvar}. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{FunctionDraws$get_output(as_matrix = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{as_matrix}}{Transform the \code{rvar} to a draws matrix +(see the \code{posterior} package).} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-FunctionDraws-as_data_frame}{}}} +\subsection{Method \code{as_data_frame()}}{ +Get the input and output as \code{data.frame} with an \code{rvar} +column. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{FunctionDraws$as_data_frame()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-FunctionDraws-as_data_frame_long}{}}} +\subsection{Method \code{as_data_frame_long()}}{ +Get the input and output as a long \code{data.frame} with each draw +stacked on top of each other. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{FunctionDraws$as_data_frame_long()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-FunctionDraws-quantiles_df}{}}} +\subsection{Method \code{quantiles_df()}}{ +Create (a possibly filtered) data frame for ggplot. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{FunctionDraws$quantiles_df( + ci_inner = 0.5, + ci_outer = 0.8, + filter_by = NULL, + kept_vals = NULL +)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{ci_inner}}{inner credible interval} + +\item{\code{ci_outer}}{outer credible interval} + +\item{\code{filter_by}}{filtering variable} + +\item{\code{kept_vals}}{value of the filter variable that are kept} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-FunctionDraws-print}{}}} +\subsection{Method \code{print()}}{ +Print object description. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{FunctionDraws$print()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-FunctionDraws-plot}{}}} +\subsection{Method \code{plot()}}{ +Plot using automatically selected aesthetics by default. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{FunctionDraws$plot( + x_var = "auto", + group_by = "auto", + color_by = "auto", + facet_by = "auto", + ribbon_alpha = 0.3, + ci_inner = 0.5, + ci_outer = 0.8, + max_levels = 40, + filter_by = NULL, + kept_vals = NULL +)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{x_var}}{Name of the x-axis variable.} + +\item{\code{group_by}}{Name of the factor used as group aesthetic.} + +\item{\code{color_by}}{Name of the factor used as color and fill aesthetics.} + +\item{\code{facet_by}}{Faceting factor.} + +\item{\code{ribbon_alpha}}{Opacity of ribbon.} + +\item{\code{ci_inner}}{Inner credible interval (ribbon).} + +\item{\code{ci_outer}}{Outer credible interval (ribbon).} + +\item{\code{max_levels}}{Maximum number of levels to facet by.} + +\item{\code{filter_by}}{Factor to filter the rows by.} + +\item{\code{kept_vals}}{Values of the factor that are not filtered out.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-FunctionDraws-scale}{}}} +\subsection{Method \code{scale()}}{ +Scale the draws as \code{f_new = f*a + b} +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{FunctionDraws$scale(a = 1, b = 0)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{a}}{multiplier} + +\item{\code{b}}{added constant} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-FunctionDraws-add}{}}} +\subsection{Method \code{add()}}{ +Add to another \code{FunctionDraws}. You can also use \code{f1 + f2}. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{FunctionDraws$add(f)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{f}}{A \code{FunctionDraws} object.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-FunctionDraws-subtract}{}}} +\subsection{Method \code{subtract()}}{ +Subtract another \code{FunctionDraws}. You can also use \code{f1 - f2}. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{FunctionDraws$subtract(f)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{f}}{A \code{FunctionDraws} object.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-FunctionDraws-log}{}}} +\subsection{Method \code{log()}}{ +Take log of function draws. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{FunctionDraws$log()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-FunctionDraws-exp}{}}} +\subsection{Method \code{exp()}}{ +Exponentiate function draws. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{FunctionDraws$exp()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-FunctionDraws-zero_offset}{}}} +\subsection{Method \code{zero_offset()}}{ +Offset every function draw so that they start from zero +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{FunctionDraws$zero_offset()}\if{html}{\out{
}} +} + +\subsection{Returns}{ +a new object of class \code{\link{FunctionDraws}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-FunctionDraws-split_by_id}{}}} +\subsection{Method \code{split_by_id()}}{ +Split to groups +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{FunctionDraws$split_by_id(id_var)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{id_var}}{Name of the subject identifier factor.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +a list of objects of class \code{\link{FunctionDraws}}, +one for each group/id +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-FunctionDraws-zero_offset_by_id}{}}} +\subsection{Method \code{zero_offset_by_id()}}{ +Offset every function draw for each subject so that they start from zero +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{FunctionDraws$zero_offset_by_id(id_var)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{id_var}}{Name of the subject identifier factor.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +a new object of class \code{\link{FunctionDraws}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-FunctionDraws-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{FunctionDraws$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} +} diff --git a/man/JointModel.Rd b/man/JointModel.Rd new file mode 100644 index 0000000..f0e0ba0 --- /dev/null +++ b/man/JointModel.Rd @@ -0,0 +1,206 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/models-JointModel.R +\name{JointModel} +\alias{JointModel} +\title{The joint model class (R6 class)} +\description{ +The joint model class (R6 class) + +The joint model class (R6 class) +} +\section{Super classes}{ +\code{\link[sfgp:StanCodeCreator]{sfgp::StanCodeCreator}} -> \code{\link[sfgp:StanModel]{sfgp::StanModel}} -> \code{JointModel} +} +\section{Public fields}{ +\if{html}{\out{
}} +\describe{ +\item{\code{tte}}{The time-to-event model, has class \code{\link{TTEModel}}.} + +\item{\code{lon}}{The longitudinal model, has class \code{\link{TSModel}}.} +} +\if{html}{\out{
}} +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-JointModel-new}{\code{JointModel$new()}} +\item \href{#method-JointModel-string}{\code{JointModel$string()}} +\item \href{#method-JointModel-create_standata}{\code{JointModel$create_standata()}} +\item \href{#method-JointModel-fit}{\code{JointModel$fit()}} +\item \href{#method-JointModel-clone}{\code{JointModel$clone()}} +} +} +\if{html}{\out{ +
Inherited methods + +
+}} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-JointModel-new}{}}} +\subsection{Method \code{new()}}{ +Create model +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{JointModel$new(lon, h0 = NULL, compile = TRUE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{lon}}{An object of class \code{\link{StanModel}}.} + +\item{\code{h0}}{An object of class \code{\link{BaselineHazard}}.} + +\item{\code{compile}}{Should the 'Stan' model code be created and compiled.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-JointModel-string}{}}} +\subsection{Method \code{string()}}{ +The model description as a string +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{JointModel$string()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-JointModel-create_standata}{}}} +\subsection{Method \code{create_standata()}}{ +Create the 'Stan' data list from data frames. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{JointModel$create_standata( + data_lon, + data_tte, + data_grid = NULL, + term_confs = NULL, + num_bf = NULL, + scale_bf = NULL, + skip_transform = NULL, + prior_only = FALSE, + set_transforms = TRUE +)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{data_lon}}{Longitudinal data, a data frame.} + +\item{\code{data_tte}}{The events data, a data frame.} + +\item{\code{data_grid}}{The integration grid data, a data frame.} + +\item{\code{term_confs}}{A list that specifies configuration of model terms. +If name of any term is not found from the list, \code{$default_conf()} +of that \code{FormulaTerm} is used.} + +\item{\code{num_bf}}{If not \code{NULL}, configurations of all +\code{GPTerm}s are updated with this value.} + +\item{\code{scale_bf}}{If not \code{NULL}, configurations of all +\code{GPTerm}s are updated with this value.} + +\item{\code{skip_transform}}{Term names whose input transform should be +skipped.} + +\item{\code{prior_only}}{Sample from prior only?} + +\item{\code{set_transforms}}{If longitudinal data transforms should be set +based on the given \code{data_lon}. This should be \code{TRUE} when +fitting a model, and \code{FALSE} when computing predictions using GQ.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A list. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-JointModel-fit}{}}} +\subsection{Method \code{fit()}}{ +Fit the model. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{JointModel$fit( + data_lon, + data_tte, + data_grid, + term_confs = NULL, + num_bf = NULL, + scale_bf = NULL, + skip_transform = NULL, + prior_only = FALSE, + ... +)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{data_lon}}{Longitudinal data, a data frame.} + +\item{\code{data_tte}}{The events data, a data frame.} + +\item{\code{data_grid}}{The integration grid data, a data frame. Can be +created using \code{\link{create_jm_grid}}.} + +\item{\code{term_confs}}{A list that specifies configuration of model terms. +If name of any term is not found from the list, \code{$default_conf()} +of that \code{FormulaTerm} is used.} + +\item{\code{num_bf}}{If not \code{NULL}, configurations of all +\code{GPTerm}s are updated with this value.} + +\item{\code{scale_bf}}{If not \code{NULL}, configurations of all +\code{GPTerm}s are updated with this value.} + +\item{\code{skip_transform}}{Term names whose input transform should be +skipped.} + +\item{\code{prior_only}}{Sample from prior only.} + +\item{\code{...}}{Arguments passed to \code{sample} method of the +'CmdStanR' model.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A \code{\link{JointModelFit}} object. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-JointModel-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{JointModel$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} +} diff --git a/man/JointModelFit.Rd b/man/JointModelFit.Rd new file mode 100644 index 0000000..abbfa20 --- /dev/null +++ b/man/JointModelFit.Rd @@ -0,0 +1,272 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/fits-JointModelFit.R +\name{JointModelFit} +\alias{JointModelFit} +\title{The time-to-event model fit class} +\description{ +The time-to-event model fit class + +The time-to-event model fit class +} +\section{Super classes}{ +\code{\link[sfgp:StanModelFit]{sfgp::StanModelFit}} -> \code{\link[sfgp:TSModelFit]{sfgp::TSModelFit}} -> \code{JointModelFit} +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-JointModelFit-print}{\code{JointModelFit$print()}} +\item \href{#method-JointModelFit-get_model}{\code{JointModelFit$get_model()}} +\item \href{#method-JointModelFit-function_draws_tte}{\code{JointModelFit$function_draws_tte()}} +\item \href{#method-JointModelFit-h0}{\code{JointModelFit$h0()}} +\item \href{#method-JointModelFit-instant_hazard}{\code{JointModelFit$instant_hazard()}} +\item \href{#method-JointModelFit-cumulative_hazard}{\code{JointModelFit$cumulative_hazard()}} +\item \href{#method-JointModelFit-survival_function}{\code{JointModelFit$survival_function()}} +\item \href{#method-JointModelFit-plot_hazard}{\code{JointModelFit$plot_hazard()}} +\item \href{#method-JointModelFit-predict}{\code{JointModelFit$predict()}} +\item \href{#method-JointModelFit-predict_time}{\code{JointModelFit$predict_time()}} +\item \href{#method-JointModelFit-clone}{\code{JointModelFit$clone()}} +} +} +\if{html}{\out{ +
Inherited methods + +
+}} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-JointModelFit-print}{}}} +\subsection{Method \code{print()}}{ +Print object description. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{JointModelFit$print()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-JointModelFit-get_model}{}}} +\subsection{Method \code{get_model()}}{ +Get the entire joint model or a particular submodel. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{JointModelFit$get_model(name = NULL)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{name}}{Name of the submodel. If \code{NULL} (default), the entire +joint model object is returned.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-JointModelFit-function_draws_tte}{}}} +\subsection{Method \code{function_draws_tte()}}{ +Extract one TTE-related function as a \code{\link{FunctionDraws}} object. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{JointModelFit$function_draws_tte(component = "inst_haz", dataname = NULL)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{component}}{Base name of the function in the 'Stan' code.} + +\item{\code{dataname}}{Data where the function was evaluated.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-JointModelFit-h0}{}}} +\subsection{Method \code{h0()}}{ +Extract baseline hazard as a \code{\link{FunctionDraws}} object. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{JointModelFit$h0(dataname = "GRD")}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{dataname}}{Data where the function was evaluated.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-JointModelFit-instant_hazard}{}}} +\subsection{Method \code{instant_hazard()}}{ +Extract instant hazard as a \code{\link{FunctionDraws}} object. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{JointModelFit$instant_hazard(dataname = NULL)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{dataname}}{Data where the function was evaluated.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-JointModelFit-cumulative_hazard}{}}} +\subsection{Method \code{cumulative_hazard()}}{ +Extract cumulative hazard as a \code{\link{FunctionDraws}} object. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{JointModelFit$cumulative_hazard(dataname = NULL)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{dataname}}{Data where the function was evaluated.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-JointModelFit-survival_function}{}}} +\subsection{Method \code{survival_function()}}{ +Extract survival function as a \code{\link{FunctionDraws}} object. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{JointModelFit$survival_function(dataname = NULL)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{dataname}}{Data where the function was evaluated.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-JointModelFit-plot_hazard}{}}} +\subsection{Method \code{plot_hazard()}}{ +Visualize model fit (instant hazard). +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{JointModelFit$plot_hazard(plot_events = TRUE, ...)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{plot_events}}{Should the event occurrences be plotted? +Compute predictions at test points} + +\item{\code{...}}{Arguments passed to the plot method of +\code{\link{FunctionDraws}}.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-JointModelFit-predict}{}}} +\subsection{Method \code{predict()}}{ +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{JointModelFit$predict( + data_lon = NULL, + data_tte = NULL, + data_grid = NULL, + fitted_params = NULL, + ... +)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{data_lon}}{Data frame. If \code{NULL}, the data used in fitting +is used.} + +\item{\code{data_tte}}{Data frame. If \code{NULL}, the data used in fitting +is used.} + +\item{\code{data_grid}}{Data frame. If \code{NULL}, the data used in fitting +is used.} + +\item{\code{fitted_params}}{Parameters or model fit.} + +\item{\code{...}}{Other arguments to \code{generate_quantities()}.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A new \code{\link{JointModelFit}} object. +Compute predictions at given time points +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-JointModelFit-predict_time}{}}} +\subsection{Method \code{predict_time()}}{ +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{JointModelFit$predict_time( + t_test, + t_var = "t", + y_test = NULL, + t_var_copy = NULL +)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{t_test}}{The test time points} + +\item{\code{t_var}}{Name of the time variable} + +\item{\code{y_test}}{Test y} + +\item{\code{t_var_copy}}{Names of other continuous covariates to which +\code{t_test} should be copied.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A new fit object +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-JointModelFit-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{JointModelFit$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} +} diff --git a/man/StanCodeCreator.Rd b/man/StanCodeCreator.Rd new file mode 100644 index 0000000..1a4e5f0 --- /dev/null +++ b/man/StanCodeCreator.Rd @@ -0,0 +1,168 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/models-StanModel.R +\name{StanCodeCreator} +\alias{StanCodeCreator} +\title{Class for objects that can create parts of 'Stan' code} +\description{ +Class for objects that can create parts of 'Stan' code + +Class for objects that can create parts of 'Stan' code +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-StanCodeCreator-string}{\code{StanCodeCreator$string()}} +\item \href{#method-StanCodeCreator-print}{\code{StanCodeCreator$print()}} +\item \href{#method-StanCodeCreator-stanfiles_functions}{\code{StanCodeCreator$stanfiles_functions()}} +\item \href{#method-StanCodeCreator-stancode_functions}{\code{StanCodeCreator$stancode_functions()}} +\item \href{#method-StanCodeCreator-stancode_data}{\code{StanCodeCreator$stancode_data()}} +\item \href{#method-StanCodeCreator-stancode_tdata}{\code{StanCodeCreator$stancode_tdata()}} +\item \href{#method-StanCodeCreator-stancode_pars}{\code{StanCodeCreator$stancode_pars()}} +\item \href{#method-StanCodeCreator-stancode_tpars}{\code{StanCodeCreator$stancode_tpars()}} +\item \href{#method-StanCodeCreator-stancode_model}{\code{StanCodeCreator$stancode_model()}} +\item \href{#method-StanCodeCreator-stancode_gq}{\code{StanCodeCreator$stancode_gq()}} +\item \href{#method-StanCodeCreator-clone}{\code{StanCodeCreator$clone()}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanCodeCreator-string}{}}} +\subsection{Method \code{string()}}{ +Description as a string. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanCodeCreator$string()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanCodeCreator-print}{}}} +\subsection{Method \code{print()}}{ +Print info. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanCodeCreator$print()}\if{html}{\out{
}} +} + +\subsection{Returns}{ +Nothing. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanCodeCreator-stanfiles_functions}{}}} +\subsection{Method \code{stanfiles_functions()}}{ +Stan files where to get the functions. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanCodeCreator$stanfiles_functions()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanCodeCreator-stancode_functions}{}}} +\subsection{Method \code{stancode_functions()}}{ +Generate 'Stan' code for the functions block. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanCodeCreator$stancode_functions()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanCodeCreator-stancode_data}{}}} +\subsection{Method \code{stancode_data()}}{ +Generate 'Stan' code for the data block. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanCodeCreator$stancode_data(datanames = NULL)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{datanames}}{Names of input data sets.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanCodeCreator-stancode_tdata}{}}} +\subsection{Method \code{stancode_tdata()}}{ +Generate 'Stan' code for the transformed data block. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanCodeCreator$stancode_tdata(datanames = NULL)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{datanames}}{Names of input data sets.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanCodeCreator-stancode_pars}{}}} +\subsection{Method \code{stancode_pars()}}{ +Generate 'Stan' code for the parameters block. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanCodeCreator$stancode_pars()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanCodeCreator-stancode_tpars}{}}} +\subsection{Method \code{stancode_tpars()}}{ +Generate 'Stan' code for the transformed parameters block. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanCodeCreator$stancode_tpars(datanames = NULL)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{datanames}}{Names of input data sets.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanCodeCreator-stancode_model}{}}} +\subsection{Method \code{stancode_model()}}{ +Generate 'Stan' code for the model block. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanCodeCreator$stancode_model()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanCodeCreator-stancode_gq}{}}} +\subsection{Method \code{stancode_gq()}}{ +Generate 'Stan' code for the generated quantities block. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanCodeCreator$stancode_gq()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanCodeCreator-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanCodeCreator$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} +} diff --git a/man/StanModel.Rd b/man/StanModel.Rd new file mode 100644 index 0000000..019fb8e --- /dev/null +++ b/man/StanModel.Rd @@ -0,0 +1,145 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/models-StanModel.R +\name{StanModel} +\alias{StanModel} +\title{Abstract for objects that can generate entire 'Stan' models} +\description{ +Abstract for objects that can generate entire 'Stan' models + +Abstract for objects that can generate entire 'Stan' models +} +\section{Super class}{ +\code{\link[sfgp:StanCodeCreator]{sfgp::StanCodeCreator}} -> \code{StanModel} +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-StanModel-new}{\code{StanModel$new()}} +\item \href{#method-StanModel-get_stanmodel}{\code{StanModel$get_stanmodel()}} +\item \href{#method-StanModel-create_stancode}{\code{StanModel$create_stancode()}} +\item \href{#method-StanModel-create_stanmodel}{\code{StanModel$create_stanmodel()}} +\item \href{#method-StanModel-compile}{\code{StanModel$compile()}} +\item \href{#method-StanModel-clone}{\code{StanModel$clone()}} +} +} +\if{html}{\out{ +
Inherited methods + +
+}} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanModel-new}{}}} +\subsection{Method \code{new()}}{ +Create model +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanModel$new(compile = TRUE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{compile}}{Should the 'Stan' model code be created and compiled.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanModel-get_stanmodel}{}}} +\subsection{Method \code{get_stanmodel()}}{ +Get the underlying 'Stan' model. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanModel$get_stanmodel()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanModel-create_stancode}{}}} +\subsection{Method \code{create_stancode()}}{ +Create the 'Stan' model code for the model. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanModel$create_stancode(autoformat = TRUE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{autoformat}}{Should automatic formatting be attempted?} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A string. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanModel-create_stanmodel}{}}} +\subsection{Method \code{create_stanmodel()}}{ +Create and compile the 'Stan' model. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanModel$create_stanmodel(dir = tempdir())}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{dir}}{Path to directory where to store the \code{.stan} file.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A 'CmdStanR' model. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanModel-compile}{}}} +\subsection{Method \code{compile()}}{ +Create and compile the 'Stan' model. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanModel$compile(...)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{...}}{Arguments passed to \code{create_stanmodel()}.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +The updated model object (invisibly). +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanModel-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanModel$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} +} diff --git a/man/StanModelFit.Rd b/man/StanModelFit.Rd new file mode 100644 index 0000000..c8ff755 --- /dev/null +++ b/man/StanModelFit.Rd @@ -0,0 +1,191 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/fits-StanModelFit.R +\name{StanModelFit} +\alias{StanModelFit} +\title{The Fit class} +\description{ +The Fit class + +The Fit class +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-StanModelFit-get_data}{\code{StanModelFit$get_data()}} +\item \href{#method-StanModelFit-datanames}{\code{StanModelFit$datanames()}} +\item \href{#method-StanModelFit-get_model}{\code{StanModelFit$get_model()}} +\item \href{#method-StanModelFit-new}{\code{StanModelFit$new()}} +\item \href{#method-StanModelFit-get_stan_fit}{\code{StanModelFit$get_stan_fit()}} +\item \href{#method-StanModelFit-get_stan_data}{\code{StanModelFit$get_stan_data()}} +\item \href{#method-StanModelFit-draws}{\code{StanModelFit$draws()}} +\item \href{#method-StanModelFit-log_z_pars}{\code{StanModelFit$log_z_pars()}} +\item \href{#method-StanModelFit-loglik}{\code{StanModelFit$loglik()}} +\item \href{#method-StanModelFit-gq}{\code{StanModelFit$gq()}} +\item \href{#method-StanModelFit-clone}{\code{StanModelFit$clone()}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanModelFit-get_data}{}}} +\subsection{Method \code{get_data()}}{ +Get original data used to fit the model. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanModelFit$get_data(dataname = "LON")}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{dataname}}{Name of dataset.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanModelFit-datanames}{}}} +\subsection{Method \code{datanames()}}{ +Get names of stored data. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanModelFit$datanames()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanModelFit-get_model}{}}} +\subsection{Method \code{get_model()}}{ +Get model. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanModelFit$get_model(name)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{name}}{Name of model. Only has effect for +\code{\link{JointModelFit}} objects.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanModelFit-new}{}}} +\subsection{Method \code{new()}}{ +Create model fit object +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanModelFit$new(model, stan_fit, datasets, stan_data)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{model}}{A \code{\link{StanModel}} object.} + +\item{\code{stan_fit}}{The created 'Stan' fit.} + +\item{\code{datasets}}{Original data sets.} + +\item{\code{stan_data}}{The created 'Stan' data. Stored mainly for easier +debugging.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanModelFit-get_stan_fit}{}}} +\subsection{Method \code{get_stan_fit()}}{ +Get the underlying 'Stan' fit object. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanModelFit$get_stan_fit()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanModelFit-get_stan_data}{}}} +\subsection{Method \code{get_stan_data()}}{ +Get the underlying 'Stan' data object (for debugging). +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanModelFit$get_stan_data()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanModelFit-draws}{}}} +\subsection{Method \code{draws()}}{ +Extract draws as \code{rvar}s +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanModelFit$draws(name = NULL)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{name}}{Param/quantity name} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanModelFit-log_z_pars}{}}} +\subsection{Method \code{log_z_pars()}}{ +Full names of parameters that start with 'log_z_'. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanModelFit$log_z_pars()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanModelFit-loglik}{}}} +\subsection{Method \code{loglik()}}{ +Extract log likelihood as 'rvars'. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanModelFit$loglik()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanModelFit-gq}{}}} +\subsection{Method \code{gq()}}{ +Generate quantities using the fit. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanModelFit$gq(stan_data = NULL, fitted_params = NULL, ...)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{stan_data}}{Full 'Stan' input list.} + +\item{\code{fitted_params}}{Argument to \code{generate_quantities()}.} + +\item{\code{...}}{Other arguments to \code{generate_quantities()}.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StanModelFit-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{StanModelFit$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} +} diff --git a/man/TSModel.Rd b/man/TSModel.Rd new file mode 100644 index 0000000..0997e9b --- /dev/null +++ b/man/TSModel.Rd @@ -0,0 +1,272 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/models-TSModel.R +\name{TSModel} +\alias{TSModel} +\title{Time series (longitudinal) model class (R6 class)} +\description{ +Time series (longitudinal) model class (R6 class) + +Time series (longitudinal) model class (R6 class) +} +\section{Super classes}{ +\code{\link[sfgp:StanCodeCreator]{sfgp::StanCodeCreator}} -> \code{\link[sfgp:StanModel]{sfgp::StanModel}} -> \code{TSModel} +} +\section{Public fields}{ +\if{html}{\out{
}} +\describe{ +\item{\code{term_list}}{The additive model terms.} + +\item{\code{y_var}}{Name of the y variable.} + +\item{\code{id_var}}{Name of the subject identifier variable.} + +\item{\code{prior_sigma}}{Prior for the noise parameter.} + +\item{\code{sigma_upper}}{Upper bound for the noise parameter.} + +\item{\code{sigma_lower}}{Lower for the noise parameter.} + +\item{\code{log_y_cap}}{Upper bound on log scale for creating a capped +predicted signal or predicted observations.} +} +\if{html}{\out{
}} +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-TSModel-new}{\code{TSModel$new()}} +\item \href{#method-TSModel-stanname_y}{\code{TSModel$stanname_y()}} +\item \href{#method-TSModel-string}{\code{TSModel$string()}} +\item \href{#method-TSModel-get_delta}{\code{TSModel$get_delta()}} +\item \href{#method-TSModel-term_names}{\code{TSModel$term_names()}} +\item \href{#method-TSModel-create_standata}{\code{TSModel$create_standata()}} +\item \href{#method-TSModel-fit}{\code{TSModel$fit()}} +\item \href{#method-TSModel-clone}{\code{TSModel$clone()}} +} +} +\if{html}{\out{ +
Inherited methods + +
+}} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TSModel-new}{}}} +\subsection{Method \code{new()}}{ +Create model +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TSModel$new( + formula, + id_var = "id", + compile = TRUE, + delta = 0, + baseline = NULL, + prior_baseline = NULL, + prior_terms = NULL, + prior_sigma = "normal(0, 2)", + sigma_upper = 3, + sigma_lower = 0 +)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{formula}}{The model formula determining the terms and the y +variable (longitudinal observation).} + +\item{\code{id_var}}{Name of the subject identifier variable.} + +\item{\code{compile}}{Should the 'Stan' model code be created and compiled.} + +\item{\code{delta}}{Offset for log transform (\code{y_log = log(y + delta)}).} + +\item{\code{baseline}}{Baseline term definition. Created automatically based +on \code{id_var} if \code{NULL} (default).} + +\item{\code{prior_baseline}}{Prior for the baseline term.} + +\item{\code{prior_terms}}{A list with names equal to a subset of the +names of the model terms. Can be used to edit priors of term parameters.} + +\item{\code{prior_sigma}}{Prior for sigma} + +\item{\code{sigma_upper}}{Upper bound for sigma} + +\item{\code{sigma_lower}}{Lower bound for sigma} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TSModel-stanname_y}{}}} +\subsection{Method \code{stanname_y()}}{ +Get name of y variable in Stan code. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TSModel$stanname_y()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TSModel-string}{}}} +\subsection{Method \code{string()}}{ +The model description as a string +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TSModel$string()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TSModel-get_delta}{}}} +\subsection{Method \code{get_delta()}}{ +Get value of \code{delta}. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TSModel$get_delta()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TSModel-term_names}{}}} +\subsection{Method \code{term_names()}}{ +Get term names in Stan code. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TSModel$term_names()}\if{html}{\out{
}} +} + +\subsection{Returns}{ +A character vector with length equal to number of terms. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TSModel-create_standata}{}}} +\subsection{Method \code{create_standata()}}{ +Create the 'Stan' data list from a data frame. Performs normalization +on continuous variables that are input to GP terms. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TSModel$create_standata( + data, + term_confs = NULL, + num_bf = NULL, + scale_bf = NULL, + skip_transform = NULL, + prior_only = FALSE, + set_transforms = TRUE, + dataname = "LON" +)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{data}}{A data frame.} + +\item{\code{term_confs}}{A list that specifies configuration of model terms. +If name of any term is not found from the list, \code{$default_conf()} +of that \code{FormulaTerm} is used.} + +\item{\code{num_bf}}{If not \code{NULL}, configurations of all +\code{GPTerm}s are updated with this value.} + +\item{\code{scale_bf}}{If not \code{NULL}, configurations of all +\code{GPTerm}s are updated with this value.} + +\item{\code{skip_transform}}{Term names whose input transform should be +skipped.} + +\item{\code{prior_only}}{Sample from prior only?} + +\item{\code{set_transforms}}{If data transforms should be set based on the given +\code{data}. This should be \code{TRUE} when fitting a model, and +\code{FALSE} when computing predictions using GQ.} + +\item{\code{dataname}}{Name of dataset.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A list. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TSModel-fit}{}}} +\subsection{Method \code{fit()}}{ +Fit the model. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TSModel$fit( + data, + term_confs = NULL, + num_bf = NULL, + scale_bf = NULL, + skip_transform = NULL, + prior_only = FALSE, + ... +)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{data}}{A data frame.} + +\item{\code{term_confs}}{A list that specifies configuration of model terms. +If name of any term is not found from the list, \code{$default_conf()} +of that \code{FormulaTerm} is used.} + +\item{\code{num_bf}}{If not \code{NULL}, configurations of all +\code{GPTerm}s are updated with this value.} + +\item{\code{scale_bf}}{If not \code{NULL}, configurations of all +\code{GPTerm}s are updated with this value.} + +\item{\code{skip_transform}}{Term names whose input transform should be +skipped.} + +\item{\code{prior_only}}{Sample from prior only.} + +\item{\code{...}}{Arguments passed to \code{sample} method of the +'CmdStanR' model.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +An \code{\link{TSModelFit}} object. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TSModel-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TSModel$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} +} diff --git a/man/TSModelFit.Rd b/man/TSModelFit.Rd new file mode 100644 index 0000000..282514c --- /dev/null +++ b/man/TSModelFit.Rd @@ -0,0 +1,306 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/fits-TSModelFit.R +\name{TSModelFit} +\alias{TSModelFit} +\title{The 'TSModelFit' class} +\description{ +The 'TSModelFit' class + +The 'TSModelFit' class +} +\section{Super class}{ +\code{\link[sfgp:StanModelFit]{sfgp::StanModelFit}} -> \code{TSModelFit} +} +\section{Public fields}{ +\if{html}{\out{
}} +\describe{ +\item{\code{term_confs}}{The term configurations used.} +} +\if{html}{\out{
}} +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-TSModelFit-new}{\code{TSModelFit$new()}} +\item \href{#method-TSModelFit-print}{\code{TSModelFit$print()}} +\item \href{#method-TSModelFit-function_draws}{\code{TSModelFit$function_draws()}} +\item \href{#method-TSModelFit-create_functiondraws}{\code{TSModelFit$create_functiondraws()}} +\item \href{#method-TSModelFit-measurement_error}{\code{TSModelFit$measurement_error()}} +\item \href{#method-TSModelFit-fit_quality_df}{\code{TSModelFit$fit_quality_df()}} +\item \href{#method-TSModelFit-fit_quality_summary}{\code{TSModelFit$fit_quality_summary()}} +\item \href{#method-TSModelFit-plot}{\code{TSModelFit$plot()}} +\item \href{#method-TSModelFit-predict}{\code{TSModelFit$predict()}} +\item \href{#method-TSModelFit-predict_time}{\code{TSModelFit$predict_time()}} +\item \href{#method-TSModelFit-clone}{\code{TSModelFit$clone()}} +} +} +\if{html}{\out{ +
Inherited methods + +
+}} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TSModelFit-new}{}}} +\subsection{Method \code{new()}}{ +Create model fit object +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TSModelFit$new(model, stan_fit, datasets, stan_data, term_confs)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{model}}{A \code{\link{StanModel}} object.} + +\item{\code{stan_fit}}{The created 'Stan' fit.} + +\item{\code{datasets}}{Original data (list of data frames).} + +\item{\code{stan_data}}{The created 'Stan' data. Stored mainly for easier +debugging.} + +\item{\code{term_confs}}{The term configurations used.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TSModelFit-print}{}}} +\subsection{Method \code{print()}}{ +Print object description. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TSModelFit$print()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TSModelFit-function_draws}{}}} +\subsection{Method \code{function_draws()}}{ +Extract one component as a \code{FunctionDraws} object. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TSModelFit$function_draws( + component = "f_sum", + data_scale = TRUE, + capped = FALSE +)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{component}}{Name of the component (term). Valid names can +be checked using \code{$model$term_names()}. Alternatively, can be +an integer that is the component index.} + +\item{\code{data_scale}}{Transform to data scale? Has no effect if +extracting a single component.} + +\item{\code{capped}}{Should a capped version of the draws be taken? This +is useful if the fit/prediction explodes to infinity. Only has effect +if \code{component="f_sum"} or \code{component="y_log_pred"}.} + +\item{\code{dataname}}{name of data set used to evaluate the function} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TSModelFit-create_functiondraws}{}}} +\subsection{Method \code{create_functiondraws()}}{ +Initialize a \code{\link{FunctionDraws}} object from the fit. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TSModelFit$create_functiondraws(input_vars, f_name)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{input_vars}}{Column names in the data.} + +\item{\code{f_name}}{Name of function in 'Stan' code.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TSModelFit-measurement_error}{}}} +\subsection{Method \code{measurement_error()}}{ +Compute estimate of the measurement error percentage of each observation +as 'rvars'. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TSModelFit$measurement_error()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TSModelFit-fit_quality_df}{}}} +\subsection{Method \code{fit_quality_df()}}{ +Get a data frame that is useful for assessing fit quality. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TSModelFit$fit_quality_df()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TSModelFit-fit_quality_summary}{}}} +\subsection{Method \code{fit_quality_summary()}}{ +Get summary statistics useful for assessing fit quality. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TSModelFit$fit_quality_summary( + metric = "error_perc", + by = NULL, + fun = stats::median +)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{metric}}{Metric used to assess quality. Can be \code{"error_perc"} +or \code{"loglik"}.} + +\item{\code{by}}{The factor by which to aggregate. If \code{NULL}, the id +variable of the model is used.} + +\item{\code{fun}}{Function used to aggregate.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TSModelFit-plot}{}}} +\subsection{Method \code{plot()}}{ +Plot fit or predictive distribution +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TSModelFit$plot( + f_reference = NULL, + plot_y = TRUE, + capped = TRUE, + predictive = TRUE, + f_ref_name = "the true signal", + filter_by = NULL, + kept_vals = NULL, + ... +)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{f_reference}}{Reference function to plot against the fit.} + +\item{\code{plot_y}}{Should y data be plotted?} + +\item{\code{capped}}{Should a capped version of the fit be plotted? This +is useful if the fit explodes to infinity.} + +\item{\code{predictive}}{Should this plot the predictive distribution for +new observations? Otherwise the inferred signal is plotted.} + +\item{\code{f_ref_name}}{Name of the reference function.} + +\item{\code{filter_by}}{Factor to filter the rows by.} + +\item{\code{kept_vals}}{Values of the factor that are not filtered out.} + +\item{\code{...}}{Arguments passed to the plot method of +\code{\link{FunctionDraws}}. +Compute predictions at test points} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TSModelFit-predict}{}}} +\subsection{Method \code{predict()}}{ +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TSModelFit$predict(data = NULL, fitted_params = NULL, ...)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{data}}{The input data frame of test points.} + +\item{\code{fitted_params}}{Parameters or model fit.} + +\item{\code{...}}{Other arguments to \code{generate_quantities()}.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A new fit object. +Compute predictions at given time points +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TSModelFit-predict_time}{}}} +\subsection{Method \code{predict_time()}}{ +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TSModelFit$predict_time( + t_test, + t_var = "t", + y_test = NULL, + t_var_copy = NULL, + ... +)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{t_test}}{The test time points} + +\item{\code{t_var}}{Name of the time variable} + +\item{\code{y_test}}{Test y} + +\item{\code{t_var_copy}}{Names of other continuous covariates to which +\code{t_test} should be copied.} + +\item{\code{...}}{Arguments passed to \code{$predict()}.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A new fit object +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TSModelFit-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TSModelFit$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} +} diff --git a/man/TTEModel.Rd b/man/TTEModel.Rd new file mode 100644 index 0000000..d497dc6 --- /dev/null +++ b/man/TTEModel.Rd @@ -0,0 +1,125 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/models-TTEModel.R +\name{TTEModel} +\alias{TTEModel} +\title{The time-to-event model class (R6 class)} +\description{ +The time-to-event model class (R6 class) + +The time-to-event model class (R6 class) +} +\section{Super classes}{ +\code{\link[sfgp:StanCodeCreator]{sfgp::StanCodeCreator}} -> \code{\link[sfgp:StanModel]{sfgp::StanModel}} -> \code{TTEModel} +} +\section{Public fields}{ +\if{html}{\out{
}} +\describe{ +\item{\code{h0}}{An object of class \code{\link{BaselineHazard}}.} + +\item{\code{id_var}}{name of id variable} +} +\if{html}{\out{
}} +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-TTEModel-stanname_id}{\code{TTEModel$stanname_id()}} +\item \href{#method-TTEModel-new}{\code{TTEModel$new()}} +\item \href{#method-TTEModel-create_standata}{\code{TTEModel$create_standata()}} +\item \href{#method-TTEModel-clone}{\code{TTEModel$clone()}} +} +} +\if{html}{\out{ +
Inherited methods + +
+}} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TTEModel-stanname_id}{}}} +\subsection{Method \code{stanname_id()}}{ +Get name of id variable in 'Stan' code. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TTEModel$stanname_id(datanames)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{datanames}}{names of data sets} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TTEModel-new}{}}} +\subsection{Method \code{new()}}{ +Create model +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TTEModel$new(h0 = NULL, link_name = "f_sum")}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{h0}}{An object of class \code{\link{BaselineHazard}}.} + +\item{\code{link_name}}{Base name of the link variable in 'Stan' code.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TTEModel-create_standata}{}}} +\subsection{Method \code{create_standata()}}{ +Create the 'Stan' data list from a data vector. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TTEModel$create_standata(event)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{event}}{a logical vector} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A list. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TTEModel-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TTEModel$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} +} diff --git a/man/TermList.Rd b/man/TermList.Rd new file mode 100644 index 0000000..ab6f0c1 --- /dev/null +++ b/man/TermList.Rd @@ -0,0 +1,248 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/terms-TermList.R +\name{TermList} +\alias{TermList} +\title{Additive terms (R6 class)} +\description{ +A semi-parametric additive model (R6 class). +} +\section{Super class}{ +\code{\link[sfgp:StanCodeCreator]{sfgp::StanCodeCreator}} -> \code{TermList} +} +\section{Public fields}{ +\if{html}{\out{
}} +\describe{ +\item{\code{terms}}{A list of \code{FormulaTerm} objects, defining the +function components.} + +\item{\code{fsum_name}}{Base name of the sum of the terms variable in 'Stan' code.} +} +\if{html}{\out{
}} +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-TermList-new}{\code{TermList$new()}} +\item \href{#method-TermList-string}{\code{TermList$string()}} +\item \href{#method-TermList-input_vars}{\code{TermList$input_vars()}} +\item \href{#method-TermList-stan_names}{\code{TermList$stan_names()}} +\item \href{#method-TermList-get_term}{\code{TermList$get_term()}} +\item \href{#method-TermList-latex}{\code{TermList$latex()}} +\item \href{#method-TermList-length}{\code{TermList$length()}} +\item \href{#method-TermList-create_standata}{\code{TermList$create_standata()}} +\item \href{#method-TermList-fill_term_confs}{\code{TermList$fill_term_confs()}} +\item \href{#method-TermList-set_transforms}{\code{TermList$set_transforms()}} +\item \href{#method-TermList-check_transforms}{\code{TermList$check_transforms()}} +\item \href{#method-TermList-clone}{\code{TermList$clone()}} +} +} +\if{html}{\out{ +
Inherited methods + +
+}} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TermList-new}{}}} +\subsection{Method \code{new()}}{ +Create term list +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TermList$new(terms)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{terms}}{The list of model terms.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TermList-string}{}}} +\subsection{Method \code{string()}}{ +The term list as a string +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TermList$string()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TermList-input_vars}{}}} +\subsection{Method \code{input_vars()}}{ +Get names of all input variables +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TermList$input_vars()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TermList-stan_names}{}}} +\subsection{Method \code{stan_names()}}{ +List names of all terms in Stan code. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TermList$stan_names()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TermList-get_term}{}}} +\subsection{Method \code{get_term()}}{ +Get term object based on term name in Stan' +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TermList$get_term(f_name_stan)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{f_name_stan}}{Name of term in 'Stan'. Valid names +can be checked using the `$stan_names()` method.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TermList-latex}{}}} +\subsection{Method \code{latex()}}{ +Latex code, mathematical notation for the terms +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TermList$latex()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TermList-length}{}}} +\subsection{Method \code{length()}}{ +List length +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TermList$length()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TermList-create_standata}{}}} +\subsection{Method \code{create_standata()}}{ +Create the 'Stan' data list from a data frame. Performs transforms +on continuous variables that are input to GP terms. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TermList$create_standata(data, dataname, term_confs = NULL)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{data}}{A data frame.} + +\item{\code{dataname}}{Name of data set.} + +\item{\code{term_confs}}{A list that specifies configuration of model terms. +If name of any term is not found from the list, \code{$default_conf()} +of that \code{FormulaTerm} is used.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A list. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TermList-fill_term_confs}{}}} +\subsection{Method \code{fill_term_confs()}}{ +Get term configuration of all terms, some of which can be given by user. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TermList$fill_term_confs(term_confs = NULL, num_bf = NULL, scale_bf = NULL)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{term_confs}}{A list that specifies configuration of model terms. +If name of any term is not found from the list, \code{$default_conf()} +of that \code{FormulaTerm} is used.} + +\item{\code{num_bf}}{If not \code{NULL}, configurations of all +\code{GPTerm}s are updated with this value.} + +\item{\code{scale_bf}}{If not \code{NULL}, configurations of all +\code{GPTerm}s are updated with this value.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +The updated model object (invisibly). +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TermList-set_transforms}{}}} +\subsection{Method \code{set_transforms()}}{ +Set variable transforms using data. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TermList$set_transforms(data, names_skip = NULL)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{data}}{A data frame.} + +\item{\code{names_skip}}{Names of terms whose input transform should be +skipped.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TermList-check_transforms}{}}} +\subsection{Method \code{check_transforms()}}{ +Check how data transforms using the current model transforms. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TermList$check_transforms(data)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{data}}{A data frame.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TermList-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{TermList$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} +} diff --git a/man/WeibullHazard.Rd b/man/WeibullHazard.Rd new file mode 100644 index 0000000..b58af4e --- /dev/null +++ b/man/WeibullHazard.Rd @@ -0,0 +1,77 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/models-BaselineHazard.R +\name{WeibullHazard} +\alias{WeibullHazard} +\title{Weibull hazard function} +\description{ +Weibull hazard function + +Weibull hazard function +} +\section{Super classes}{ +\code{\link[sfgp:StanCodeCreator]{sfgp::StanCodeCreator}} -> \code{\link[sfgp:BaselineHazard]{sfgp::BaselineHazard}} -> \code{WeibullHazard} +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-WeibullHazard-new}{\code{WeibullHazard$new()}} +\item \href{#method-WeibullHazard-clone}{\code{WeibullHazard$clone()}} +} +} +\if{html}{\out{ +
Inherited methods + +
+}} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-WeibullHazard-new}{}}} +\subsection{Method \code{new()}}{ +Create hazard +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{WeibullHazard$new( + prior_lambda = "gamma(5, 5)", + prior_gamma = "inv_gamma(3, 6)" +)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{prior_lambda}}{prior of Weibull scale param} + +\item{\code{prior_gamma}}{prior of Weibull shape param} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-WeibullHazard-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{WeibullHazard$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} +} diff --git a/man/add_sff_input.Rd b/man/add_sff_input.Rd new file mode 100644 index 0000000..7a80c81 --- /dev/null +++ b/man/add_sff_input.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-data.R +\name{add_sff_input} +\alias{add_sff_input} +\title{Add input for 'FormulaSFTerm'} +\usage{ +add_sff_input(df, model, debug = FALSE) +} +\arguments{ +\item{df}{the data frame} + +\item{model}{an object of class \code{\link{TSModel}}} + +\item{debug}{print debugging messages?} +} +\description{ +Duplicates original data columns, adding new columns with the \code{_kg} and +\code{_ks} suffixes if these are missing. +} diff --git a/man/create_jm_grid.Rd b/man/create_jm_grid.Rd new file mode 100644 index 0000000..49f2344 --- /dev/null +++ b/man/create_jm_grid.Rd @@ -0,0 +1,31 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-data.R +\name{create_jm_grid} +\alias{create_jm_grid} +\title{Create a dense grid until event time for each subject (for 'JointModel')} +\usage{ +create_jm_grid( + df_tte, + df_lon, + num_points = 30, + id_var = "id", + time_var = "t", + even_spacing = FALSE +) +} +\arguments{ +\item{df_tte}{time-to-event data frame} + +\item{df_lon}{the longitudinal data frame} + +\item{num_points}{number of grid points} + +\item{id_var}{id variable} + +\item{time_var}{time variable} + +\item{even_spacing}{space grid points evenly?} +} +\description{ +Create a dense grid until event time for each subject (for 'JointModel') +} diff --git a/man/create_termlist.Rd b/man/create_termlist.Rd new file mode 100644 index 0000000..168c079 --- /dev/null +++ b/man/create_termlist.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/terms-TermList.R +\name{create_termlist} +\alias{create_termlist} +\title{Create additive terms} +\usage{ +create_termlist(formula, priors) +} +\arguments{ +\item{formula}{the model formula} + +\item{priors}{Prior configurations.} +} +\value{ +An object of \code{R6} class \code{TermList}. +} +\description{ +Create additive terms +} diff --git a/man/dot-FunctionDraws.Rd b/man/dot-FunctionDraws.Rd new file mode 100644 index 0000000..c4771eb --- /dev/null +++ b/man/dot-FunctionDraws.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/misc-FunctionDraws.R +\name{-.FunctionDraws} +\alias{-.FunctionDraws} +\title{Subtract} +\usage{ +\method{-}{FunctionDraws}(f1, f2) +} +\arguments{ +\item{f1}{component 1} + +\item{f2}{component 2} +} +\value{ +the difference \code{f1 - f2} +} +\description{ +Subtract +} diff --git a/man/example.Rd b/man/example.Rd new file mode 100644 index 0000000..a374f2a --- /dev/null +++ b/man/example.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/aaa.R +\name{example} +\alias{example} +\title{Run an example} +\usage{ +example(num_bf = 32, scale_bf = 1.5, formula = "y ~ gp(x)", ...) +} +\arguments{ +\item{num_bf}{Number of basis functions.} + +\item{scale_bf}{Basis function domain scale.} + +\item{formula}{The model formula.} + +\item{...}{Other arguments to the \code{$fit()} method of +\code{\link{TSModel}}.} +} +\value{ +An \code{\link{TSModelFit}} object. +} +\description{ +Fits a model to simple simulated data. +} diff --git a/man/example2.Rd b/man/example2.Rd new file mode 100644 index 0000000..4588526 --- /dev/null +++ b/man/example2.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/aaa.R +\name{example2} +\alias{example2} +\title{Run an example} +\usage{ +example2( + num_bf = 24, + scale_bf = 1.5, + formula = "y ~ gp(time) + gp(time,arm) + gp(time,id)", + ... +) +} +\arguments{ +\item{num_bf}{Number of basis functions.} + +\item{scale_bf}{Basis function domain scale.} + +\item{formula}{The model formula.} + +\item{...}{Other arguments to the \code{$fit()} method of +\code{\link{TSModel}}.} +} +\value{ +An \code{\link{TSModelFit}} object. +} +\description{ +Fits a model to \code{testdata}. +} diff --git a/man/example_data_jm.Rd b/man/example_data_jm.Rd new file mode 100644 index 0000000..90e22d4 --- /dev/null +++ b/man/example_data_jm.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/data.R +\docType{data} +\name{example_data_jm} +\alias{example_data_jm} +\title{Simulated joint longitudinal and time-to-event test data} +\format{ +A list of two data frames \code{lon} (longitudinal data) and +\code{tte} (time-to-event data). Generated using \code{simjm}. +} +\usage{ +example_data_jm +} +\description{ +Simulated joint longitudinal and time-to-event test data +} +\seealso{ +Other built-in datasets: +\code{\link{testdata}} +} +\concept{built-in datasets} +\keyword{datasets} diff --git a/man/extend_df.Rd b/man/extend_df.Rd new file mode 100644 index 0000000..854061e --- /dev/null +++ b/man/extend_df.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-data.R +\name{extend_df} +\alias{extend_df} +\title{Extend data frame} +\usage{ +extend_df(df, t, time_var) +} +\arguments{ +\item{df}{original data frame} + +\item{t}{vector of new time values} + +\item{time_var}{name of time variable} +} +\value{ +new data frame +} +\description{ +Extend data frame +} diff --git a/man/extend_df2.Rd b/man/extend_df2.Rd new file mode 100644 index 0000000..e9b0590 --- /dev/null +++ b/man/extend_df2.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-data.R +\name{extend_df2} +\alias{extend_df2} +\title{Extend data frame and add a constant continuous variable} +\usage{ +extend_df2(df, t, time_var, x, x_var, id_var) +} +\arguments{ +\item{df}{original data frame} + +\item{t}{vector of new time values} + +\item{time_var}{name of time variable} + +\item{x}{value of other continuous variable (will be constant)} + +\item{x_var}{name of other continuous variable} + +\item{id_var}{name of id variable} +} +\value{ +new data frame +} +\description{ +Extend data frame and add a constant continuous variable +} diff --git a/man/new_draws.Rd b/man/new_draws.Rd new file mode 100644 index 0000000..6a2683b --- /dev/null +++ b/man/new_draws.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-rvar.R +\name{new_draws} +\alias{new_draws} +\title{Reset values of given parameters in 'rvars' list to a constant value} +\usage{ +new_draws(rvars, names, value = 0) +} +\arguments{ +\item{rvars}{An object of class \code{draws_rvars}.} + +\item{names}{A character vector of parameter names.} + +\item{value}{Value for the parameters.} +} +\value{ +An updated object of class \code{draws_rvars}. +} +\description{ +Reset values of given parameters in 'rvars' list to a constant value +} diff --git a/man/pipe.Rd b/man/pipe.Rd new file mode 100644 index 0000000..1f8f237 --- /dev/null +++ b/man/pipe.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-pipe.R +\name{\%>\%} +\alias{\%>\%} +\title{Pipe operator} +\usage{ +lhs \%>\% rhs +} +\arguments{ +\item{lhs}{A value or the magrittr placeholder.} + +\item{rhs}{A function call using the magrittr semantics.} +} +\value{ +The result of calling `rhs(lhs)`. +} +\description{ +See \code{magrittr::\link[magrittr:pipe]{\%>\%}} for details. +} +\keyword{internal} diff --git a/man/plot_metric.Rd b/man/plot_metric.Rd new file mode 100644 index 0000000..f82dd87 --- /dev/null +++ b/man/plot_metric.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/main-treatment_effect.R +\name{plot_metric} +\alias{plot_metric} +\title{Example implementation of plotting trajectory metrics.} +\usage{ +plot_metric(df, metric = "depth", group_var = "arm") +} +\arguments{ +\item{df}{A full long data frame returned by +\code{\link{trajectory_metrics}}.} + +\item{metric}{Name of the metric. See possible metric names in +documentation of \code{\link{trajectory_metrics}}.} + +\item{group_var}{Name of grouping variable.} +} +\description{ +Example implementation of plotting trajectory metrics. +} diff --git a/man/plus-.FunctionDraws.Rd b/man/plus-.FunctionDraws.Rd new file mode 100644 index 0000000..08ceee9 --- /dev/null +++ b/man/plus-.FunctionDraws.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/misc-FunctionDraws.R +\name{+.FunctionDraws} +\alias{+.FunctionDraws} +\title{Add} +\usage{ +\method{+}{FunctionDraws}(f1, f2) +} +\arguments{ +\item{f1}{component 1} + +\item{f2}{component 2} +} +\value{ +the sum \code{f1 + f2} +} +\description{ +Add +} diff --git a/man/predict_new_subjects.Rd b/man/predict_new_subjects.Rd new file mode 100644 index 0000000..1d936e9 --- /dev/null +++ b/man/predict_new_subjects.Rd @@ -0,0 +1,38 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/main-treatment_effect.R +\name{predict_new_subjects} +\alias{predict_new_subjects} +\title{Generate predictions for new imaginary subjects from each group} +\usage{ +predict_new_subjects( + fit_post, + fit_prior, + time_var, + t_pred = NULL, + group_var = "arm", + zero_log_z = FALSE, + prior_param_names = NULL +) +} +\arguments{ +\item{fit_post}{posterior fit} + +\item{fit_prior}{prior fit} + +\item{time_var}{time variable} + +\item{t_pred}{Vector of time points. Chosen automatically if \code{NULL}.} + +\item{group_var}{grouping variable} + +\item{zero_log_z}{Use zero for all \code{log_z_*} parameters. This +effectively takes group means of all \code{HierOffsetTerm}s.} + +\item{prior_param_names}{Names of parameters for which prior draws should +be used. If \code{NULL} (default), these are attempted be to detected +automatically. These are usually all individual-specific parameters whose +values we do not know for "new" subjects so we draw them from the prior.} +} +\description{ +Generate predictions for new imaginary subjects from each group +} diff --git a/man/prior_sigma_informed.Rd b/man/prior_sigma_informed.Rd new file mode 100644 index 0000000..1bcf86b --- /dev/null +++ b/man/prior_sigma_informed.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/models-TSModel.R +\name{prior_sigma_informed} +\alias{prior_sigma_informed} +\title{Create a prior of noise parameter based on information about expected +amount of measurement error} +\usage{ +prior_sigma_informed(p = 0.065, p_sd = 0.015) +} +\arguments{ +\item{p}{A priori the most likely value of measurement error percentage} + +\item{p_sd}{Prior std in measurement error percentage} +} +\description{ +Create a prior of noise parameter based on information about expected +amount of measurement error +} diff --git a/man/sample_subjects.Rd b/man/sample_subjects.Rd new file mode 100644 index 0000000..174decc --- /dev/null +++ b/man/sample_subjects.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/main-treatment_effect.R +\name{sample_subjects} +\alias{sample_subjects} +\title{Draw one subjects from each group} +\usage{ +sample_subjects(data, id_var = "id", group_var = "arm") +} +\arguments{ +\item{data}{original data frame} + +\item{id_var}{name of the subject identifier variable} + +\item{group_var}{grouping variable} +} +\value{ +new data frame +} +\description{ +Draw one subjects from each group +} diff --git a/man/sfgp-package.Rd b/man/sfgp-package.Rd new file mode 100644 index 0000000..446a4f7 --- /dev/null +++ b/man/sfgp-package.Rd @@ -0,0 +1,39 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/aaa.R +\docType{package} +\name{sfgp-package} +\alias{sfgp-package} +\alias{sfgp} +\title{The 'sfgp' package.} +\description{ +SF+GP modeling using 'Stan'. +} +\section{Getting started}{ + +See the following \code{R6} classes. +\itemize{ + \item \code{\link{TSModel}}: Main model class. + \item \code{\link{TSModelFit}}: Fit class. + \item \code{\link{JointModel}}: Main model class. + \item \code{\link{JointModelFit}}: Fit class. + \item \code{\link{TermList}}: Class describing model terms. + \item \code{\link{FunctionDraws}}: Class to hold fitted function + distributions. +} +} + +\section{Data}{ + +The data that you wish to analyze with 'sfgp' should be in an \R +\code{data.frame} where columns correspond to measured variables and rows +correspond to observations. Categorical variables should be \code{factor}s +and continuous ones should be \code{numeric}. +} + +\author{ +Juho Timonen (first.last at iki.fi) +} +\keyword{Bayesian} +\keyword{GP} +\keyword{Stan} +\keyword{tumor} diff --git a/man/sfsim.Rd b/man/sfsim.Rd new file mode 100644 index 0000000..01dcbb7 --- /dev/null +++ b/man/sfsim.Rd @@ -0,0 +1,60 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/main-sim.R +\name{sfsim} +\alias{sfsim} +\title{Simulate from SF model (3 arms)} +\usage{ +sfsim( + n1 = 4, + n2 = 4, + n3 = 4, + ts0_scale = 20, + mu_log_ks = c(3, 1.5, -1), + mu_log_kg = c(-2, 0, 2), + times = c(0.5, 3, 6, 12, 24, 48)/48, + kg_sd = 0.05, + ks_sd = 0.05, + sigma = 0.2, + x_effect = 0, + log_C_kg = -2, + log_C_ks = -1, + t_sd = 0.02 +) +} +\arguments{ +\item{n1}{number of subjects in arm 1} + +\item{n2}{number of subjects in arm 2} + +\item{n3}{number of subjects in arm 3} + +\item{ts0_scale}{initial tumor sizes will be sampled from the uniform +distribution \code{ts0_scale * U(1,3)}} + +\item{mu_log_ks}{Mean log \code{k_s} parameters for the three arms.} + +\item{mu_log_kg}{Mean log \code{k_g} parameters for the three arms.} + +\item{times}{Time points where measurements are made. Scatter is added to +these.} + +\item{kg_sd}{Jitter added to kg.} + +\item{ks_sd}{Jitter added to ks.} + +\item{sigma}{Noise param value.} + +\item{x_effect}{Magnitude of effect of x on ks.} + +\item{log_C_kg}{offset for log kg} + +\item{log_C_ks}{offset for log ks} + +\item{t_sd}{Jitter added to time points.} +} +\value{ +a data frame +} +\description{ +Simulate from SF model (3 arms) +} diff --git a/man/stancode_ts.Rd b/man/stancode_ts.Rd new file mode 100644 index 0000000..fe03b59 --- /dev/null +++ b/man/stancode_ts.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/models-TSModel.R +\name{stancode_ts} +\alias{stancode_ts} +\title{Can be used to just generate Stan code for a TS model} +\usage{ +stancode_ts(formula, print = TRUE, ...) +} +\arguments{ +\item{formula}{model formula} + +\item{print}{Should the code be printed?} + +\item{...}{Arguments passed to the \code{$create_stancode()} method +of \code{\link{TSModel}}.} +} +\value{ +Stan code as character string (invisibly) +} +\description{ +Can be used to just generate Stan code for a TS model +} diff --git a/man/stanmodel_functions.Rd b/man/stanmodel_functions.Rd new file mode 100644 index 0000000..ec844f5 --- /dev/null +++ b/man/stanmodel_functions.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-misc.R +\name{stanmodel_functions} +\alias{stanmodel_functions} +\title{Create a 'Stan' model with only given functions in it} +\usage{ +stanmodel_functions(stan_file, ...) +} +\arguments{ +\item{stan_file}{Path to the Stan file relative to the directory +\code{inst/functions}, without the \code{.stan} suffix.} + +\item{...}{Optional arguments to \code{cmdstanr::cmdstan_model()}.} +} +\description{ +Create a 'Stan' model with only given functions in it +} diff --git a/man/term_to_code.Rd b/man/term_to_code.Rd new file mode 100644 index 0000000..ef0fd8e --- /dev/null +++ b/man/term_to_code.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-misc.R +\name{term_to_code} +\alias{term_to_code} +\title{Name of a term in the formula syntax to its Stan code name} +\usage{ +term_to_code(term) +} +\arguments{ +\item{term}{A string.} +} +\description{ +Name of a term in the formula syntax to its Stan code name +} diff --git a/man/testdata.Rd b/man/testdata.Rd new file mode 100644 index 0000000..725e899 --- /dev/null +++ b/man/testdata.Rd @@ -0,0 +1,30 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/data.R +\docType{data} +\name{testdata} +\alias{testdata} +\title{Simulated longitudinal test data} +\format{ +A data frame with 176 rows and 7 columns: +\describe{ + \item{id}{Subject id, factor} + \item{arm}{Trial arm, factor} + \item{sex}{Sex, factor} + \item{time}{Measurement time, numeric} + \item{weight}{Weight, numeric} + \item{y}{Response variable, numeric} + \item{f_true}{True signal} +} +} +\usage{ +testdata +} +\description{ +Simulated longitudinal test data +} +\seealso{ +Other built-in datasets: +\code{\link{example_data_jm}} +} +\concept{built-in datasets} +\keyword{datasets} diff --git a/man/trajectory_metrics.Rd b/man/trajectory_metrics.Rd new file mode 100644 index 0000000..4bcb431 --- /dev/null +++ b/man/trajectory_metrics.Rd @@ -0,0 +1,33 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/main-treatment_effect.R +\name{trajectory_metrics} +\alias{trajectory_metrics} +\title{Compute trajectory metrics.} +\usage{ +trajectory_metrics(trajectories, id_var, time_var) +} +\arguments{ +\item{trajectories}{An object of class \code{\link{FunctionDraws}}.} + +\item{id_var}{Name of id variable.} + +\item{time_var}{Name of the time variable.} +} +\value{ +The following metrics are computed for each draw for each id. +\itemize{ + \item \code{depth} = depth of response (RECIST) + \item \code{depth_br} = depth of response (best response definition, can + be negative) + \item \code{duration} = duration of response (RECIST) + \item \code{start} = start time of response (RECIST) + \item \code{end} = end time of response (RECIST) + \item \code{exists} = whether response exists (RECIST) + \item \code{endures} = whether response endures whole time interval (RECIST) + \item \code{ttb120} = Time to 1.2 x baseline (different duration metric). +} +See the \code{treatment-effect} vignette for definition of the metrics. +} +\description{ +Compute trajectory metrics. +} diff --git a/man/treatment_effect.Rd b/man/treatment_effect.Rd new file mode 100644 index 0000000..9ff4362 --- /dev/null +++ b/man/treatment_effect.Rd @@ -0,0 +1,38 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/main-treatment_effect.R +\name{treatment_effect} +\alias{treatment_effect} +\title{Analyse treatment effect} +\usage{ +treatment_effect( + fit_post, + fit_prior, + time_var = "t", + t_pred = NULL, + group_var = "arm", + method = "new_sub" +) +} +\arguments{ +\item{fit_post}{Posterior fit.} + +\item{fit_prior}{Prior fit. Needs to have same number of draws as +\code{fit_post}.} + +\item{time_var}{name of the time variable} + +\item{t_pred}{Vector of time points (in days). Chosen based on data range +if \code{t_pred=NULL}. Chosen to be an evenly spaced vector from +0 to at most 2 years every 8 weeks if \code{t_pred="auto"}.} + +\item{group_var}{grouping variable} + +\item{method}{Used method for analyzing the treatment effect. Can be either +\itemize{ + \item \code{new_sub}: Simulating new imaginary subjects + \item \code{group_est}: Using group-level parameter estimates +}} +} +\description{ +Analyse treatment effect +} diff --git a/sfgp.Rproj b/sfgp.Rproj new file mode 100644 index 0000000..497f8bf --- /dev/null +++ b/sfgp.Rproj @@ -0,0 +1,20 @@ +Version: 1.0 + +RestoreWorkspace: Default +SaveWorkspace: Default +AlwaysSaveHistory: Default + +EnableCodeIndexing: Yes +UseSpacesForTab: Yes +NumSpacesForTab: 2 +Encoding: UTF-8 + +RnwWeave: Sweave +LaTeX: pdfLaTeX + +AutoAppendNewline: Yes +StripTrailingWhitespace: Yes + +BuildType: Package +PackageUseDevtools: Yes +PackageInstallArgs: --no-multiarch --with-keep.source diff --git a/tests/testthat.R b/tests/testthat.R new file mode 100644 index 0000000..613a685 --- /dev/null +++ b/tests/testthat.R @@ -0,0 +1,4 @@ +library(testthat) +library(sfgp) + +test_check("sfgp") diff --git a/tests/testthat/test-FormulaSF.R b/tests/testthat/test-FormulaSF.R new file mode 100644 index 0000000..9339a89 --- /dev/null +++ b/tests/testthat/test-FormulaSF.R @@ -0,0 +1,29 @@ +test_that("creating an SFF term with formula works", { + dat <- sfsim() + form <- y ~ sff(t | ks ~ offset(id_ks) + gp(t_ks), kg ~ offset(id_kg) + gp(t_kg)) + m <- TSModel$new(form) + dat <- add_sff_input(dat, m) + term <- m$term_list$terms$f_log_sff_t + num_bf <- sample(10:20, 1) + fit <- m$fit(dat, chains = 1, num_bf = num_bf) + expect_true(inherits(term, "FormulaSFTerm")) + + # Check that term configuration works for terms inside FormulaSFTerm + expect_equal(fit$get_stan_data()$B_t_ks, num_bf) + + # TODO: Not working! + # conf_sff <- list(kg = list(f_gp_t_kg = list(scale_bf = 2.3))) + # conf2 <- list(f_log_sff_t = conf_sff) + # fit2 <- m$fit(dat, chains = 1, term_confs = conf2, scale_bf = NULL) + + plt <- fit$plot() + expect_s3_class(plt, "ggplot") + + # Plot inferred ks, kg as a function of time + f_t_ks <- fit$create_functiondraws(c("id_ks", "t_ks"), "ks") + f_t_kg <- fit$create_functiondraws(c("id_kg", "t_kg"), "kg") + plt_ks <- f_t_ks$log()$plot() # should depend on time + plt_kg <- f_t_kg$log()$plot() # should depend on time + expect_s3_class(plt_ks, "ggplot") + expect_s3_class(plt_kg, "ggplot") +}) diff --git a/tests/testthat/test-FormulaTerm.R b/tests/testthat/test-FormulaTerm.R new file mode 100644 index 0000000..a1dcfc2 --- /dev/null +++ b/tests/testthat/test-FormulaTerm.R @@ -0,0 +1,8 @@ +test_that("stancode creation for a GPTerm works", { + x <- GPTerm$new("x", NA) + expect_output(print(x), "f_gp_x") + code1 <- x$stancode_tdata(datanames = c("OBS")) + code2 <- x$stancode_tdata(datanames = c("OBS", "PRD")) + expect_gt(nchar(code1), 100) # 198? + expect_gt(nchar(code2), nchar(code1)) +}) diff --git a/tests/testthat/test-OffsetTerm.R b/tests/testthat/test-OffsetTerm.R new file mode 100644 index 0000000..d4e0f57 --- /dev/null +++ b/tests/testthat/test-OffsetTerm.R @@ -0,0 +1,7 @@ +test_that("different types of OffsetTerm work correctly", { + m <- TSModel$new(y ~ offset(id2 | arm) + gp(t)) + dat <- sfsim() + dat$id2 <- dat$id + fit <- m$fit(dat, chains = 1) + expect_s3_class(fit$plot(), "ggplot") +}) diff --git a/tests/testthat/test-TermList.R b/tests/testthat/test-TermList.R new file mode 100644 index 0000000..ea61598 --- /dev/null +++ b/tests/testthat/test-TermList.R @@ -0,0 +1,53 @@ +test_that("a TermList can be created", { + a <- create_termlist(y ~ gp(x) + sf(x), NULL) + expect_equal(a$length(), 2) + expect_output(a$print()) + expect_error(a$get_term("foo"), "is not a Stan variable name") + sn <- a$stan_names() + t <- a$get_term(sn[2]) + expect_match(t$stanname_base(), "f_log_sf_x") + expect_equal(length(a$stanfiles_functions()), 5) +}) + +test_that("creating Stan code from TermList works", { + a <- create_termlist(y ~ gp(x) + sf(x), NULL) + expect_match(a$stancode_data("OBS"), "f_gp_x") + code_tp <- a$stancode_tpars(c("OBS", "PRED")) + expect_output(cat(code_tp), "f_log_sf_x_PRED") + expect_output(cat(a$stancode_gq()), "f_sum") +}) + +test_that("creating Stan data for TermList works", { + a <- create_termlist(y ~ gp(x) + sf(x), NULL) + d <- data.frame(x = c(1, 2, 3, 4)) + expect_message(a$check_transforms(d), "[1, 4]") + a$set_transforms(d) + expect_message(a$check_transforms(d), "[-1, 1]") + expect_message(a$check_transforms(d), "[0.25, 1]") + + # Should work + r <- a$create_standata(data = d, dataname = "ASD") + expect_true("L_x" %in% names(r)) + + # cannot predict outside [-L, L] + d2 <- data.frame(x = c(1, 2, 3, 4, 10)) + expect_error( + a$create_standata(data = d2, dataname = "TEST"), + "GP approximation not valid" + ) +}) + +test_that("A custom prior can be given", { + prior_sf <- list(prior_ks = "normal(3, 4)") + prior_invalid <- list(prior_asd = "normal(3, 4)") + a <- create_termlist(y ~ sf(x), list(f_log_sf_x = prior_sf)) + expect_output(cat(a$stancode_model()), "3, 4") + expect_error( + create_termlist(y ~ sf(x), list(f_log_sf_x = prior_invalid)), + "cannot add bindings to a locked environment" + ) + expect_error( + create_termlist(y ~ sf(x), list(FOO = prior_sf)), + "is not a Stan variable name" + ) +}) diff --git a/tests/testthat/test-Transform.R b/tests/testthat/test-Transform.R new file mode 100644 index 0000000..53a2448 --- /dev/null +++ b/tests/testthat/test-Transform.R @@ -0,0 +1,41 @@ +test_that("IdentityTransform works correctly", { + x_test <- stats::rnorm(5) + r <- IdentityTransform$new() + a <- r$forward(x_test) + expect_true(all(a == x_test)) + b <- r$backward(x_test) + c <- r$backward(a) + expect_true(all(b == x_test)) + expect_true(all(c == x_test)) +}) + +test_that("LinearTransform works correctly", { + x_test <- stats::rnorm(5) + m <- rnorm(1) + sd <- 3.4 + r <- LinearTransform$new(multiplier = sd, offset = m) + expect_output(r$print(), "LinearTransform") + a <- r$forward(x_test) + b <- r$backward(a) + expect_lt(max(b - x_test), 1e-12) +}) + + +test_that("UnitScaleTransform works correctly", { + x_test <- stats::rnorm(5) + r <- UnitScaleTransform$new() + r <- r$set_using_data(x_test) + a <- r$forward(x_test) + expect_output(r$print(), "UnitScaleTransform") + expect_equal(max(a), 1.0) + expect_equal(min(a), -1.0) +}) + +test_that("MaxScaleTransform works correctly", { + x_test <- 100 * (2 + stats::rnorm(100)) + r <- MaxScaleTransform$new() + r <- r$set_using_data(x_test) + a <- r$forward(x_test) + expect_output(r$print(), "MaxScaleTransform") + expect_equal(max(a), 1.0) +}) diff --git a/tests/testthat/test-emax.R b/tests/testthat/test-emax.R new file mode 100644 index 0000000..e94f414 --- /dev/null +++ b/tests/testthat/test-emax.R @@ -0,0 +1,35 @@ +test_that("Emax term works", { + sim_emax <- function(x, e0, emax, ed50, gamma, sigma) { + xg <- x^gamma + f <- e0 + (emax * xg) / (ed50^gamma + xg) + y <- exp(f + sigma * rnorm(length(f))) + data.frame(x = x, f = f, y = y) + } + + gamma <- 10 + e0 <- 3 + x <- seq(0, 400, by = 4) + df1 <- sim_emax(x, e0, 3, 160, gamma, 0.25) + df2 <- sim_emax(x, e0, 3, 260, gamma, 0.25) + df3 <- sim_emax(x, e0, 3, 160, gamma, 0.25) + df4 <- sim_emax(x, e0, 3, 260, gamma, 0.25) + + df1$dose <- df1$x + df2$dose <- df2$x + df3$dose <- df3$x + df4$dose <- df4$x + + df <- rbind(df1, df2, df3, df4) + df$id <- as.factor(rep(1:4, each = nrow(df1))) + df$mutation <- as.factor(rep(rep(0:1, each = nrow(df1)), 2)) + + # Fitting a model + m <- TSModel$new(y ~ emax(x, mutation)) + fit <- m$fit(data = df, chains = 1) + + # Studying the fit + plt1 <- fit$function_draws()$log()$plot(facet_by = "id") + plt2 <- fit$plot(capped = FALSE, facet_by = "id") + expect_s3_class(plt1, "ggplot") + expect_s3_class(plt2, "ggplot") +}) diff --git a/tests/testthat/test-models-JointModel.R b/tests/testthat/test-models-JointModel.R new file mode 100644 index 0000000..576e3a6 --- /dev/null +++ b/tests/testthat/test-models-JointModel.R @@ -0,0 +1,48 @@ +test_that("creating and fitting a JointModel works", { + # Create model + tsm <- TSModel$new(y ~ gp(t), compile = F, id_var = "subject") + m <- JointModel$new(lon = tsm) + expect_output(print(m), "Longitudinal submodel") + + # Create data + t <- c(55, 55, 60, 50, 60, 60) / 60 + event <- as.logical(c(1, 1, 0, 1, 0, 0)) + subject <- as.factor(c(1, 2, 3, 4, 5, 6)) + dat_tte <- data.frame(subject, t, event) + dat_ts <- sfsim(n1 = 2, n2 = 2, n3 = 2) + colnames(dat_ts)[1] <- "subject" + dat_grid <- create_jm_grid(dat_tte, dat_ts, + num_points = 20, + id_var = "subject" + ) + + # Fit + fit <- m$fit( + data_lon = dat_ts, data_tte = dat_tte, data_grid = dat_grid, chains = 1 + ) + expect_output(print(fit), "JointModelFit") + + # Tests + plt1 <- fit$plot() + expect_s3_class(plt1, "ggplot") + expect_s3_class(fit$h0()$plot(), "ggplot") + + cum_haz <- fit$cumulative_hazard() + expect_equal(cum_haz$get_name(), "cum_haz") + + expect_s3_class(fit$plot_hazard(), "ggplot") + + # Predict + p <- fit$predict() + expect_output(print(p), "JointModelFit") + plt2 <- p$plot() + expect_s3_class(plt2, "ggplot") + + # Predict dense in time + t_test <- seq(0, 65, by = 2) / 60 + pp <- fit$predict_time(t_test) + + # Smooth plot + plt3 <- pp$function_draws()$plot() + expect_s3_class(plt3, "ggplot") +}) diff --git a/tests/testthat/test-models-StanModel.R b/tests/testthat/test-models-StanModel.R new file mode 100644 index 0000000..f1bdff7 --- /dev/null +++ b/tests/testthat/test-models-StanModel.R @@ -0,0 +1,6 @@ +test_that("creating a Stan model and Stan code for empty model works", { + sm <- StanModel$new(compile = FALSE) + sc <- sm$create_stancode() + expect_true(inherits(sm, "StanModel")) + expect_gt(nchar(sc), 100) +}) diff --git a/tests/testthat/test-models-TSModel.R b/tests/testthat/test-models-TSModel.R new file mode 100644 index 0000000..c231392 --- /dev/null +++ b/tests/testthat/test-models-TSModel.R @@ -0,0 +1,183 @@ +test_that("generating only the Stan code works", { + code1 <- stancode_ts(y ~ gp(x), print = FALSE) + expect_match(code1, "Term f_gp_x") + expect_gt(nchar(code1), 20) +}) + + +test_that("creating a TSModel works", { + m <- TSModel$new(hello ~ gp(foo) + gp(foo, bar)) + expect_output(m$print(), "TSModel") + expect_output(cat(m$term_list$latex()), "alpha") + expect_output(m$term_list$terms[[1]]$print()) + expect_equal(m$y_var, "hello") + expect_equal(m$term_list$length(), 3) + tn <- m$term_list$terms[[1]]$name_long() + expect_true(is.character(tn)) + expect_gt(nchar(tn), 3) +}) + +test_that("creating the Stan data works", { + m <- TSModel$new(hello ~ gp(foo) + gp(foo, bar), compile = F, id_var = "bar") + a <- data.frame( + foo = c(-100, 2, 3, 4), + hello = c(0, 3, 2, 1), + bar = as.factor(c(1, 1, 2, 2)) + ) + expect_error(m$fit(), "you need to call compile") + sbf <- 2.7 + confs <- m$term_list$fill_term_confs(num_bf = 2, scale_bf = sbf) + m$term_list$set_transforms(a) + sd <- m$create_standata(a, term_confs = confs) + sd <- sd$stan_data + expect_equal(sd$n_LON, 4) + expect_equal(sd$B_foo, 2) + expect_equal(max(sd$dat_foo_unit_LON), 1) + expect_equal(min(sd$dat_foo_unit_LON), -1) + expect_equal(sd$L_foo, sbf) + expect_equal(m$id_var, "bar") +}) + + +test_that("fitting a model and plotting function draws work", { + m <- TSModel$new(hello ~ gp(foo) + gp(foo, bar), delta = 0.05) + a <- data.frame( + foo = c(1, 2, 3, 4), + hello = c(0, 3, 2, 1), + bar = as.factor(c(1, 1, 2, 2)) + ) + num_bf <- 8 + fit <- m$fit(data = a, refresh = 0, num_bf = num_bf) + B_foo <- fit$term_confs[["f_gp_foo"]]$num_bf + expect_equal(B_foo, num_bf) + expect_equal(m$get_delta(), 0.05) + fd <- fit$function_draws() + f1 <- fit$function_draws("f_gp_foo") + f2 <- fit$function_draws("f_gp_fooXbar") + f3 <- f1 + f2 + f4 <- f3 - f1 + expect_output(fd$print(), "FunctionDraws") + expect_output(f1$print(), "FunctionDraws") + expect_output(f2$print(), "FunctionDraws") + expect_output(f3$print(), "FunctionDraws") + expect_output(f4$print(), "FunctionDraws") + expect_s3_class(fd$plot(), "ggplot") + expect_s3_class(f1$plot(), "ggplot") + expect_s3_class(f2$plot(), "ggplot") + expect_s3_class(f3$plot(), "ggplot") + expect_s3_class(f4$plot(), "ggplot") + expect_s3_class(fit$plot(), "ggplot") + plt <- fit$plot(f_reference = rep(1, 4)) + ll <- fit$loglik() + ep <- fit$measurement_error() + expect_equal(length(ll), nrow(a)) + expect_equal(length(ep), nrow(a)) + expect_s3_class(plt, "ggplot") + + df <- fit$fit_quality_summary() + expect_equal(nrow(df), 1) + expect_equal(ncol(df), 2) # colnames c("id", "error_perc") +}) + +test_that("the simplest sf() example works", { + # Simplest example + fit <- example(formula = "y~sf(x)") + expect_s3_class(fit$plot(), "ggplot") +}) + + +test_that("a more complex example works", { + r <- example( + formula = "y ~ sf(x) + gp(x)", + iter_warmup = 500, iter_sampling = 500, chains = 1 + ) + p1 <- r$plot() + p2 <- (r$function_draws(data_scale = FALSE) - r$function_draws("f_gp_x"))$plot() + p3 <- (r$function_draws("f_gp_x") + r$function_draws("f_baseline_id"))$plot() + expect_s3_class(p1, "ggplot") + expect_s3_class(p2, "ggplot") + expect_s3_class(p3, "ggplot") + p4 <- r$predict()$plot(predictive = FALSE, capped = FALSE) + expect_s3_class(p4, "ggplot") +}) + +test_that("simplest model with empty formula (only grouped offset) works", { + a <- TSModel$new(y ~ .) + r <- a$fit(testdata) + expect_equal(ncol(r$function_draws()$get_input()), 1) +}) + +test_that("example2() works with grouped sf and prior makes sense", { + a <- example2(formula = "y ~ sf(time, id)", prior_only = TRUE, chains = 2) + expect_s3_class(a$plot(), "ggplot") + f <- a$function_draws(data_scale = F) + fmax <- max(max(f$get_output())) + expect_lt(fmax, expected = Inf) # can still be huge + plt <- a$plot(filter_by = "arm", kept_vals = 1, group_by = "id", facet_by = "id") + expect_s3_class(plt, "ggplot") +}) + +test_that("grouped sf models work and predicting works", { + dat <- sfsim() + m1 <- TSModel$new(y ~ sf(t, id) + gp(t)) + dat1 <- add_sff_input(dat, m1) + + expect_output(cat(m1$term_list$latex()), "alpha") + f1 <- m1$fit(dat1, chains = 1) + + expect_s3_class(f1$plot(), "ggplot") + expect_error(m1$term_list$get_term("foo"), "is not a Stan variable name") + + pred1 <- f1$predict_time(seq(0, 1.2, by = 0.05)) + p1 <- pred1$plot(plot_y = FALSE) + + # Should be mean-zero + plt_gp <- pred1$function_draws("f_gp_t")$plot() + expect_s3_class(plt_gp, "ggplot") + + + # Check Stan data + t_range <- range(dat$t) + sd <- f1$get_stan_data() + expect_equal(max(sd$dat_t_unit_LON), 1) + expect_equal(min(sd$dat_t_unit_LON), -1) + sd_pred1 <- pred1$get_stan_data() + expect_gt(max(sd_pred1$dat_t_unit_LON), 1) +}) + +test_that("treatment effect estimation works", { + dat <- sfsim() + model <- TSModel$new(y ~ sf(t, id | arm) + gp(t) + gp(t, arm)) + dat <- add_sff_input(dat, model) + + fit_post <- model$fit(dat, chains = 1) + fit_prior <- model$fit(dat, chains = 1, prior_only = TRUE) + + # Treatment effect methods + te <- treatment_effect(fit_post, fit_prior, time_var = "t", group_var = "arm") + te2 <- treatment_effect(fit_post, fit_prior, + time_var = "t", group_var = "arm", method = "group_est" + ) + expect_s3_class(te$traj$plot(), "ggplot") + expect_s3_class(te2$traj$plot(), "ggplot") + + # SF Trajectories + sf1 <- te$p_new$function_draws("f_log_sff_t") + sf2 <- te2$p_new$function_draws("f_log_sff_t") + expect_s3_class(sf1$plot(), "ggplot") # these should be similar but + expect_s3_class(sf2$plot(), "ggplot") # this should have less variation + + # Depth of response + plt_dor <- plot_metric(te$metrics, "depth") + expect_s3_class(plt_dor, "ggplot") + + # Duration of response + plt_dur <- plot_metric(te$metrics, "duration") + expect_s3_class(plt_dur, "ggplot") +}) + + +test_that("prior_sigma_informed() works", { + pr <- prior_sigma_informed() + expect_equal(pr, "normal(0.06509, 0.01506)") +}) diff --git a/tests/testthat/test-sim.R b/tests/testthat/test-sim.R new file mode 100644 index 0000000..9c881ff --- /dev/null +++ b/tests/testthat/test-sim.R @@ -0,0 +1,34 @@ +test_that("data simulation works", { + dat <- sfsim() + expect_equal(ncol(dat), 9) + plt <- ggplot(dat, aes(x = t, y = f, group = id, color = arm)) + + geom_line() + + geom_point(aes(x = t, y = y, group = id), inherit.aes = F) + + facet_wrap(. ~ id) + expect_s3_class(plt, "ggplot") +}) + +test_that("sf-only makes sense for sim data of different scales", { + model <- TSModel$new(y ~ sf(t, id | arm)) + scales <- c(0.001, 0.1, 10, 10000) + plots_prior <- list() + plots_post <- list() + j <- 0 + for (s in scales) { + j <- j + 1 + set.seed(123) + tt <- paste0("ts0_scale = ", s) + dat <- sfsim(ts0_scale = s) + dat <- add_sff_input(dat, model) + fit_prior <- model$fit(dat, prior_only = TRUE, refresh = 0, chains = 1) + p1 <- fit_prior$plot(color_by = "arm_kg") + scale_y_log10() + ggtitle(tt) + fit_post <- model$fit(dat, refresh = 0, chains = 1) + p2 <- fit_post$plot(color_by = "arm_kg") + ggtitle(tt) + + plots_prior[[j]] <- p1 + plots_post[[j]] <- p2 + } + J <- length(scales) + expect_equal(length(plots_post), J) + expect_equal(length(plots_prior), J) +}) diff --git a/tests/testthat/test-stan-integrate.R b/tests/testthat/test-stan-integrate.R new file mode 100644 index 0000000..d6346b3 --- /dev/null +++ b/tests/testthat/test-stan-integrate.R @@ -0,0 +1,31 @@ +# test_that("expose_stan_functions() works", { +# # Expose Stan function to R +# b <- expose_stan_functions("hazard/integrate") +# +# # Test +# h <- 0.01 +# xx <- seq(0, 10, by = h) +# yy <- sin(xx) + 0.4 * sin(2.5 * xx) +# x <- seq(0, 10, by = 0.2) +# I_analytic <- -cos(x) - 0.16 * cos(2.5 * x) + 1.16 +# I_numeric <- b$functions$integrate_1d(x, xx, yy) +# mae <- max(abs(I_analytic - I_numeric)) +# expect_lt(mae, 0.001) +# +# # Integration for 3 subjects +# t_grid <- c(xx, xx, xx) +# t_out <- c(x, x, x) +# y_grid <- c(yy, yy + 2, yy + 1) +# G <- length(xx) +# N <- length(x) +# io <- list(c(1, N), c(N + 1, 2 * N), c(2 * N + 1, 3 * N)) +# ig <- list(c(1, G), c(G + 1, 2 * G), c(2 * G + 1, 3 * G)) +# I_numeric <- b$functions$integrate_1d_many(t_out, t_grid, y_grid, io, ig) +# i2 <- io[[2]] +# I2 <- I_numeric[i2[1]:i2[2]] +# +# # Test that integral is correct for subject 2 +# I2_analytic <- 2 * x - cos(x) - 0.16 * cos(2.5 * x) + 1.16 +# mae2 <- max(abs(I2_analytic - I2)) +# expect_lt(mae, 0.001) +# }) diff --git a/tests/testthat/test-trajectory_metrics.R b/tests/testthat/test-trajectory_metrics.R new file mode 100644 index 0000000..dc78e8d --- /dev/null +++ b/tests/testthat/test-trajectory_metrics.R @@ -0,0 +1,66 @@ +test_that("depth and duration are correct for an only-growing trajectory", { + t <- seq(0, 10, by = 0.1) + y <- 1 + sqrt(t) + depth <- depth_of_response(y) + dur <- duration_of_response(t, y) + br <- best_response(y) + expect_lt(br, 0) + expect_equal(depth, 0) + expect_equal(dur$start, 0) + expect_equal(dur$end, 0) + expect_false(dur$response_exists) + expect_false(dur$response_endures) + plt <- trajectory_plot(t, y) + expect_s3_class(plt, "ggplot") +}) + +test_that("depth and duration are correct for an only-shrinking trajectory", { + t <- seq(0, 10, by = 0.1) + y <- exp(1 - sqrt(t)) + depth <- depth_of_response(y) + dur <- duration_of_response(t, y) + br <- best_response(y) + expect_equal(depth, br) + expect_gt(depth, 0.95) + expect_lt(depth, 1) + ed <- (y[1] - min(y)) / (y[1]) + expect_equal(depth, ed) + expect_equal(dur$end - dur$start, 9.8) + expect_true(dur$response_endures) + expect_true(dur$response_exists) + plt <- trajectory_plot(t, y) + expect_s3_class(plt, "ggplot") + TTB120 <- ttb120(t, y) + expect_equal(TTB120, 10) +}) + +test_that("depth and duration are correct for a non-monotonic trajectory", { + t <- seq(0, 10, by = 0.1) + y <- exp(1 - sqrt(t) + sin(t)) + depth <- depth_of_response(y) + dur <- duration_of_response(t, y) + expect_equal(dur$start, 1.8) + expect_equal(dur$end, 5.6) + plt <- trajectory_plot(t, y) + expect_s3_class(plt, "ggplot") + expect_true(dur$response_exists) + expect_false(dur$response_endures) + expect_equal(depth, 0.9754317) + TTB120 <- ttb120(t, y) + expect_equal(TTB120, 10) +}) + + +test_that("depth and duration are correct for a regrowth trajectory", { + t <- seq(0, 10, by = 0.1) + y <- exp(1 - sqrt(t) + sin(t)) + 0.04 * t^2 + depth <- depth_of_response(y) + dur <- duration_of_response(t, y) + TTB120 <- ttb120(t, y) + expect_equal(TTB120, 8.7) + plt <- trajectory_plot(t, y) + expect_s3_class(plt, "ggplot") + expect_true(dur$response_exists) + expect_false(dur$response_endures) + expect_equal(round(depth, 5), 0.71295) +}) diff --git a/tests/testthat/test-utils-misc.R b/tests/testthat/test-utils-misc.R new file mode 100644 index 0000000..edc4de0 --- /dev/null +++ b/tests/testthat/test-utils-misc.R @@ -0,0 +1,4 @@ +test_that("stanmodel_functions works", { + sm <- stanmodel_functions("hazard/integrate") + expect_true(inherits(sm, "CmdStanModel")) +}) diff --git a/tests/testthat/test-utils-rvar.R b/tests/testthat/test-utils-rvar.R new file mode 100644 index 0000000..6adec1d --- /dev/null +++ b/tests/testthat/test-utils-rvar.R @@ -0,0 +1,16 @@ +test_that("rvar utils work", { + S <- 2000 + x <- posterior::rvar(rnorm(S, mean = 1, sd = 1)) + x0 <- rvar_set(x, 2.3) + x1 <- rvar_std_normal(x) + expect_equal(mean(x0)[1], 2.3) + expect_true(all(abs(mean(x1)) < 0.1)) + + n <- 4 + mu <- rep(1:n, each = S) + y <- posterior::rvar(array(rnorm(S * n, mean = mu, sd = 2), dim = c(S, n))) + y0 <- rvar_set(y, -5.4) + y1 <- rvar_std_normal(y) + expect_true(all(mean(y0) == -5.4)) + expect_true(all(abs(mean(y1)) < 0.1)) +}) diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R new file mode 100644 index 0000000..763194f --- /dev/null +++ b/tests/testthat/test-utils.R @@ -0,0 +1,19 @@ +test_that("extend_df2 works", { + dat <- testdata + t <- c(1, 3) + w <- 40 + df <- extend_df2(dat, t, "t", w, "weight", "id") + expect_true(all(df$w == 40)) + num_ids <- length(unique(dat$id)) + expect_true(nrow(df) == num_ids * length(t)) +}) + +test_that("extend_df2 works", { + dat <- testdata + t <- c(1, 3) + w <- 40 + df <- extend_df2(dat, t, "t", w, "weight", "id") + expect_true(all(df$w == 40)) + num_ids <- length(unique(dat$id)) + expect_true(nrow(df) == num_ids * length(t)) +}) diff --git a/vignettes/.gitignore b/vignettes/.gitignore new file mode 100644 index 0000000..097b241 --- /dev/null +++ b/vignettes/.gitignore @@ -0,0 +1,2 @@ +*.html +*.R diff --git a/vignettes/math.Rmd b/vignettes/math.Rmd new file mode 100644 index 0000000..a57a888 --- /dev/null +++ b/vignettes/math.Rmd @@ -0,0 +1,276 @@ +--- +title: "Mathematical description of SFGP models" +author: "Juho Timonen" +output: + rmarkdown::html_vignette: + toc: true + toc_depth: 3 + number_sections: true +vignette: > + %\VignetteIndexEntry{Mathematical description of SFGP models} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +```{r setup} +library(ggplot2) +library(sfgp) +``` + +# Longitudinal model + +This section describes mathematically the model corresponding to +the `TSModel` class. + +## Observation model +The likelihood of observation $i$ is +$$ +\log(y_i + \delta) \sim \mathcal{N}(f_i, \sigma^2), +$$ +where + +$$ +f_i = f(\mathbf{x}_i)= \sum_{j=1}^J f^{(j)}(\mathbf{x}_i) +$$ +is the expected log tumor size, $\sigma$ is an unknown parameter, and +the $\delta$ value is a constant. We use $y_i$ to denote the observed +longitudinal measurement, and $\mathbf{x}_i$ the corresponding covariate +vector. Typically, $\mathbf{x}_i$ includes a subject +identifier $\text{id}_i$, measurement time $t_i$, treatment arm $\text{arm}_i$ +and possible other factors or continuous covariates. +The functions $f^{(j)}$, $j=1, \ldots, J$ are the additive function +components and they are detailed next. + +## Model terms + + +```{r} +create_term <- function(term_str) { + m <- TSModel$new(y ~ ., baseline = term_str, compile = FALSE) + m$term_list$terms[[1]] +} +``` + +The model can have a different number $J$ of terms, depending on the +`formula` that is given. + +### `GroupedOffsetTerm` + +```{r} +term <- create_term("offset(g)") +print(term) +cat(term$latex()) +``` + +The `GroupedOffsetTerm` for a grouping variable $g$ is +$$ +f^{\text{BAS}} \left(\text{g} \mid \mathbf{c}_{0}\right) +$$ +where $\mathbf{c}_{0}$ is an unknown parameter vector containing one +parameter for each level of the grouping variable. These parameters have +independent priors. + +### `HierOffsetTerm` + +```{r} +term <- create_term("offset(g | h)") +print(term) +cat(term$latex()) +``` + +The `HierOffsetTerm` for grouping variables $z$ and $h$ is +$$ +f^{\text{BAS}} \left(\text{g}, \text{h} \mid \mathbf{c}_{0}\right) +$$ +where $\mathbf{c}_{0}$ is again an unknown parameter vector containing one +parameter for each level of the grouping variable. The difference to +`GroupedOffsetTerm` is that the prior is hierarchical. It is assumed that +for the value of $h = h[g]$ is always determined by $g$. So for example $g$ +can be the subject identifier and $h[g]$ the treatment arm for that subject. The +prior is parametrized so that for group $g$ the parameter is + +$$ +\mathbf{c}_{0, g} = z_{g} \cdot \sigma_{h[g]} + \mu_{h[g]} +$$ +where by default +$\mu_{h} \sim \mathcal{N}(0,1), \sigma_{h} \sim \mathcal{N}(0,5)$ independently +over all $h$ and $z_g \sim \mathcal{N}(0,1)$ independently over all $g$. + +```{r} +cat(term$stancode_model()) +``` + +### Shared GP terms (`GPTerm`) + +```{r} +term <- create_term("gp(x)") +print(term) +cat(term$latex()) +``` + +Let $x$ be some continuous variable. We use +$$ +f^{\text{GP}}(x \mid \alpha, \ell) \sim \mathcal{GP}(0,\ k_{\text{EQ}}(x,x' \mid \alpha, \ell)). +$$ +to denote that a function is modeled as a zero-mean Gaussian process (GP), +with the exponentiated quadratic kernel function +$$ +k_{\text{EQ}}(x, x' \mid \alpha, \ell) = \alpha^2 \exp\left( -\frac{(x-x')^2}{2\ell^2}\right), +$$ +where the parameters $\alpha > 0$ and $\ell > 0$ are the kernel magnitude and +lengthscale parameters, respectively. For GP terms, we use a Hilbert space +basis function approximation +$$ +f^{\text{GP}}(x \mid \alpha, \ell) \approx f^{\text{HSGP}}(x \mid \mathbf{\xi}, \alpha, \ell, B, L) = \sum_{b=1}^{B} \xi_b S_{\text{EQ}}\left(\sqrt{\lambda_{L,b}} \mid \alpha, \ell \right) \phi_{L,b}(x), +$$ + where the auxiliary parameters +$\mathbf{\xi} = \left\{\xi_1, \ldots, \xi_B\right\}$ are a priori standard +normal distributed $\xi_b \sim \mathcal{N}(0,1)$. This approximation approaches +the exact GP in the limit $L \rightarrow \infty$, $B \rightarrow \infty$. The +basis functions +$$ +\phi_{L,b}(x) = \frac{1}{\sqrt{L}} \sin \left( \frac{\pi b (x + L)}{2L}\right) +$$ +are eigenfunctions of a certain Dirichlet boundary value problem, +and $\lambda_{L, b} = \left(\frac{\pi b}{2 L} \right)^2$ are their +corresponding eigenvalues. The function +$$ +S_{\text{EQ}}(\omega \mid \alpha, \ell) = \alpha^2 \ell \sqrt{2 \pi} \exp \left(-\frac{\ell^2 \omega^2 }{2} \right) +$$ +is the spectral density of the exponentiated quadratic kernel.The approximation +domain size $L$ and number of basis functions $B$ are +constants that are set beforehand. The approximation is accurate only on +the interval $x \in (-L, L)$. + +The unknown parameters of a shared GP term are $\alpha, \ell$ and +$\mathbf{\xi} \in \mathbb{R}^B$. + +### Group-specific GP terms (`GPTerm`) + +```{r} +term <- create_term("gp(x, z)") +print(term) +cat(term$latex()) +``` + +Let $x$ be some continuous variable and $z$ some grouping factor, which can +without loss of generality take integer values $z \in \{1, \ldots, G\}$. A +group-specific GP term is +$$ +f^{\text{HSGP}}(x, z \mid \mathbf{\xi}_1, \ldots, \mathbf{\xi}_G, \alpha, \ell, B, L) = +\begin{cases} +f^{\text{HSGP}}(x \mid \mathbf{\xi}_1, \alpha, \ell, B, L) & \text{if } z = 1\\ +\vdots \\ +f^{\text{HSGP}}(x \mid \mathbf{\xi}_G, \alpha, \ell, B, L) & \text{if } z = G +\end{cases} +$$ +which approximates the GP +$$ +f^{\text{GP}}(x,z) \sim \mathcal{GP}(0,\ k_{\text{CAT}}(z,z') \cdot k_{\text{EQ}}(x,x')), +$$ +where $k_{\text{CAT}}(z,z') = 1$ if $z=z'$ and zero otherwise. The unknown +parameters of a group-specific GP term are $\alpha, \ell$ and a different vector +$\mathbf{\xi}_g \in \mathbb{R}^B$ for each group $g=1, \ldots, G$. + +### The Stein-Fojo term (`SFTerm`) + +```{r} +term <- create_term("sf(x)") +print(term) +cat(term$latex()) +``` + + +The Stein-Fojo (SF) function is +$$ +f^{\text{SF}}(x \mid k_g, k_s) = \exp(k_g x) + \exp(-k_s x) - 1, +$$ +where $k_g, k_s$ are unknown growth and shrinkage parameters, respectively. + +### `FormulaSFTerm` + +In the `FormulaSFTerm`, the parameters $k_g$ and $k_s$ can depend on other +covariates using all the other terms defined earlier. + +```{r} +term <- create_term("sff(t | kg ~ offset(id_kg) + gp(x_kg), ks ~ offset(id_ks))") +print(term) +cat(term$latex()) +cat(term$term_list_kg$latex()) +cat(term$term_list_ks$latex()) +``` + +The formulae defined for $k_g$ and $k_s$ mean that +$$ +k_{g}(\mathbf{x})) = \exp(\log(C_g) + f_{g}(\mathbf{x})) +$$ +and + +$$ +k_{s}(\mathbf{x})) = \exp(\log(C_s) + f_{s}(\mathbf{x})) +$$ +where $C_g, C_s$ are unknown parameters and $f_g$, $f_s$ are functions +defined by the formulae. + + +Short-hand for the basic hierarchical SF term is + +```{r} +term <- create_term("sf(t, id | arm)") +print(term) +cat(term$latex()) +cat(term$term_list_kg$latex()) +cat(term$term_list_ks$latex()) +``` + +## Examples + +### The SF-only model + +In the basic SF-only model with the formula we have +$\mathbf{x} = \{t, \text{id}, \text{arm}\}$, $J = 2$ and + +```{r} +mod1 <- TSModel$new(y ~ sf(t, id | arm), compile = FALSE) +cat(mod1$term_list$latex()) +``` + +\begin{align} +f^{(1)}(\mathbf{x}) &= f^{\text{log-SF}} \left(\text{t} \mid \mathbf{k}_{g}, \mathbf{k}_{s}\right)\\ + f^{(2)}(\mathbf{x}) &= f^{\text{BAS}} \left(\text{id} \mid \mathbf{c}_{0}\right) +\end{align}. + +Notice that in this case + +$$ +\exp(f(\mathbf{x})) = \exp\left(\log \text{c}_{0,\text{id}} + \log f^{\text{SF}}(t \mid k_{g, \text{id}}, \mid k_{s, \text{id}}) \right) = \text{c}_{0,\text{id}} \left( \exp(k_{g, \text{id}} \cdot t) + \exp(-k_{s, \text{id}} \cdot t) - 1 \right) +$$ +which is the original formulation of the SF tumor size model. + +### An SF+GP model + +An example of an SF+GP model with $\mathbf{x} = \{t, \text{id}, \text{arm}\}$, +$J = 4$ is + +```{r} +mod2 <- TSModel$new(y ~ sf(age, id | arm) + gp(age) + gp(age, arm), compile = FALSE) +cat(mod2$term_list$latex()) +``` + +It has the terms + +\begin{align} +f^{(1)}(\mathbf{x}) &= f^{\text{log-SF}} \left(\text{age} \mid \mathbf{k}_{g}, \mathbf{k}_{s}\right)\\ + f^{(2)}(\mathbf{x}) &= f^{\text{HSGP}} \left(\text{age} \mid \mathbf{\xi}_{\text{age}}, \alpha_{\text{age}}, \ell_{\text{age}}, B_{\text{age}}, L_{\text{age}}\right)\\ + f^{(3)}(\mathbf{x}) &= f^{\text{HSGP}} \left(\text{age}, \text{arm} \mid \mathbf{\xi}^{(1)}_{\text{age} \times \text{arm}}, \ldots, \mathbf{\xi}^{(G_{arm})}_{\text{age} \times \text{arm}}, \alpha_{\text{age} \times \text{arm}}, \ell_{\text{age} \times \text{arm}}, B_{\text{age} \times \text{arm}}, L_{\text{age} \times \text{arm}}\right)\\ + f^{(4)}(\mathbf{x}) &= f^{\text{BAS}} \left(\text{id} \mid \mathbf{c}_{0}\right) +\end{align} + +where $G_{\text{arm}}$ is the number of treatment arms. diff --git a/vignettes/models-formula-sf.Rmd b/vignettes/models-formula-sf.Rmd new file mode 100644 index 0000000..b37b266 --- /dev/null +++ b/vignettes/models-formula-sf.Rmd @@ -0,0 +1,217 @@ +--- +title: "SF model with different formulas for ks parameter" +output: + rmarkdown::html_vignette: + toc: true +vignette: > + %\VignetteIndexEntry{SF model with different formulas for ks parameter} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +```{r setup} +library(ggplot2) +library(sfgp) +library(dplyr) +``` + + +# Models + +```{r models, fig.width = 7.2, fig.height = 4.3} +form1 <- y ~ sff(t | ks ~ offset(id_ks | arm_ks) + gp(auc_ks), kg ~ offset(id_kg | arm_kg)) +form2 <- y ~ sff(t | ks ~ offset(id_ks | arm_ks) + emax(auc_ks, dummy_ks), kg ~ offset(id_kg | arm_kg)) +prior_bas <- list(prior_intercept = "normal(4,2)") +m1 <- TSModel$new(form1, prior_baseline = prior_bas) +m2 <- TSModel$new(form2, prior_baseline = prior_bas) +``` + +# Data +```{r dat1, fig.width = 7.2, fig.height = 4} +# Creates data with auc +create_data <- function(...) { + N <- 10 + dat <- sfsim( + n1 = N + 1, n2 = N, n3 = N - 1, kg_sd = 0.2, ks_sd = 0.2, + sigma = 0.2, ... + ) + dat$auc <- 400 * dat$x + dat$dummy <- as.factor(rep(1, nrow(dat))) + dat +} + +# Plot +plot_auc <- function(dat) { + p_exp <- ggplot(dat, aes(x = t, y = auc, group = id)) + + geom_line(color = "firebrick", alpha = 0.6) + + ggtitle("Exposure profiles") + + ylab("Exposure") + p_eff <- ggplot(dat, aes(x = auc, y = f_x_ks)) + + geom_line() + + ggtitle("True effect of exposure on ks") + + ylab("Effect on log(ks)") + + xlab("Exposure") + p_eff2 <- ggplot(dat, aes(x = t, y = f_x_ks, group = id)) + + geom_line(color = "firebrick", alpha = 0.6) + + ggtitle("True effect profiles") + + ylab("Effect on log(ks)") + ggpubr::ggarrange(p_exp, p_eff, p_eff2) +} + +ttt <- seq(0.01, 48, by = 4) / 48 +dat <- create_data(times = ttt, x_effect = 2) + +# Plot effect +plot_auc(dat) + +# Plot data +ggplot(dat, aes(x = t, y = y, group = id, color = arm)) + + geom_line() + + ylab("PSA") + + xlab("Normalized time") +``` +# Fits + +```{r fit1, fig.width = 7.2, fig.height = 4} +dat1 <- add_sff_input(dat, m1) +dat2 <- add_sff_input(dat, m2) +f1 <- m1$fit(dat1, chains = 1, refresh = 500) +f2 <- m2$fit(dat2, chains = 1, refresh = 500) + + +f_gp <- f1$create_functiondraws(c("auc", "auc_ks"), "f_gp_auc_ks") +f_emax <- f2$create_functiondraws(c("auc", "auc_ks"), "f_emax_auc_ksXdummy_ks") +plt_f <- ggpubr::ggarrange( + f_gp$plot() + theme(legend.position = "none") + ggtitle("GP"), + f_emax$plot() + theme(legend.position = "none") + ggtitle("Emax") +) +print(f2$draws("gamma")) +plt_f +``` + +# Expected PSA with given constant exposure + +```{r, fig.width=7, fig.height=3.5} +# Predict for all subjects given constant AUC, and take mean of each arm +predict_at_auc <- function(dat, fit, auc) { + t_test <- seq(0, 1, by = 0.05) + test_dat <- extend_df2(dat, t_test, "t", auc, "auc", "id") + capture.output({ + test_dat <- add_sff_input(test_dat, fit$get_model()) + p <- fit$predict(test_dat) + }) + df <- p$function_draws()$quantiles_df() + df <- aggregate(med ~ arm + t + auc, data = df, FUN = mean, na.rm = TRUE) + list( + df = df, + p = p + ) +} + +# Loop over range of AUC values +predict_at_auc_range <- function(dat, fit, AUC_range) { + df <- NULL + p <- list() + j <- 0 + for (auc in AUC_range) { + j <- j + 1 + message("* auc = ", auc) + pa <- predict_at_auc(dat, fit, auc = auc) + p[[j]] <- pa$p + df <- rbind(df, pa$df) + } + df$auc <- factor(df$auc, ordered = TRUE) + names(p) <- AUC_range + list(df = df, p = p) +} + +# Plot +plot_auc_range <- function(df) { + ggplot(df, aes(x = t, y = med, color = auc, group = auc)) + + geom_line(lwd = 1) + + facet_wrap(. ~ arm) + + ylab("PSA") + + ggtitle("Expected PSA by arm given exposure (auc)") + + xlab("Normalized time") + + scale_color_brewer(palette = 7, type = "div") +} + +AUC_range <- c(1, 100, 125, 150, 200, 400) +``` + +## GP model result + +```{r, fig.width=7, fig.height=3.5} +pa1 <- predict_at_auc_range(dat, f1, AUC_range) +plot_auc_range(pa1$df) +``` + +## Emax model result + +```{r, fig.width=7, fig.height=3.5} +pa2 <- predict_at_auc_range(dat, f2, AUC_range) +plot_auc_range(pa2$df) +``` + +# Depth and duration + + +```{r} +# Helper to get trajectories relative to start +get_traj <- function(p) { + p$function_draws(data_scale = F)$zero_offset_by_id("id")$exp() +} +# Helper to get trajectory metrics +get_tm <- function(p) { + trajectory_metrics(get_traj(p), "id", "t") +} + +# Combined data frame with all auc +summary_df <- function(p) { + df_list <- lapply(p, get_tm) + df <- dplyr::bind_rows(df_list, .id = "auc") + df$auc <- factor(df$auc, ordered = TRUE) + df +} +``` + +## GP model + +```{r} +df1 <- summary_df(pa1$p) # takes a while to compute all metrics +df1a <- dplyr::left_join(df1, dat[, c("id", "arm")], by = "id") +df1a %>% + group_by(arm, auc) %>% + summarize(mean_depth = mean(depth), mean_dur = mean(duration)) +``` + +## Emax model + +```{r} +df2 <- summary_df(pa2$p) # takes a while to compute all metrics +df2a <- dplyr::left_join(df2, dat[, c("id", "arm")], by = "id") +df2a %>% + group_by(arm, auc) %>% + summarize(mean_depth = mean(depth), mean_dur = mean(duration)) +``` + +# Used Stan code + +## Model with GP + +```{r} +cat(m1$create_stancode()) +``` + +## Model with sigmoid (Emax) + +```{r} +cat(m2$create_stancode()) +``` diff --git a/vignettes/models-manycomp-gp.Rmd b/vignettes/models-manycomp-gp.Rmd new file mode 100644 index 0000000..211ebc5 --- /dev/null +++ b/vignettes/models-manycomp-gp.Rmd @@ -0,0 +1,57 @@ +--- +title: "GP model with more variables" +author: "Juho Timonen" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{GP model with more variables} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +```{r setup} +library(ggplot2) +library(sfgp) +``` + +## Test data + +We load test data. +```{r data} +dat <- testdata +head(dat) +``` + +## GP model + +We create a GP model with many terms. + +```{r model} +m <- TSModel$new(y ~ gp(time) + gp(time, arm) + gp(time, id) + gp(time, sex) + gp(weight)) +``` + + +We fit the model. +```{r fit, fig.width=7, fig.height=6} +f <- m$fit(data = dat, chains = 1, iter_warmup = 600, iter_sampling = 600) +f$plot(facet_by = "id") +``` + +We visualize individual components +```{r plots, fig.width=7, fig.height=3.7} +f$function_draws("f_gp_time")$plot() +f$function_draws("f_gp_timeXarm")$plot() +f$function_draws("f_gp_timeXsex")$plot() +f$function_draws("f_gp_weight")$plot() +``` + +```{r plots2, fig.width=7, fig.height=6} +f$function_draws("f_gp_timeXid")$plot() +``` + diff --git a/vignettes/models-sf-plus-gp.Rmd b/vignettes/models-sf-plus-gp.Rmd new file mode 100644 index 0000000..4026669 --- /dev/null +++ b/vignettes/models-sf-plus-gp.Rmd @@ -0,0 +1,97 @@ +--- +title: "SF + GP model" +author: "Juho Timonen" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{SF + GP model} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +```{r setup} +library(ggplot2) +library(sfgp) +``` + +## Test data + +We create rapidly wiggling data. +```{r data} +set.seed(123) +xx <- seq(1, 8, by = 0.15) +ff <- 20 + 5 * sin(xx) + 2 * sin(5 * xx) + xx +yy <- ff + rnorm(n = length(xx), mean = 0, sd = 1) +a <- data.frame(x = xx, y = yy) +head(a) +``` + +## SF model + +This is probably the simplest possible SF model. +```{r model} +m1 <- TSModel$new(y ~ sf(x)) +``` + + +We fit the model. +```{r fits, fig.width=7, fig.height=4} +f1 <- m1$fit(data = a, chains = 1, iter_warmup = 300, iter_sampling = 300) +f1$plot() +``` + +## GP model + +This is probably the simplest possible GP model. +```{r model2} +m2 <- TSModel$new(y ~ gp(x)) +``` + + +We fit the model. +```{r fit2s, fig.width=7, fig.height=4} +f2 <- m2$fit(data = a, chains = 1, iter_warmup = 300, iter_sampling = 300) +f2$plot() +``` + + +## SF + GP model + +This is probably the simplest possible SF+GP model. +```{r model3} +m3 <- TSModel$new(y ~ sf(x) + gp(x)) +``` + + +We fit the model. +```{r fit3s, fig.width=7, fig.height=4} +f3 <- m3$fit(data = a, chains = 1, iter_warmup = 600, iter_sampling = 600) +f3$plot() +``` + +We plot components of the model + +```{r plotc, fig.width=7, fig.height=4} +f3$function_draws("f_gp_x")$plot() +f3$function_draws("f_log_sf_x")$plot() +``` + +## Predicting + +We can predict at new test points. + +```{r plots2, fig.width=7, fig.height=6} +t_pred <- seq(0, 9, by = 0.2) +pred <- f3$predict_time(t_pred, t_var = "x") +plt <- pred$plot(plot_y = FALSE) + + ggplot2::geom_point(data = a, inherit.aes = F, aes(x = x, y = y)) +plt +``` + + diff --git a/vignettes/models-simple-gp.Rmd b/vignettes/models-simple-gp.Rmd new file mode 100644 index 0000000..8af7db9 --- /dev/null +++ b/vignettes/models-simple-gp.Rmd @@ -0,0 +1,53 @@ +--- +title: "Simple GP model" +author: "Juho Timonen" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Simple GP model} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +```{r setup} +library(ggplot2) +library(sfgp) +``` + +This is probably the simplest possible GP model. +```{r model} +m <- TSModel$new(y ~ gp(x)) +m +``` + +We create rapidly wiggling data. +```{r data} +set.seed(123) +xx <- seq(1, 10, by = 0.15) +ff <- 20 + 5 * sin(xx) + 2 * sin(5 * xx) + xx +yy <- ff + rnorm(n = length(xx), mean = 0, sd = 1) +a <- data.frame(x = xx, y = yy) +head(a) +``` + +We fit the model using different numbers of basis functions (`num_bf`). +```{r fits, fig.width=7, fig.height=4} +B <- seq(12, 32, by = 4) +fitter <- function(b) { + fit <- m$fit(data = a, refresh = 0, num_bf = b, chains = 1) + fit$plot(f_reference = ff) + ggtitle(paste0("num_bf = ", b)) +} +plots <- lapply(as.list(B), fitter) +names(plots) <- paste0("num_bf = ", B) +plots +``` + +We notice that a too low number of basis functions is not enough to recover the +true signal well. + diff --git a/vignettes/models-survival-check.Rmd b/vignettes/models-survival-check.Rmd new file mode 100644 index 0000000..761ea53 --- /dev/null +++ b/vignettes/models-survival-check.Rmd @@ -0,0 +1,273 @@ +--- +title: "survival-model-check" +output: + rmarkdown::html_vignette: + toc: true + toc_depth: 3 + number_sections: true +author: Jacqueline Buros Novik and Juho Timonen +vignette: > + %\VignetteIndexEntry{survival-model-check} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +# Setup + +```{r setup} +# library(devtools) +# Sys.setenv(GITHUB_PAT = "") +# devtools::install_github("sambrilleman/simjm") +library(simjm) +library(sfgp) +library(ggplot2) +library(survminer) +library(survival) +library(tidyverse) +``` + +# Model + +```{r} +tsm <- TSModel$new(y ~ offset(z) + gp(t), compile = F) +m <- JointModel$new(lon = tsm) +print(m) + +format_lon_data <- function(data) { + dat_lon <- data$Long1[, c("id", "Z1", "Z2", "tij", "Yij_1")] + dat_lon$id <- as.factor(dat_lon$id) + dat_lon$Z1 <- as.factor(dat_lon$Z1) + colnames(dat_lon) <- c("id", "z", "x", "t", "y") + dat_lon +} +format_tte_data <- function(data) { + dat_tte <- data$Event[, c("id", "Z1", "Z2", "eventtime", "status")] + dat_tte$id <- as.factor(dat_tte$id) + dat_tte$Z1 <- as.factor(dat_tte$Z1) + dat_tte$status <- as.logical(dat_tte$status) + colnames(dat_tte) <- c("id", "z", "x", "t", "event") + dat_tte +} +format_data <- function(data, num_points = 30) { + dat_lon <- format_lon_data(data) + # truncating at values less than 0.01 + dat_lon$y <- if_else(dat_lon$y < 0.01, 0.01, dat_lon$y) + dat_tte <- format_tte_data(data) + list( + lon = dat_lon, + tte = dat_tte, + grid = create_jm_grid(dat_tte, dat_lon, + num_points = num_points, + id_var = "id" + ) + ) +} +plot_h0 <- function(fit) { + h0 <- fit$h0() + # We do this to avoid grouping by id in the plot + inp <- h0$get_input() + inp <- inp[, "t", drop = FALSE] + h0_new <- FunctionDraws$new(inp, h0$get_output(), h0$get_name()) + + h0_new$plot() +} +plot_params <- function(post_params, true_params) { + ggplot(post_params, aes(x = .value, group = .variable)) + + tidybayes::stat_halfeye() + + facet_wrap(~.variable, scale = "free") + + geom_vline(aes(xintercept = .value), data = true_params, color = "firebrick") +} +plot_corr <- function(post_params) { + ggplot( + post_params |> tidyr::spread(.variable, .value), + aes(x = assoc, y = h0_lambda) + ) + + geom_point() + + ggtitle("Correlation between posterior draws for assoc & h0_lambda") +} +``` + +# Scenario 1: No association, Intercept-only, Weibull hazard + +First we simulate data with no covariate effects on the hazard & no association. + +This is a Weibull model with an intercept only and gamma set to 1, so it's equivalent to an exponential model with constant hazard. + +The question is whether we can recover the shape parameter. + +```{r sim01, fig.width=7, fig.height=5} +params <- list(h0_gamma = 1, h0_lambda = 0.2) +data <- simjm( + assoc = "null", + n = 30, basehaz = "weibull", + fixed_trajectory = "linear", + random_trajectory = "none", + b_sd = 1, + betaEvent_intercept = c(log(params$h0_lambda)), + betaEvent_assoc = c(0), + betaEvent_binary = c(0), + betaEvent_aux = c(params$h0_gamma), + max_fuptime = 50, + betaEvent_continuous = c(0), + seed = 10 +) +params1 <- attr(data, "params") +plot(data) + +# plot simulated event data +sfit <- survfit(Surv(eventtime, status) ~ 1, data = data$Event) +ggsurvplot(sfit, data = data$Event) +``` + + +## Viewing the data + +Next, let's plot the raw longitudinal data + +```{r, fig.width=7, fig.height=5} +data$Long1 |> + ggplot(aes(x = tij, y = Yij_1, group = factor(id), colour = factor(id))) + + geom_line() +``` + + + +```{r} +dat <- format_data(data) +case1_fit <- fit <- m$fit( + data_lon = dat$lon, data_tte = dat$tte, data_grid = dat$grid, + chains = 1 +) +``` + +## Summarize results + + +### Baseline and instant hazard + +Note that the "assoc" term (and other betas) can absorb some of the "lambda" +term, particularly if predictors are not centered. + + +```{r, fig.width=7, fig.height=5} +plot_h0(fit) +fit$plot_hazard() +fit$plot(facet_by = "id") +``` + +### Association and parameters + +```{r, fig.width=7.2, fig.height=3} +print(fit$draws("assoc")) # association +true_params <- tibble::tibble(!!!params, assoc = 0) |> + tidyr::gather(".variable", ".value") +stan_fit <- fit$get_stan_fit() +post_params <- tidybayes::gather_draws(stan_fit, h0_lambda, h0_gamma, assoc) +plot_params(post_params, true_params) +``` + +Note that the "assoc" term (and other betas) can absorb some of the "lambda" +term, particularly if predictors are not centered. + +```{r, fig.width=5.2, fig.height=4} +plot_corr(post_params) +``` + +### Survival function + +```{r, fig.width=7, fig.height=5} +p <- fit$predict(data_tte = dat$grid) +p$survival_function()$plot() +``` + + +# Scenario 2: With association + +```{r sim02, fig.width=7, fig.height=5} +params <- list(h0_lambda = 0.01, h0_gamma = 1) +true_assoc <- 0.35 +data <- simjm( + n = 30, basehaz = "weibull", + fixed_trajectory = "linear", + random_trajectory = "none", + b_sd = 1, + betaEvent_intercept = c(log(params$h0_lambda)), + betaEvent_assoc = c(true_assoc), + betaEvent_binary = c(0), + betaEvent_continuous = c(0), + betaEvent_aux = c(params$h0_gamma), + max_fuptime = 50, + max_yobs = 20, + seed = 100 +) +print(attr(data, "params")) +plot(data) +# plot_traj(yvar = "Yij_1", data = data$Long1) +``` + +## Viewing the data + +Next, let's plot the raw longitudinal data + +```{r, fig.width=7, fig.height=5} +data$Long1 |> + ggplot(aes(x = tij, y = Yij_1, group = factor(id), colour = factor(id))) + + geom_line() +``` + + + +```{r} +dat <- format_data(data) +case2_fit <- fit <- m$fit( + data_lon = dat$lon, data_tte = dat$tte, data_grid = dat$grid, + chains = 1 +) +``` + +## Summarize results + + +### Baseline and instant hazard + + +```{r, fig.width=7, fig.height=5} +plot_h0(fit) +fit$plot_hazard() + scale_y_log10() +fit$plot(facet_by = "id") +``` + +### Association and parameters + +```{r, fig.width=7.2, fig.height=3} +print(fit$draws("assoc")) # association +true_params <- tibble::tibble(!!!params, assoc = true_assoc) |> + tidyr::gather(".variable", ".value") +stan_fit <- fit$get_stan_fit() +post_params <- tidybayes::gather_draws(stan_fit, h0_lambda, h0_gamma, assoc) +plot_params(post_params, true_params) +``` + +```{r, fig.width=5.2, fig.height=4} +plot_corr(post_params) +``` +### Survival function + +```{r, fig.width=7, fig.height=5} +p <- fit$predict(data_tte = dat$grid) +p$survival_function()$plot() +``` + + + +# Used Stan code +```{r} +cat(m$create_stancode()) +``` diff --git a/vignettes/models-survival.Rmd b/vignettes/models-survival.Rmd new file mode 100644 index 0000000..8b1db98 --- /dev/null +++ b/vignettes/models-survival.Rmd @@ -0,0 +1,136 @@ +--- +title: "survival-model" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{survival-model} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +## Setup + +```{r setup} +library(sfgp) +library(ggplot2) +``` + +## Data simulation + +The data has been simulated using `simjm`. Here we just load it. + +```{r sim, fig.width=7, fig.height=5} +# data <- simjm( +# n = 30, fixed_trajectory = "quadratic", +# max_fuptime = 15 +# ) +data <- example_data_jm +``` + +## Format data for sfgp + + +### Longitudinal data + +```{r format-lon} +dat_lon <- data$lon[, c("id", "Z1", "Z2", "tij", "Yij_1")] +dat_lon$id <- as.factor(dat_lon$id) +dat_lon$Z1 <- as.factor(dat_lon$Z1) +colnames(dat_lon) <- c("id", "z", "x", "t", "y") +head(dat_lon) +``` + +### Time-to-event data + +```{r format-tte} +dat_tte <- data$tte[, c("id", "Z1", "Z2", "eventtime", "status")] +dat_tte$id <- as.factor(dat_tte$id) +dat_tte$Z1 <- as.factor(dat_tte$Z1) +dat_tte$status <- as.logical(dat_tte$status) +colnames(dat_tte) <- c("id", "z", "x", "t", "event") +head(dat_tte) +``` + +### Numerical integration grid + +```{r grid} +dat_grid <- create_jm_grid(dat_tte, dat_lon, + num_points = 30, + id_var = "id" +) +``` + + +## Creating a Joint model + +```{r model} +tsm <- TSModel$new(y ~ gp(t) + offset(z) + gp(x), compile = F) +m <- JointModel$new(lon = tsm) +print(m) +``` + +## Fitting a model + +```{r fit, fig.width=7, fig.height=6} +fit <- m$fit( + data_lon = dat_lon, data_tte = dat_tte, data_grid = dat_grid, + chains = 1, iter_warmup = 800, iter_sampling = 600 +) +``` + + +## Visualizing the fit + +### Longitudinal model + +```{r, fig.width=7, fig.height=6} +fit$plot() + facet_wrap(. ~ id) +``` + +The shared GP component +```{r, fig.width=7, fig.height=4} +fit$function_draws("f_gp_t")$plot() +``` + +### Instant hazard + +```{r, fig.width=7, fig.height=6} +print(fit$draws("assoc")) # association +print(fit$draws("h0_lambda")) # hazard function param +print(fit$draws("h0_gamma")) # hazard function param +fit$plot_hazard(plot_events = FALSE) +``` + +### Baseline hazard + +```{r, fig.width=7, fig.height=6} +h0 <- fit$h0() + +# We do this to avoid grouping by id in the plot +inp <- h0$get_input() +inp <- inp[, "t", drop = FALSE] +h0_new <- FunctionDraws$new(inp, h0$get_output(), h0$get_name()) + +h0_new$plot() +``` + + +## Predicting survival + +```{r, fig.width=7, fig.height=6} +dat_pred <- dat_grid +p <- fit$predict(data_tte = dat_pred) + +# H(t) +p$cumulative_hazard()$plot() + +# exp(-H(t)) +surv <- p$survival_function(NULL) +surv$plot() +``` diff --git a/vignettes/references.bib b/vignettes/references.bib new file mode 100644 index 0000000..3355efc --- /dev/null +++ b/vignettes/references.bib @@ -0,0 +1,13 @@ +@article{johnson2020, + author = {Johnson, Kaitlyn E. and Gomez, Axel and Burton, Jackson and White, Douglas and Chakravarty, Arijit and Schmid, Annette and Bottino, Dean}, + title = "{Abstract 11: An alternative time-to-event metric that is consistent with tumor response speed and depth, unlike RECIST time to progression}", + journal = {Clinical Cancer Research}, + volume = {26}, + pages = {11-11}, + year = {2020}, + month = {06}, + issn = {1078-0432}, + doi = {10.1158/1557-3265.ADVPRECMED20-11}, + url = {https://doi.org/10.1158/1557-3265.ADVPRECMED20-11}, +} + diff --git a/vignettes/treatment-effect.Rmd b/vignettes/treatment-effect.Rmd new file mode 100644 index 0000000..1707aca --- /dev/null +++ b/vignettes/treatment-effect.Rmd @@ -0,0 +1,296 @@ +--- +title: "Assessing the effect of a treatment with SFGP" +author: "Juho Timonen" +output: + rmarkdown::html_vignette: + toc: true + toc_depth: 3 + number_sections: true +vignette: > + %\VignetteIndexEntry{Assessing the effect of a treatment with SFGP} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +bibliography: references.bib +--- + + +# Setup + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + + +```{r setup} +library(ggplot2) +library(sfgp) +library(dplyr) +``` + +# Simulated data + +```{r sim, fig.width = 7.2, fig.height = 4} +set.seed(10) +dat <- sfsim() +head(dat) +plt1 <- ggplot(dat, aes(x = t, y = f, group = id, color = arm)) + + geom_line() +plt2 <- ggplot(dat, aes(x = t, y = y, group = id, color = arm)) + + geom_line() + + geom_point() +plt1 +plt2 +``` + +# Fitting the model + +```{r fit, fig.width = 7.2, fig.height = 3.3} +model <- TSModel$new(y ~ sf(t, id | arm) + gp(t) + gp(t, arm)) +dat <- add_sff_input(dat, model) +fit_post <- model$fit(dat, chains = 1, refresh = 1000) +fit_prior <- model$fit(dat, chains = 1, refresh = 1000, prior_only = TRUE) +``` + +# Treatment effect + +## Trajectories for new subjects + +We now assume that we have $G_{\text{arm}} \geq 2$ treatment arms, and +a model of the form + +$$ +f(\mathbf{x})= \sum_{j=1}^J f^{(j)}(\mathbf{x}) +$$ +where $J \geq 2$, and + +$$ +\begin{align} +f^{(1)}(\mathbf{x}) &= f^{\text{BAS}} \left(\text{id} \mid \mathbf{c}_{0}\right) \\ +f^{(2)}(\mathbf{x}) &= f^{\text{log-SF}} \left(t, \text{id}, \text{arm} \mid \mathbf{k}_{g}, \mathbf{k}_{s}\right) +\end{align}, +$$ +and the possible remaining terms $f^{(j)}$, $j=1, \ldots, J$ do not depend on +$\text{id}$. We can divide the covariates as +$\mathbf{x} = \left\{t, \text{id}, \text{arm}, \mathbf{x}'\right\}$, +where $\mathbf{x}'$ are the possible remaining covariates. In the following, +we assume that $\mathbf{x}' = \emptyset$, i.e. there are no other covariates. + +## Generating the trajectories + +For each treatment arm $a \in \left\{1, \ldots, G_{\text{arm}}\right \}$, +we generate trajectories $f_a(t^*)$, $t \in T$ that represent +new imaginary subjects from arm $a$. + +The trajectories are generated by drawing +the components so that any individual-specific parameters are drawn from +prior and posterior draws are used for the remaining parameters. Finally, +for each trajectory, a constant is added +so that they each trajectory has $f_a(0) = 0$. + + +## Exponentiated trajectories + +Before computing trajectory metrics, the trajectories are exponentiated as +$g_a(t^*) = \exp\left(f_a(t^*)\right)$, meaning that $g_a(0) = 1$. Therefore, +the trajectories describe growth of tumor relative to the baseline size. + +```{r te, fig.width = 7.2, fig.height = 3.3} +# "New" subjects +te <- treatment_effect(fit_post, fit_prior, time_var = "t", group_var = "arm") + +te$traj$plot() +``` + +## Depth of response + +### Definition +For a given trajectory $g = \exp(f)$ on time interval interval $[0, T]$, +the depth of response is defined as + +$$ +\text{DoR}_{T}\left(g\right) = \frac{g(0) - \min_{t \in [0, T]} g(t)}{g(0)} +$$ + +### Example +```{r dor, fig.width = 7.2, fig.height = 3.3} +# Depth of response +dor <- te$metrics %>% + group_by(arm) %>% + summarize(mean = mean(depth)) +print(dor) +plot_metric(te$metrics, "depth") +``` + +## Duration of response + +### Definition +For a given trajectory $g$ on time interval interval $[0, T]$, +the duration of response is defined as + +$$ +\text{DuR}_{T}\left(g\right) = t^{\text{end}}_T(g) - t^{\text{start}}_T(g) +$$ +where the start time of response $t^{\text{start}}_T(g)$ is the smallest +$t \in [0, T]$ that satisfies +$$ +g(t) \leq 0.7 \cdot g(0) +$$ +and the response end time $t^{\text{end}}_T(g)$ is +the smallest $t \in \left(t^{\text{start}}_T(g), T\right]$ that satisfies +$$ +g(t) \geq 1.2 \cdot \min_{t' \in [0, t)} g(t'). +$$ +In other words, the start time is the first time +point where the trajectory is at least $30\%$ lower than the start value, +and end point is the first time point after the start point where the +trajectory is at least +$20\%$ higher than the minimum until that time point. If the start point does +not exist, then $\text{DuR}_{T}\left(g\right) = 0$. If the end point does not +exist but start point does, then +$$ +\text{DuR}_{T}\left(g\right) = T - t^{\text{start}}_T(g). +$$ + +### TTB120 + +An alternative metric is time to $120\%$ of baseline (@johnson2020). It is +defined as +$$ +\text{TTB120}_T \left( g \right) = \arg \min_{t \in (0, T)} \left\{g(t) \geq 1.2 \cdot g(0) \right\}. +$$ +and $T$ if the condition is not satisfied on the interval $(0, T)$. + +### Example +```{r dur, fig.width = 7.2, fig.height = 3.3} +# Duration of response +dur <- te$metrics %>% + group_by(arm) %>% + summarize(mean = mean(duration)) +print(dur) +plot_metric(te$metrics, "duration") +plot_metric(te$metrics, "ttb120") +``` + +We can compute the metrics only for the trajectories for which the +response exists like so + +```{r durfilt} +# Compute proportion of trajectories where response exists +p_responding <- te$metrics %>% + group_by(arm) %>% + summarize(mean = mean(exists)) +print(p_responding) + +# Duration of response, for those new subjects that are responding +te$metrics %>% + group_by(arm) %>% + filter(exists) %>% + summarize(mean = mean(duration)) +``` + +## Distribution of the difference to control + +We can plot the difference to control arm. + +```{r diff_control} +# TODO +``` + +## Probability of difference to control + +This is how we can calculate + +$$ +P(x_a > a_b \mid x_a \neq x_b) +$$ +where $x_a, x_b$ are the values of some metric for arms $a$ and $b$, respectively. + +```{r p_diff_control} +# TODO +``` + +## Invariance of metrics under trajectory scaling + +Assume that $f^*(t) = f(t) + \log C$. Then + +$$ +g^*(t) = \exp(f^*(t)) = \exp(f(t) + \log C) = g(t)\cdot C +$$ +and +$$ +\begin{align} +\text{DoR}_{T}\left(g^*\right) &= \frac{g(0)\cdot C - \min_{t \in [0, T]} g(t) \cdot C}{g(0) \cdot C}\\ +&= \frac{g(0) - \min_{t \in [0, T]} g(t)}{g(0)} \\ +&= \text{DoR}_{T}\left(g\right) +\end{align} +$$ + +Furthermore, the inequalities in the definition of response duration and TTB120 +are equivalent for $g$ and $g^*$, which can be seen by multiplying both sides +by $C$. The response depth, duration, and TTB120 of a trajectory are +therefore not affected by multiplying by a constant, +which is done when we move the trajectories to start from 1. + +## Using group-level mean trajectories + +In this method, the trajectories are generated otherwise the same as above, +but we generate each trajectory draw using +the group-level means of the parameters $k_s$, $k_g$ +(i.e. set $\log(z) = 0$, see the SF term +parametrization in the \code{math} vignette). + +```{r te2, fig.width = 7.2, fig.height = 3.3} +# Group-level mean trajectories +te2 <- treatment_effect(fit_post, fit_prior, + time_var = "t", group_var = "arm", + method = "group_est" +) + +te2$traj$plot() + +# Depth of response +dor2 <- te2$metrics %>% + group_by(arm) %>% + summarize(mean = mean(depth)) +print(dor2) +plot_metric(te2$metrics, "depth") +``` + +## Using an alternative definition of depth + +In this method, we define actual expected measurement times in `t_depth`, +and then compute the depth based on a "best response" which is the minimum +tumor size after baseline, at these expected measurement times. + +So for a given trajectory $g = \exp(f)$, baseline time point $t_0$, and +a vector of post-baseline time points $\textbf{t}$, the best response depth is + +$$ +\text{BR}_{t_0, \textbf{t}}\left(g\right) = \frac{g(t_0) - \min_{t \in \textbf{t}} g(t_0)}{g(t_0)} +$$ +which will be negative for trajectories that never go below baseline. + +```{r te3, fig.width = 7.2, fig.height = 3.3} +t_pred <- seq(0, 46, by = 1 * 7) / 46 # expected measurement every week +te3 <- treatment_effect(fit_post, fit_prior, + time_var = "t", group_var = "arm", + t_pred = t_pred +) + +te3$traj$plot() + +# Depth of response +dor3 <- te3$metrics %>% + group_by(arm) %>% + summarize(mean = mean(depth_br)) +print(dor3) +plot_metric(te3$metrics, "depth_br") +``` + +# References + + +