Skip to content

Commit 8592e78

Browse files
authored
Merge pull request #46 from Merck/likepsm2
Extend compare_psm_likes() to cover splines models
2 parents d643f69 + f7e72c5 commit 8592e78

File tree

4 files changed

+218
-34
lines changed

4 files changed

+218
-34
lines changed

DESCRIPTION

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Description: Fits and evaluates three-state partitioned survival analyses
1010
(PartSAs) and Markov models (clock forward or clock reset) to
1111
progression and overall survival data typically collected in oncology clinical trials. These model structures are typically considered in
1212
cost-effectiveness modeling in advanced/metastatic cancer indications.
13-
Muston (2024). "Informing structural assumptions for three state oncology cost-effectiveness models through model efficiency and fit". Applied Health Economics and Health Policy. DOI 10.1007/s40258-024-00884-2
13+
Muston (2024). "Informing structural assumptions for three state oncology cost-effectiveness models through model efficiency and fit". Applied Health Economics and Health Policy.
1414
License: GPL (>= 3)
1515
URL: https://merck.github.io/psm3mkv/, https://github.com/Merck/psm3mkv
1616
BugReports: https://github.com/Merck/psm3mkv/issues

R/likepsm.R

+211-28
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,28 @@
1+
# Copyright (c) 2023 Merck & Co., Inc., Rahway, NJ, USA and its affiliates.
2+
# All rights reserved.
3+
#
4+
# This file is part of the psm3mkv program.
5+
#
6+
# psm3mkv is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU General Public License as published by
8+
# the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# This program is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU General Public License
17+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
#
19+
# ==================================================================
20+
# Functions relating to comparing likelihoods of parametric and splines PSMs
21+
# - compare_psm_likes()
22+
# - compare_psm_likes_par()
23+
# - compare_psm_likes_spl()
24+
# ==================================================================
25+
126
#' Compare likelihoods of PSMs
227
#'
328
#' Compare the total log-likelihood values for the patient-level dataset after fitting PSM-simple and PSM-complex models to each combination of endpoint distributions
@@ -6,66 +31,77 @@
631
#' @importFrom rlang .data
732
#' @return List containing
833
#' - `res`: Dataset of calculation results for each model
9-
#' - `ind_aic`: Set of statistical distributions for TTP, PFS and OS which individually fit each endpoint with the best (lowest) AIC
10-
#' - `ind_bic`: Set of statistical distributions for TTP, PFS and OS which individually fit each endpoint with the best (lowest) BIC
11-
#' - `jt_aic`: Set of statistical distributions for TTP, PFS and OS which overall fit a PSM with the best (lowest) AIC
12-
#' - `jt_bic`: Set of statistical distributions for TTP, PFS and OS which overall fit a PSM with the best (lowest) BIC
34+
#' - `best`: Tibble indicating which is the best fitting model individually or jointly, to each endpoint, according to AIC or BIC
1335
#' @export
1436
#' @examples
1537
#' # Fit parametric distributions to a dataset
1638
#' bosonc <- create_dummydata("flexbosms")
1739
#' parfits <- fit_ends_mods_par(bosonc)
40+
#' splfits <- fit_ends_mods_spl(bosonc)
1841
#' # Present comparison of likelihood calculations
42+
#' \donttest{
1943
#' compare_psm_likes(bosonc, parfits)
44+
#' compare_psm_likes(bosonc, splfits)
45+
#' }
2046
compare_psm_likes <- function(ptdata, fitslist, cuttime=0) {
47+
# Determine whether fit is a spline, then call the right function
48+
if (fitslist$pfs[[1]]$result$dlist$name=="survspline") {
49+
compare_psm_likes_spl(ptdata, fitslist, cuttime)
50+
} else {
51+
compare_psm_likes_par(ptdata, fitslist, cuttime)
52+
}
53+
}
54+
55+
# Compare likelihoods of PSMs for parametric models
56+
compare_psm_likes_par <- function(ptdata, fitslist, cuttime=0) {
2157
# Check that fitslist is a list of 6 endpoints
2258
if (length(fitslist)!=6) {stop("The list provided to fitslist must contain all 6 endpoints")}
2359
# Create local variables
2460
eps <- ndists <- aic_indbest <- bic_indbest <- bests <- res <- thisfit <- aic_jtbest <- bic_jtbest <- NULL
2561
ll <- rank_aic <- ttp_meth <- pfs_dist <- os_dist <- rank_bic <- NULL
26-
# TTP, PFS and OS are endpoints 1, 3 and 4
2762
eps <- c(1, 3, 4)
28-
# Number of distributions for each endpoint
29-
ndists <- eps |>
30-
purrr::map_vec(~length(fitslist[[.x]]))
31-
# Best fits for each endpoint - AIC
32-
aic_indbest <- eps |>
33-
purrr::map_vec(~find_bestfit(fitslist[[.x]], crit="aic")$fit$dlist$name)
34-
# Best fits for each endpoint - BIC
35-
bic_indbest <- eps |>
36-
purrr::map_vec(~find_bestfit(fitslist[[.x]], crit="bic")$fit$dlist$name)
37-
# Join as a tibble
38-
bests <- rbind(aic_indbest, bic_indbest)
63+
# Best fits by AIC or BIC
64+
fits_aic <- eps |>
65+
purrr::map(~find_bestfit(fitslist[[.x]], crit="aic")$fit)
66+
fits_bic <- eps |>
67+
purrr::map(~find_bestfit(fitslist[[.x]], crit="bic")$fit)
68+
fits_all <- c(fits_aic, fits_bic)
69+
# Pull out names/distributions
70+
fits_names <- seq(2*length(eps)) |>
71+
purrr::map_vec(~fits_all[[.x]]$dlist$name)
72+
# Summarize parametric results in a table
3973
bests <- tibble::tibble(
40-
ttp_meth = bests[,1],
41-
pfs_dist = bests[,2],
42-
os_dist = bests[,3],
74+
ttp_meth = fits_names[c(1, 4)],
75+
pfs_dist = fits_names[c(2, 5)],
76+
os_dist = fits_names[c(3, 6)],
4377
meth = "ind",
4478
ic = c("aic", "bic")
4579
)
4680
# Create results table for each model combination
81+
ndists <- eps |>
82+
purrr::map_vec(~length(fitslist[[.x]]))
4783
res <- tibble::tibble(
4884
id = 1:(ndists[3]*ndists[2]*(ndists[1]+1)),
4985
ttp_meth = NA,
5086
pfs_dist = NA,
5187
os_dist = NA,
5288
ll = NA,
5389
npar = NA,
54-
npts = fitslist$os[[1]]$result$N
90+
npts = NA
5591
)
5692
# Create a safe calculation of the PSM-simple likelihood (returns NA on error)
5793
slike_simple <- purrr::possibly(
5894
~calc_likes_psm_simple(
5995
ptdata=ptdata,
6096
dpam=.x,
61-
cuttime=cuttime)$ll[2],
97+
cuttime=cuttime),
6298
otherwise = NA)
6399
# Create a safe calculation of the PSM-complex likelihood (returns NA on error)
64100
slike_complex <- purrr::possibly(
65101
~calc_likes_psm_complex(
66102
ptdata=ptdata,
67103
dpam=.x,
68-
cuttime=cuttime)$ll[2],
104+
cuttime=cuttime),
69105
otherwise = NA)
70106
# Compute results for PSM-simple models
71107
message("Calculating PSM simple")
@@ -78,8 +114,12 @@ compare_psm_likes <- function(ptdata, fitslist, cuttime=0) {
78114
res$ttp_meth[resrow] <- "simple"
79115
res$pfs_dist[resrow] <- thisfit$pfs$dlist$name
80116
res$os_dist[resrow] <- thisfit$os$dlist$name
81-
res$ll[resrow] <- slike_simple(thisfit)
82-
res$npar[resrow] <- thisfit$pfs$npars + thisfit$os$npars + 1
117+
psmsimple <- slike_simple(thisfit)
118+
if (is.na(psmsimple)[1]==FALSE) {
119+
res$ll[resrow] <- psmsimple$ll[2]
120+
res$npar[resrow] <- thisfit$pfs$npars + thisfit$os$npars + 1
121+
res$npts[resrow] <- psmsimple$npts[2]
122+
}
83123
}
84124
}
85125
# Compute results for PSM-complex models
@@ -95,8 +135,12 @@ compare_psm_likes <- function(ptdata, fitslist, cuttime=0) {
95135
res$ttp_meth[resrow] <- thisfit$ttp$dlist$name
96136
res$pfs_dist[resrow] <- thisfit$pfs$dlist$name
97137
res$os_dist[resrow] <- thisfit$os$dlist$name
98-
res$ll[resrow] <- slike_complex(thisfit)
99-
res$npar[resrow] <- thisfit$ttp$npars + thisfit$pfs$npars + thisfit$os$npars
138+
psmcomplex <- slike_complex(thisfit)
139+
if (is.na(psmcomplex)[1]==FALSE) {
140+
res$ll[resrow] <- psmcomplex$ll[2]
141+
res$npar[resrow] <- thisfit$ttp$npars + thisfit$pfs$npars + thisfit$os$npars
142+
res$npts[resrow] <- psmcomplex$npts[2]
143+
}
100144
}
101145
}
102146
}
@@ -114,8 +158,8 @@ compare_psm_likes <- function(ptdata, fitslist, cuttime=0) {
114158
best_bic = 0
115159
)
116160
# Identify best AIC and best BIC model
117-
res$best_aic[res$ttp_meth==aic_indbest[1] & res$pfs_dist==aic_indbest[2] & res$os_dist==aic_indbest[3]] <- 1
118-
res$best_bic[res$ttp_meth==bic_indbest[1] & res$pfs_dist==bic_indbest[2] & res$os_dist==bic_indbest[3]] <- 1
161+
res$best_aic[res$ttp_meth==bests$ttp_meth[1] & res$pfs_dist==bests$pfs_dist[1] & res$os_dist==bests$os_dist[1]] <- 1
162+
res$best_bic[res$ttp_meth==bests$ttp_meth[2] & res$pfs_dist==bests$pfs_dist[2] & res$os_dist==bests$os_dist[2]] <- 1
119163
# Identify best distributions for overall AIC and BIC
120164
aic_jtbest <- res |>
121165
dplyr::filter(rank_aic==1) |>
@@ -124,7 +168,146 @@ compare_psm_likes <- function(ptdata, fitslist, cuttime=0) {
124168
bic_jtbest <- res |>
125169
dplyr::filter(rank_bic==1) |>
126170
dplyr::select(ttp_meth, pfs_dist, os_dist) |>
171+
dplyr::mutate(meth="joint", ic="bic")
172+
# Join together
173+
bests <- bests |>
174+
tibble::add_row(aic_jtbest) |>
175+
tibble::add_row(bic_jtbest)
176+
# Return
177+
return(list(results=res, bests=bests))
178+
}
179+
180+
# Compare likelihoods of PSMs for spline models
181+
compare_psm_likes_spl <- function(ptdata, fitslist, cuttime=0) {
182+
# Check that fitslist is a list of 6 endpoints
183+
if (length(fitslist)!=6) {stop("The list provided to fitslist must contain all 6 endpoints")}
184+
# Create local variables
185+
eps <- ndists <- aic_indbest <- bic_indbest <- bests <- res <- thisfit <- aic_jtbest <- bic_jtbest <- NULL
186+
ll <- rank_aic <- ttp_meth <- ttp_knots <- pfs_scales <- pfs_knots <- os_scales <- os_knots <- rank_bic <- NULL
187+
# Best fits by AIC or BIC
188+
eps <- c(1, 3, 4)
189+
fits_aic <- eps |>
190+
purrr::map(~find_bestfit(fitslist[[.x]], crit="aic")$fit)
191+
fits_bic <- eps |>
192+
purrr::map(~find_bestfit(fitslist[[.x]], crit="bic")$fit)
193+
fits_all <- c(fits_aic, fits_bic)
194+
# Pull out scales and knots
195+
fits_scales <- seq(2*length(eps)) |>
196+
purrr::map_vec(~fits_all[[.x]]$aux$scale)
197+
fits_knots <- seq(2*length(eps)) |>
198+
purrr::map_vec(~length(fits_all[[.x]]$aux$knots))
199+
# Summarize parametric results in a table
200+
bests <- tibble::tibble(
201+
ttp_meth = fits_scales[c(1, 4)],
202+
ttp_knots = fits_knots[c(1, 4)],
203+
pfs_scales = fits_scales[c(2, 5)],
204+
pfs_knots = fits_knots[c(2, 5)],
205+
os_scales = fits_scales[c(3, 6)],
206+
os_knots = fits_knots[c(3, 6)],
207+
meth = "ind",
208+
ic = c("aic", "bic")
209+
)
210+
# Create results table for each model combination
211+
ndists <- eps |>
212+
purrr::map_vec(~length(fitslist[[.x]]))
213+
res <- tibble::tibble(
214+
id = 1:(ndists[3]*ndists[2]*(ndists[1]+1)),
215+
ttp_meth = NA,
216+
ttp_knots = NA,
217+
pfs_scales = NA,
218+
pfs_knots = NA,
219+
os_scales = NA,
220+
os_knots = NA,
221+
ll = NA,
222+
npar = NA,
223+
npts = NA
224+
)
225+
# Create a safe calculation of the PSM-simple likelihood (returns NA on error)
226+
slike_simple <- purrr::possibly(
227+
~calc_likes_psm_simple(
228+
ptdata=ptdata,
229+
dpam=.x,
230+
cuttime=cuttime),
231+
otherwise = NA)
232+
# Create a safe calculation of the PSM-complex likelihood (returns NA on error)
233+
slike_complex <- purrr::possibly(
234+
~calc_likes_psm_complex(
235+
ptdata=ptdata,
236+
dpam=.x,
237+
cuttime=cuttime),
238+
otherwise = NA)
239+
# Compute results for PSM-simple models
240+
message("Calculating PSM simple")
241+
thisfit <- list(ttp=NA, pfs=NA, os=NA)
242+
for (p in 1:ndists[2]) {
243+
thisfit$pfs <- fitslist$pfs[[p]]$result
244+
for (o in 1:ndists[3]) {
245+
thisfit$os <- fitslist$os[[o]]$result
246+
resrow <- (p-1)*ndists[3] + o
247+
res$ttp_meth[resrow] <- "simple"
248+
res$ttp_knots[resrow] <- 0
249+
res$pfs_scales[resrow] <- thisfit$pfs$aux$scale
250+
res$pfs_knots[resrow] <- length(thisfit$pfs$aux$knots)
251+
res$os_scales[resrow] <- thisfit$os$aux$scale
252+
res$os_knots[resrow] <- length(thisfit$os$aux$knots)
253+
psmsimple <- slike_simple(thisfit)
254+
if (is.na(psmsimple)[1]==FALSE) {
255+
res$ll[resrow] <- psmsimple$ll[2]
256+
res$npar[resrow] <- thisfit$pfs$npars + thisfit$os$npars + 1
257+
res$npts[resrow] <- psmsimple$npts[2]
258+
}
259+
}
260+
}
261+
# Compute results for PSM-complex models
262+
message("Calculating PSM complex")
263+
thisfit <- list(ttp=NA, pfs=NA, os=NA)
264+
for (t in 1:ndists[1]) {
265+
thisfit$ttp <- fitslist$ttp[[t]]$result
266+
for (p in 1:ndists[2]) {
267+
thisfit$pfs <- fitslist$pfs[[p]]$result
268+
for (o in 1:ndists[3]) {
269+
thisfit$os <- fitslist$os[[o]]$result
270+
resrow <- t*ndists[3]*ndists[2] + (p-1)*ndists[3] + o
271+
res$ttp_meth[resrow] <- thisfit$ttp$aux$scale
272+
res$ttp_knots[resrow] <- length(thisfit$ttp$aux$knots)
273+
res$pfs_scales[resrow] <- thisfit$pfs$aux$scale
274+
res$pfs_knots[resrow] <- length(thisfit$pfs$aux$knots)
275+
res$os_scales[resrow] <- thisfit$os$aux$scale
276+
res$os_knots[resrow] <- length(thisfit$os$aux$knots)
277+
psmcomplex <- slike_complex(thisfit)
278+
if (is.na(psmcomplex)[1]==FALSE) {
279+
res$ll[resrow] <- psmcomplex$ll[2]
280+
res$npar[resrow] <- thisfit$ttp$npars + thisfit$pfs$npars + thisfit$os$npars
281+
res$npts[resrow] <- psmcomplex$npts[2]
282+
}
283+
}
284+
}
285+
}
286+
# Set log-likelihood values to NA if if cannot be calculated (=-Inf)
287+
res$ll[res$ll==-Inf] <- NA
288+
# Add AIC and BIC, with ranks
289+
message("Wrapping up")
290+
res <- res |>
291+
dplyr::mutate(
292+
aic = 2*.data$npar-2*ll,
293+
bic = .data$npar*log(.data$npts)-2*ll,
294+
rank_aic = rank(.data$aic),
295+
rank_bic = rank(.data$bic),
296+
best_aic = 0,
297+
best_bic = 0
298+
)
299+
# Identify best AIC and best BIC model
300+
res$best_aic[res$ttp_meth==bests$ttp_meth[1] & res$ttp_knots==bests$ttp_knots[1] & res$pfs_scales==bests$pfs_scales[1] & res$pfs_knots==bests$pfs_knots[1] & res$os_scales==bests$os_scales[1] & res$os_knots==bests$os_knots[1]] <- 1
301+
res$best_bic[res$ttp_meth==bests$ttp_meth[2] & res$ttp_knots==bests$ttp_knots[2] & res$pfs_scales==bests$pfs_scales[2] & res$pfs_knots==bests$pfs_knots[2] & res$os_scales==bests$os_scales[2] & res$os_knots==bests$os_knots[2]] <- 1
302+
# Identify best distributions for overall AIC and BIC
303+
aic_jtbest <- res |>
304+
dplyr::filter(rank_aic==1) |>
305+
dplyr::select(ttp_meth, ttp_knots, pfs_scales, pfs_knots, os_scales, os_knots) |>
127306
dplyr::mutate(meth="joint", ic="aic")
307+
bic_jtbest <- res |>
308+
dplyr::filter(rank_bic==1) |>
309+
dplyr::select(ttp_meth, ttp_knots, pfs_scales, pfs_knots, os_scales, os_knots) |>
310+
dplyr::mutate(meth="joint", ic="bic")
128311
# Join together
129312
bests <- bests |>
130313
tibble::add_row(aic_jtbest) |>

man/compare_psm_likes.Rd

+5-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/psm3mkv-package.Rd

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)