| Title: | Backward Joint Model for the Dynamic Prediction of Both Time-to-Event and Longitudinal Outcomes |
| Version: | 0.3.0 |
| Maintainer: | Wenhao Li <wenhaoli.jlu@gmail.com> |
| Description: | Provides tools to fit joint models of multivariate longitudinal data and time-to-event data for dynamic prediction. It allows the joint prediction of both future time-to-event outcomes and future longitudinal outcomes conditional on survival. The models accommodate irregularly measured longitudinal data and competing risks outcomes. The use of the backward joint model enables fast and efficient computation, especially for applications with large sample sizes and many longitudinal variables. Methods without competing risks are described in Shen and Li (2021) <doi:10.1002/sim.9037>, and their extension to competing risks is described in Li et al. (2026) <doi:10.1007/s10985-026-09725-x>. |
| License: | MIT + file LICENSE |
| URL: | https://wenhaoli18.github.io/BJM/, https://github.com/wenhaoli18/BJM |
| BugReports: | https://github.com/wenhaoli18/BJM/issues |
| LazyData: | true |
| Encoding: | UTF-8 |
| RoxygenNote: | 7.3.2 |
| Depends: | R (≥ 3.5.0), survival |
| Imports: | nlme, mvtnorm, ggplot2 (≥ 3.4.0), Matrix, parallel, graphics |
| Suggests: | testthat (≥ 3.0.0), knitr, rmarkdown, torch, ordinal, timeROC, riskRegression, prodlim |
| Config/testthat/edition: | 3 |
| VignetteBuilder: | knitr |
| NeedsCompilation: | no |
| Packaged: | 2026-10-31 03:46:17 UTC; wenhaoli |
| Author: | Wenhao Li [aut, cre], Liang Li [aut] |
| Repository: | CRAN |
| Date/Publication: | 2026-10-31 08:10:35 UTC |
Recode ordinal biomarker responses onto the training categories
Description
Shared helper for predictRisk,
dynamicPredictionBio, and dynamicPredictionBioAll. The
copula densities turn an ordinal response into its category code with
as.numeric() and index the fitted thresholds with it, and the
candidate categories of an ordinal bio_i are read off
levels(). Both used to come from the prediction data itself, so
any difference from the training categories – unused levels dropped
(e.g. by droplevels() or subsetting and re-creating the factor),
a different level order, or a character column – silently bracketed the
latent score between the wrong thresholds (in one check, a risk of 0.53
instead of 0.10). Each ordinal response is now rebuilt as an ordered
factor on the categories the clmm() fit was estimated with,
matching by label; a value that is not one of those categories is an
error. Missing values stay missing (drop_missing_longitudinal()
removes them afterwards).
Usage
align_ordinal_levels(data_predict_all, long_fit_all)
Value
data_predict_all, with each ordinal response recoded.
Set a patient's future covariates
Description
For each row of future_rows (one per simulated time),
the covariates of the last row of future_covariates (for this
patient, if it has an id column) at or before that time; a covariate it
does not set keeps its value in future_rows.
Usage
apply_future_covariates(
future_rows,
future_covariates,
time_variable,
id,
patient_id
)
Call a single survival_trans_function element and validate its output
Description
assert_survival_trans() only probes each transform
once, at a single representative time value, before the prediction grid
runs. That catches a transform that is broken everywhere (wrong return
type/length, or throws), but not one that only misbehaves away from the
probe point – e.g. log(x - 10), which is fine near the probe value
but returns NaN once the internal prediction grid (which can range
up to 2 * max(observed survival time)) reaches x <= 10. This
helper wraps every actual call site inside conditionalYT()/
conditionalYTBio()/conditionalYDT()/conditionalYDTBio()
so a bad value is caught immediately, with a clear error, instead of
silently corrupting a data.frame column or surfacing later as a cryptic
"replacement has ... rows, data has ..." error.
Usage
apply_survival_trans(fun, x, surv_i)
Assert that every element of a list is a formula
Description
Shared input-validation helper for long_sub_fixed/
long_sub_random-style arguments, after they have been normalized
to a list (a bare formula is wrapped in list() by the caller
before calling this).
Usage
assert_all_formulas(x, arg_name)
Assert that a bandcount argument is a positive number or "auto"
Description
Shared input-validation helper for the bandcount1/
bandcount2/bandcount3 arguments of predictRisk(),
dynamicPredictionBio(), predictPlot(), and riskPlot(),
which now accept either an explicit positive number (the original
behavior) or the literal string "auto" to have the value chosen
automatically (see auto_tune_bandcount()).
Usage
assert_bandcount(x, arg_name)
Assert that biomarker_type is a valid per-biomarker type vector
Description
Shared input-validation helper for the biomarker_type
argument of longitudinalSub: when supplied explicitly (as
opposed to being auto-detected from each biomarker's response column),
it must be a character vector with exactly one entry per longitudinal
outcome, and every entry must be one of "continuous" or
"ordinal".
Usage
assert_biomarker_type(
biomarker_type,
M,
valid_types = c("continuous", "ordinal")
)
Assert that an object is the output of a specific BJM fitting function
Description
Shared input-validation helper: checks the S3 class tag
attached by survivalSub()/longitudinalSub(), so passing the
wrong object (or the arguments in the wrong order) fails immediately with
a clear message instead of deep inside the prediction code.
Usage
assert_class(x, expected_class, arg_name, expected_source)
Assert that an object is a non-empty data.frame
Description
Shared input-validation helper: raises a clear error instead of letting a malformed argument fail deep inside model-fitting code with a cryptic message.
Usage
assert_data_frame(x, arg_name)
Assert that an object is a list of data.frame objects of the expected length
Description
Shared input-validation helper for the data_fit_all/
data_predict_all arguments, which must be a list with one
data.frame per longitudinal outcome. When allow_bare_df = TRUE, a
single bare data.frame is also accepted, matching the auto-repeat
convenience some functions apply for a single data.frame shared across
every outcome.
Usage
assert_data_list(x, arg_name, n_expected, allow_bare_df = FALSE)
Assert that an index is a valid, in-range biomarker position
Description
Shared input-validation helper for bio_i: catches an
out-of-range or non-integer value before it becomes a "subscript out of
bounds" error from indexing into data_predict_all/lfit.
Usage
assert_index(x, max_value, arg_name, context)
Check that the survival time enters the event-type model linearly
Description
Used by survivalSub(): at prediction time the
event-type model form_conditional_cr is evaluated at every point of
the survival-time integration grid by multiplying the survival time's
coefficient by the grid value. That is only correct when the survival
time appears as a plain main effect (status ~ time + ...); a
transformation (log(time), I(time^2)) or an interaction
(time:age) would silently be evaluated at the wrong value, so it is
rejected here. Leaving the survival time out entirely is allowed (event
type then does not depend on event time).
Usage
assert_linear_time_term(form_conditional_cr, survival_variable)
Arguments
form_conditional_cr |
The event-type formula. |
survival_variable |
Name of the survival-time variable. |
Assert that an optional (Suggests-only) package is installed
Description
Shared input-validation helper for functions that depend on
a package listed in Suggests rather than Imports (so that
most users – and CRAN's own checks – are unaffected by a large,
optional dependency they never call). Every call site into that package
must still be fully namespaced (pkg::fun()), never
@importFrom; this helper just turns a missing package into a
clear, actionable error instead of a cryptic "could not find function"
failure deep inside the calling code.
Usage
assert_package_installed(pkg, context)
Assert that a list of longitudinalSub.BJM fits is poolable with Rubin's rules
Description
Shared input-validation helper for
poolLongitudinalSub: checks that long_fit_all_list
is a list of at least two longitudinalSub.BJM objects (the S3
class longitudinalSub attaches to its return value), and
that every one of them was fit for the same set of biomarkers with the
same fixed-effect coefficients – otherwise "the mean of the estimates"
would be averaging unrelated quantities across completions.
Usage
assert_poolable_longitudinal_fits(
long_fit_all_list,
arg_name = "long_fit_all_list"
)
Assert that an object is a single positive integer
Description
Shared input-validation helper for n_cores: catches a
non-integer, zero, negative, or non-scalar value before it reaches
parallel::mclapply()'s own (less informative) mc.cores
validation.
Usage
assert_positive_integer(x, arg_name)
Assert that an object is a single, non-missing numeric value
Description
Assert that an object is a single, non-missing numeric value
Usage
assert_scalar_numeric(x, arg_name, positive = FALSE)
Assert that an object is a single, non-missing character string
Description
Assert that an object is a single, non-missing character string
Usage
assert_string(x, arg_name)
Assert that survival_variable_all/survival_trans_function are consistent
Description
Shared input-validation helper for predictRisk(),
dynamicPredictionBio(), predictPlot(), and riskPlot():
the two arguments must have matching length, and every transform must be
a function. When probe_value is supplied, every transform is also
test-called once on it, and must return a single, finite, non-missing
numeric value. Without this, a transform that throws an error, or
returns a character value, a length != 1 vector, or a non-finite value
(e.g. log(x) evaluated at x <= 0), would only surface deep
inside the per-patient prediction grid built by conditionalYT()/
conditionalYDT()/conditionalYTBio()/conditionalYDTBio()
– as a cryptic error, or, worse, as silently corrupted data with no
error at all. The probe is a single call per transform, so it is cheap
even though the same transform is later called many times inside the
prediction grid.
Usage
assert_survival_trans(
survival_variable_all,
survival_trans_function,
probe_value = NULL
)
Check that a longitudinal sub-model uses the event time
Description
Check that a longitudinal sub-model uses the event time
Usage
assert_uses_event_time(formula_i, i, survival_variable, survival_variable_all)
Arguments
formula_i |
The biomarker's fixed-effects formula. |
Value
Invisibly TRUE; an error naming biomarker i
otherwise.
Assert that a formula's variables are all present in a data.frame
Description
Shared input-validation helper: catches missing columns
before they surface as an opaque error from deep inside coxph(),
lme(), or model.matrix().
Usage
assert_vars_in_data(vars, data, source_name, data_name)
Auto-select "auto" bandcount arguments by doubling until convergence
Description
Shared implementation backing bandcount1/
bandcount2/bandcount3 = "auto" support in
predictRisk()/dynamicPredictionBio(), and the
bandcount pre-resolution done once, up front, by predictPlot()/
riskPlot() (so their internal horizon/landmark loops do not repeat
the auto-tuning search on every iteration).
Starts every argument named in auto_names at its entry in
bandcount_auto_start(), doubles all of them together each round,
and compares consecutive results with max_relative_diff() until
the largest relative change drops below tol, or max_rounds
extra doublings have been tried – a hard cap, so this never loops
indefinitely: at most max_rounds + 1 calls to predict_fun
(the default max_rounds = 2 means at most 3 calls). If the cap is
hit without converging, a warning is issued and the result/bandcount at
the largest value tried is returned anyway, rather than erroring, so
automated pipelines are not interrupted.
Usage
auto_tune_bandcount(
predict_fun,
args,
auto_names,
tol = 0.01,
max_rounds = 2,
multiplier = 2
)
Arguments
predict_fun |
|
args |
A named list of all of |
auto_names |
Character vector naming which element(s) of |
Value
A list with result (predict_fun's return value at
the resolved bandcount) and bandcount (a named list of the
resolved numeric bandcount value(s), one per element of auto_names).
Auto-select "auto" bandcount3 for a single biomarker's per-marker step
Description
dynamicPredictionBioAll()'s counterpart to
auto_tune_bandcount(): bandcount3 only controls
compute_bio_marker_step()'s own candidate-value grid (Y_all),
not the shared step, so it is tuned per biomarker by re-calling
compute_bio_marker_step() directly against an already-computed
shared object (from compute_bio_shared_step()), rather than
re-running the whole dynamicPredictionBio() pipeline – which would
recompute the shared denominator on every doubling round, for every
biomarker, exactly the redundant work dynamicPredictionBioAll() is
meant to avoid. Same doubling-until-stable check as
auto_tune_bandcount() (compares Y_predict via
max_relative_diff(), capped at max_rounds doublings, warns
instead of erroring if not converged by then).
Usage
auto_tune_marker_bandcount3(
shared,
bio_i,
long_fit_all,
survival_fit_all,
prediction_time,
horizon,
time_variable,
survival_variable_all,
survival_trans_function,
tol = 0.01,
max_rounds = 2,
multiplier = 2
)
Arguments
shared |
Output of |
bio_i |
See |
long_fit_all |
See |
survival_fit_all |
See |
prediction_time |
See |
horizon |
See |
time_variable |
See |
survival_variable_all |
See |
survival_trans_function |
See |
Value
A list with result (compute_bio_marker_step()'s
return value at the resolved bandcount3) and bandcount3
(the resolved numeric value).
Starting values for "auto" bandcount doubling
Description
The built-in starting point auto_tune_bandcount()
doubles from for each bandcount argument. bandcount1/
bandcount2/bandcount3 used to default to fixed numbers
(10, 40, and 300 respectively, across
predictRisk()/dynamicPredictionBio()); those same
numbers are reused here as the starting point for auto-tuning, so that
the first call auto_tune_bandcount() makes matches what a caller
relying on the old fixed defaults would have gotten. This cannot instead
be read off formals(predict_fun), because that default is now the
literal string "auto" itself. The bandcount3 start was
lowered from 300 to 100 when its grid started covering only where the
predictive densities are not negligible (see
continuous_value_grid()): 100 points there are finer than 300
were over the old fixed, wide range.
Usage
bandcount_auto_start
Format
An object of class numeric of length 3.
Barnard & Rubin (1999) small-sample pooled degrees of freedom
Description
Internal helper for rubin_pool_scalar.
Interpolates between the classical Rubin (1987) degrees of freedom
(df_old, which ignores the complete-data sample size and can
therefore exceed it) and the complete-data degrees of freedom
(dfcom), weighted by how much of the total variance is due to
missingness (lambda).
Usage
barnard_rubin_df(m, lambda, dfcom)
Ordinal box probabilities by adaptive Gauss-Hermite quadrature
Description
For each column k, \log P(lower < m + l u + e
\le upper) with u \sim N(0, 1) and e \sim N(0, I)
independent: \log \int \prod_j P_j(u) \phi(u) du, where P_j
is a univariate normal probability. The integrand is log-concave in
u; Newton's method finds its mode and the curvature there, which
place an n_nodes-point Gauss-Hermite rule, all columns at once.
With 20 nodes the relative error was below 10^{-5} in every check.
Usage
box_prob_quadrature(m, lower, upper, l, n_nodes = 20)
Arguments
m, lower, upper |
Matrices, one row per ordinal observation and one column per evaluation. |
l |
Output of |
n_nodes |
Number of quadrature nodes. |
Value
A vector of log probabilities.
Reshape recurrent-event data into gap-time episodes for the backward joint model
Description
Experimental. Prepares recurrent-event data for the backward joint
model by a gap-time (renewal) reformulation: it splits each subject's
event history into episodes – the interval from one restart (the
previous event, or the discharge from the stay it began) to the next event,
with the clock reset to 0 at the start of each – and re-clocks any
accompanying longitudinal measurements onto the same per-episode timeline.
The result plugs into the existing survivalSub()/
longitudinalSub()/predictRisk() pipeline, unchanged, to
predict for a subject currently at risk in an open episode whether their
next event (within a horizon) is a recurrence or a terminal event;
predictRecurrentCount builds on the same data to predict the
number of recurrences in a window. Unlike the single-event and
competing-risks methods the rest of the package rests on (Shen and Li,
2021; Li et al., 2026 – see citation("BJM")), this extension is
unpublished, and has been checked only in simulation studies, not on real
data (see the "Recurrent events" article on the package website).
Usage
buildRecurrentEpisodes(
data_events,
data_long = NULL,
id_variable = "id",
time_variable = "time",
status_variable = "status",
time_variable_long = NULL,
episode_variable = "episode",
gap_variable = "gap",
time_in_episode_variable = "time_in_episode",
event_any_variable = "event_any",
event_type_variable = "event_type",
restart_variable = NULL,
lookback = 0,
episode_id_variable = "episode_id"
)
Arguments
data_events |
One row per subject per event-or-censoring occurrence,
in calendar time (i.e. not yet re-clocked to gap time – this function
does that). Must contain |
data_long |
Optional repeated-measurement data to re-clock onto the
same per-episode timeline, in the same format |
id_variable, time_variable, status_variable |
Column names in
|
time_variable_long |
Column name of the measurement time in
|
episode_variable, gap_variable, time_in_episode_variable, event_any_variable, event_type_variable, episode_id_variable |
Column names to create in the output (defaults |
restart_variable |
Optional name of a numeric column of
|
lookback |
A single non-negative number ( |
Details
What counts as an episode. Episode 1 runs from time 0 to
a subject's first row in data_events; episode k > 1 runs
from the end of episode k - 1 to that subject's k-th row.
A subject contributes no further episodes after a row with
status %in% c(0, 2) (administrative censoring or a terminal
event); any later rows for that id are ignored, with a message
saying how many. status == 1
rows (recurrences) always start a new episode.
Feeding the result into survivalSub(). Fit the gap-time
recurrence hazard and the recurrence-vs-terminal-event type model exactly
as survivalSub() already fits a marginal hazard and a competing-risks
type model, using episode_data in place of one-row-per-subject
data, Surv(gap, event_any) ~ covariates in place of
form_marginal_surv, and event_type ~ gap + covariates in
place of form_conditional_cr (event_type is 0 for a
recurrence, 1 for a terminal event – so predictRisk()'s
risk_prob_1/risk_prob_2 come out as the recurrence/terminal
risk respectively). Administratively-censored episodes get
event_any = 0 and event_type = NA, exactly like
status3 = 0/status4 = NA for a censored subject in
pbc3. The episodes of one subject are then treated as
independent: the coefficients are still consistent for the gap-time
model, but the standard errors that summary() reports ignore the
correlation between a subject's episodes and are too small.
Feeding the result into longitudinalSub()/predictRisk().
gap plays the same dual role years plays for pbc3: it
is both the Surv() time in episode_data and the backward
covariate swept over candidate values inside predictRisk(), so it
must appear in long_sub_fixed (e.g. biomarker ~
time_in_episode + gap + gap * time_in_episode + covariates), and the
training data passed to longitudinalSub() must be filtered to
event_any == 1 rows of long_data – only completed episodes
have an observed gap – exactly as the package's own examples
filter pbc3 to status3 == 1 before fitting
longitudinalSub(). time_in_episode (elapsed time since the
current episode began) plays the role year plays for
pbc3. To predict from "now" for a subject currently at risk in an
open episode, add a trailing status = 0 row to data_events
at the current data-cutoff time, and call predictRisk() with
prediction_time set to that subject's elapsed
time_in_episode at the cutoff.
Random effects: group by episode, not by patient. Fit the biomarker
model with the random effects grouped by episode_id
(random = ~ time_in_episode | episode_id), so that each episode is
its own "subject", as it already is in the survival sub-model. A patient's
biomarker level generally differs from one episode to the next (the
underlying trajectory keeps moving), so a single random effect per patient
is the wrong unit; in a simulation in which the biomarker's current level
drives the risk, grouping by patient gave a clearly worse AUC than grouping
by episode. predictRisk() and predictRecurrentCount() then
take one episode_id per prediction, so give each prediction its own
id.
The open episode's gap is NA. For an episode that
has not ended (event_any == 0) – typically the one a subject is
currently at risk in – the gap column of long_data is
NA: its final length is exactly what is being predicted.
predictRisk() reads a missing survival-time variable as "event time
not yet known, still at risk", so that episode's rows can be passed
straight to it, with no placeholder to set or override (episode_data
keeps the real elapsed time of such an episode, which is what the Cox fit
needs as a censoring time). Training rows are unaffected, since
longitudinalSub() is fit on event_any == 1 rows only. Note that
predictRisk() also expects the outcome columns it was fit with
(the time and event-type variables) to exist in the prediction data;
long_data already carries event_type (NA for an open
episode).
Episodes that start at discharge (restart_variable,
lookback). By default an episode starts at the previous event
(time 0 for the first). For hospitalization data that is not the
at-risk clock: after a readmission the patient is in hospital, not at risk,
until discharge. Give data_events a numeric column – passed as
restart_variable – holding, for each recurrence row, the time the
process restarts (the discharge ending that stay); the next episode then
starts there and its gap is measured from it. Time 0 is the first
episode's start (e.g. the index discharge). Biomarkers measured during the
stay, or before the index discharge, precede the episode they predict:
lookback (default 0, none) keeps measurements up to that
long before an episode's start – never reaching back past the previous
event, so a stay's own labs are not mixed into the earlier at-risk period –
and gives them negative time_in_episode. Use Inf to keep
the whole stay (and, for the first episode, everything before time
0). Negative times are fine for longitudinalSub() and
predictRisk().
Death, or the end of follow-up, during a hospital stay. With
restart_variable, a terminal event or censoring row whose time is at
or before the restart time of the readmission before it happened while the
patient was in hospital, not at risk. That readmission then ends the
subject's process: no further episode is opened, the row is dropped with a
message, and so a death in hospital is not counted as an out-of-hospital
terminal event (predictRecurrentCount()'s p_death is the
probability of dying after discharge). Record such a stay's restart time
as the time of death or of the end of follow-up, or later, when the
discharge never happened. A data cutoff during a stay gives no open
episode to predict for.
Covariates that change over time. Each episode's covariates come
from the row of data_events that ends it, as in counting-process
data, where each interval carries the covariate values in force during it.
A covariate that changes over time must therefore be recorded on each row
as its value at the start of that row's episode (age at the discharge
that started it, the number of earlier admissions, the length of the
previous stay), and the open episode's row in the same way. A value
measured at the event itself – age at readmission, say – depends on how
long the episode lasted, which is what the survival sub-model explains,
and so biases it; a value measured at the time of the prediction is not
what the model was fit with. Biomarkers that change within an episode
belong in data_long. Covariates built from earlier episodes can
also be added to episode_data afterwards, e.g. the length of the
previous gap:
ed <- episodes$episode_data ed$previous_gap <- ave(ed$gap, ed$id, FUN = function(g) c(0, g[-length(g)]))
Value
A list with elements:
- episode_data
One row per subject per episode:
id_variable,episode_variable(episode number, starting at 1),gap_variable(that episode's gap time),event_any_variable(0/1: did the episode end in an event of either type, i.e. isgapan observed event time rather than an administrative censoring time),event_type_variable(0= recurrence,1= terminal event,NAwhenevent_any == 0), and every other column ofdata_events.- long_data
NULLifdata_longwas not supplied; otherwisedata_long, in the same bare-data.frame-or-list shape it was supplied in, withepisode_variable,gap_variable,time_in_episode_variable,event_any_variable, andevent_type_variablecolumns added (and out-of-range rows dropped, as described above). Any other column ofepisode_data(baseline or event-time-varying covariates, e.g.age/sex) is also carried onto matching rows here, the way every row of pbc3's long-format table already repeatsage/sex/years/status3– but only into a columndata_longdoes not already have of that name.
Examples
## A minimal example: reshaping alone (no model fitting).
## Subject 1: two recurrences, then a terminal event; subject 2: censored.
data_events <- data.frame(
id = c(1, 1, 1, 2),
time = c(1.2, 2.5, 4.0, 3.1),
status = c(1, 1, 2, 0),
age = c(58, 58, 58, 61)
)
episodes <- buildRecurrentEpisodes(data_events)
episodes$episode_data
## Hospitalizations: each episode starts at DISCHARGE, not at the readmission.
## `time` = readmission (or death/censoring) day; `discharge` = day the stay
## it started ends. Labs drawn in the stay (and before the index discharge at
## day 0) are kept with negative time_in_episode via lookback = Inf.
events <- data.frame(id = 1, time = c(20, 60, 90), status = c(1, 1, 0),
discharge = c(26, 66, NA))
labs <- data.frame(id = 1, day = c(-3, 0, 5, 21, 26, 40, 61, 66, 80))
hf <- buildRecurrentEpisodes(events, labs, time_variable_long = "day",
restart_variable = "discharge", lookback = Inf)
hf$episode_data # gaps measured from each discharge: 20, 34, 24
hf$long_data[, c("day", "episode", "time_in_episode", "gap")]
## A full pipeline on simulated recurrent-event + longitudinal data:
## build episodes -> survivalSub() -> longitudinalSub() -> predictRisk() and
## predictRecurrentCount().
set.seed(1)
n <- 60
data_events <- do.call(rbind, lapply(seq_len(n), function(id) {
k <- sample(1:3, 1)
gaps <- rexp(k, rate = 1)
data.frame(
id = id, time = cumsum(gaps),
status = c(rep(1, k - 1), sample(c(0, 2), 1, prob = c(0.6, 0.4))),
age = round(50 + stats::rnorm(1, sd = 8)), sex = id %% 2
)
}))
## subject 9999 is still at risk right now, 0.7 time units into their
## 2nd episode (1 prior recurrence, no resolution yet) -- this is who
## we will predict for below.
data_events <- rbind(data_events, data.frame(
id = 9999, time = c(0.8, 1.6), status = c(1, 0), age = 55, sex = 1
))
data_long <- do.call(rbind, lapply(c(seq_len(n), 9999), function(id) {
maxt <- max(data_events$time[data_events$id == id]) + 0.2
tt <- seq(0, maxt, by = 0.25)
data.frame(id = id, year = tt, biomarker = rnorm(length(tt)))
}))
built <- buildRecurrentEpisodes(data_events, data_long, time_variable_long = "year")
## gap-time recurrence hazard + recurrence-vs-terminal-event type model
survival_fit_recur <- survivalSub(
built$episode_data,
Surv(gap, event_any) ~ age + sex + episode,
event_type ~ gap + age + sex + episode
)
## longitudinal sub-model, trained on completed episodes only, with the
## random effects grouped by episode (see Details)
long_fit_recur <- longitudinalSub(
built$long_data[built$long_data$event_any == 1, ],
biomarker ~ time_in_episode + age + sex + gap + gap * time_in_episode,
~ time_in_episode | episode_id
)
## subject 9999's open episode: its gap is NA, which predictRisk() reads as
## "still at risk" -- nothing to override
open <- built$long_data[built$long_data$id == 9999 & built$long_data$event_any == 0, ]
landmark <- max(open$time_in_episode)
## the risk that their *next* event, within 1 time unit, is a recurrence /
## a terminal event
risk <- predictRisk(
open, long_fit_recur, survival_fit_recur,
prediction_time = landmark, horizon = 1, time_variable = "time_in_episode",
survival_variable_all = list(), survival_trans_function = list(),
bandcount1 = 10, bandcount2 = 10
)
risk$risk_prob_1 # P(next event within horizon is a recurrence)
risk$risk_prob_2 # P(next event within horizon is the terminal event)
## the number of recurrences over the next 3 time units
predictRecurrentCount(
open, long_fit_recur, survival_fit_recur,
prediction_time = landmark, horizon = 3, time_variable = "time_in_episode",
n_sim = 100, seed = 1
)
Build the per-patient longitudinal design matrices
Description
Shared helper for conditionalYT and conditionalYDT:
constructs the longitudinal outcome matrix, fixed-effect parameter matrix,
and random-effect variance-covariance pieces used by the conditional
density quadratic form, for a single patient.
Usage
build_conditional_design(
rep_num_i_list,
data_num_i_list,
lfit,
Sigma,
sigma.longitudinal,
time_variable,
n_longitudinal,
long_sub_random
)
Value
A list with longitudinal_all_matrix, parameter_matrix,
and cov_fac, the patient's factored covariance from
cov_factor().
Build the per-patient longitudinal design pieces for a mixed continuous/ordinal (Gaussian-copula) joint model
Description
Copula-aware counterpart to build_conditional_design()
(see R/conditionalDesign.R), for use whenever
long_fit_all$biomarker_type contains at least one "ordinal"
biomarker (see longitudinalSubCopula()). Continuous biomarkers'
rows are handled identically to build_conditional_design(): their
exact observed value is recorded. For an ordinal biomarker's rows, the
exact underlying latent Gaussian score is unobserved – only the pair of
cumulative-link thresholds (long_fit_all$thresholds[[m]]) bracketing
the observed category is known (the classical Albert & Chib probit
data-augmentation representation, same convention as
longitudinalSubVarCopula() in R/copulaLongitudinal.R: latent
score Z = eta + epsilon, epsilon ~ N(0, 1), category
k iff full_alpha[k] < Z <= full_alpha[k+1] with
full_alpha = c(-Inf, alpha_m, Inf), thresholds used directly with
no sign flip).
The random-effects design (A_i) and Sigma_all construction
is unchanged in form from build_conditional_design() – marker type
only affects sigma.longitudinal (already fixed at 1 for
ordinal markers by the caller, the probit identification constraint; see
longitudinalSubCopula()), which flows in exactly the same way for
both continuous and ordinal markers.
Usage
build_conditional_design_copula(
rep_num_i_list,
data_num_i_list,
lfit,
Sigma,
sigma.longitudinal,
time_variable,
n_longitudinal,
biomarker_type,
long_sub_fixed,
thresholds,
long_sub_random
)
Value
A list with y_all_vec (stacked observed values; NA
at ordinal-marker rows), alpha_lower_vec/alpha_upper_vec
(stacked threshold bounds; NA at continuous-marker rows),
row_marker_type (stacked per-row "continuous"/"ordinal"
label), parameter_matrix (block-diagonal fixed-effect coefficients,
lfit[[i]]$coefficients$fixed for continuous markers,
lfit[[i]]$beta – no intercept – for ordinal markers), and
Sigma_all.
Build the conditional diffusion denoising network
Description
Internal helper for imputeLongitudinal's
method = "diffusion" backend. A single MLP predicts the noise in
the target entries of a row from: the noisy target entries (zero
elsewhere), the conditioning entries' values (zero elsewhere), the
conditioning mask, the covariates and an embedding of the diffusion step
– a tabular version of CSDI's conditional denoiser (Tashiro et al.,
2021).
Usage
build_diffusion_module(n_cov, M, hidden_units, time_emb_dim)
Arguments
n_cov |
Number of (standardized) covariate columns. |
M |
Number of biomarkers. |
|
Hidden-layer sizes of the MLP body. | |
time_emb_dim |
Width of the timestep embedding. |
Value
An instantiated torch::nn_module.
Build the longitudinal outcome matrix for all candidate biomarker values
Description
Shared helper for conditionalYTBio and
conditionalYDTBio: for each candidate value in Y_all,
assembles the row of observed longitudinal outcomes across biomarkers,
substituting the candidate value for the biomarker being predicted.
Usage
build_longitudinal_matrix_bio(
data_num_i_list,
lfit,
bio_i,
Y_select_all,
n_longitudinal,
Y_all
)
Value
A matrix with length(Y_all) rows, one per candidate value.
Build the MIWAE encoder/decoder network
Description
Internal helper for imputeLongitudinal. The
encoder input is (standardized covariates, zero-filled standardized
biomarkers, missingness mask); the decoder input is (latent draw,
standardized covariates), matching the "condition the generative model
on fully-observed covariates too" design used throughout.
Usage
build_miwae_module(n_cov, M, hidden_units, latent_dim)
Plot calibration of dynamic risk predictions
Description
Checks whether the predicted risks are numerically right, not just well
ranked. At each landmark s in prediction_time, every subject
in data_predict_all still event-free at s gets a predicted
risk from predictRisk of an event in
(s, s + horizon], using only their longitudinal history up to
s. The subjects are split into n_groups groups of about
equal size by predicted risk, and for each group the mean predicted risk
(horizontal axis) is plotted against the observed risk (vertical axis):
one minus the Kaplan–Meier estimate at s + horizon or, with
competing risks, the Aalen–Johansen cumulative incidence of that event
type, both of which account for censoring, with 95\
intervals. Points on the diagonal mean the predictions are well
calibrated; points below it mean the model overestimates the risk, above
it that it underestimates it.
Evaluating on the data the sub-models were fit on gives an optimistic
(apparent) calibration; pass held-out validation data in
data_predict_all when available.
For an interval-censored fit (experimental), the observed risk
of a group is a nonparametric (Turnbull-type) estimate from the
subjects' intervals, left-truncated at each subject's last visit up to
the landmark – with competing risks, the cumulative incidence estimate
of Hudgens, Satten and Longini (2001) – so it estimates the same
quantity predictRisk() predicts, without using the model. Within
an interval the timing of the event is not identified, so the estimate
is less precise than with exact times, and no confidence interval is
drawn. In simulations it was unbiased when the visits were at most about
horizon apart, but with visits further apart than horizon
the window mostly falls inside the subjects' intervals and the observed
risk of the higher-risk groups was much too high (up to +0.27 with visits
every 2–6 years and a 2-year window). An observed risk computed from the
model's own posterior was more precise, but followed the predictions of
a deliberately miscalibrated model instead of the truth, so it is not
offered.
Usage
calibrationPlot(
data_predict_all,
long_fit_all,
survival_fit_all,
prediction_time,
horizon,
time_variable,
survival_variable_all,
survival_trans_function,
n_groups = 10,
bandcount1 = "auto",
bandcount2 = "auto"
)
Arguments
data_predict_all |
The evaluation data, in the same format as for
|
long_fit_all |
A |
survival_fit_all |
A |
prediction_time |
A vector of landmark times. |
horizon |
The prediction horizon (a single positive number): the window after each landmark in which events are counted. |
time_variable |
The name of the time variable in the linear mixed models. |
survival_variable_all, survival_trans_function |
As for
|
n_groups |
Number of risk groups per landmark. Default |
bandcount1, bandcount2 |
As for |
Value
A ggplot object with one panel per landmark (and, with
competing risks, per event type). Its data element is a
data.frame with one row per risk group, and columns
landmark, cause, group, predicted,
observed, lower, upper, n (subjects in the
group) and n_cases (events of that type observed in the window).
Examples
data(pbc3)
survival_fit_all <- survivalSub(pbc3[!duplicated(pbc3$id), ],
Surv(years, status3) ~ age + sex, NULL)
long_sub_fixed <- list("long1" = serBilir ~ year + age + sex + years)
long_sub_random <- list("long1" = ~ year | id)
long_fit_all <- longitudinalSub(list(pbc3[pbc3$status3 == 1, ]),
long_sub_fixed, long_sub_random)
trans <- survivalTrans(c(1, 3, 5, 7))
# apparent calibration on the fitting data: 3-year landmark, 2-year window
calibrationPlot(pbc3, long_fit_all, survival_fit_all,
prediction_time = 3, horizon = 2, time_variable = "year",
trans$survival_variable_all, trans$survival_trans_function, n_groups = 5)
Predicted and observed risk per risk group at one landmark
Description
Predicted and observed risk per risk group at one landmark
Usage
calibration_groups(risk, time, status, cause, k, end, n_groups)
Arguments
risk |
Predicted risks of event type |
time, status, cause |
Observed outcome of the same subjects (see
|
k |
Event type evaluated (ignored without competing risks, i.e.
when |
end |
End of the prediction window, |
n_groups |
Number of groups (fewer if there are fewer subjects). |
Value
A data.frame with one row per group: group,
predicted (mean predicted risk), observed,
lower, upper (Kaplan–Meier/Aalen–Johansen estimate at
end with 95\
Observed risk per risk group for interval-censored outcomes
Description
The interval-censored counterpart of
calibration_groups(): within each group of predicted risk, the
observed risk of an event of type k in (s, e] for a subject
known to be event-free at their last visit V is
(F_k(e) - F_k(s)) / (1 - F(V)) with F from
npmle_interval(), averaged over the group's subjects – the same
quantity predictRisk() predicts. No confidence interval is given.
Usage
calibration_groups_interval(risk, L, R, cause, V, k, s, end, n_groups)
Arguments
risk |
Predicted risks of event type |
L, R, cause |
Observed outcome (see |
V |
Each subject's last visit up to the landmark. |
k, s, end |
Event type, landmark and window end. |
n_groups |
Number of groups. |
Value
As calibration_groups().
Check whether bandcount1/bandcount2/bandcount3 are large enough
Description
predictRisk()/predictLongitudinal()
approximate the integrals behind the predicted risk probabilities (and,
for predictLongitudinal(), the predicted biomarker density) with a
finite grid, controlled by bandcount1/bandcount2/
bandcount3. There is no universal correct value: too few grid
points silently bias the answer, and too many just cost more time, and
the right value depends on the data (e.g. how wide the follow-up range
is). Rather than guess, or auto-loop until some tolerance is met –
which multiplies runtime unpredictably, especially for
predictLongitudinal()'s nested bandcount2 x
bandcount3 grid – this runs the prediction once at the
bandcount value(s) you supply, once more with those value(s) scaled up,
and reports the largest relative change between the two, so you can see
whether you have already converged or need to increase the checked
bandcount(s) and re-run. It costs exactly 2 calls to predict_fun,
regardless of how many times you invoke it.
Usage
checkBandcountConvergence(
predict_fun,
...,
bandcount_args,
multiplier = 2,
tol = 0.01
)
Arguments
predict_fun |
The prediction function to check: |
... |
Arguments to forward to |
bandcount_args |
A named list giving the bandcount value(s) to
check, e.g. |
multiplier |
Factor the checked bandcount value(s) are scaled by
for the second call. Must be greater than 1. Defaults to |
tol |
Relative-change tolerance below which the result is reported
as converged. Defaults to |
Value
An object of class "checkBandcountConvergence.BJM", a
named list with elements:
- base_bandcount
The bandcount value(s) checked, as supplied in
bandcount_args.- scaled_bandcount
base_bandcountscaled bymultiplier.- tol
The relative-change tolerance used to decide
converged.- by_field
A named numeric vector giving the largest relative change, per comparable output field (e.g.
risk_prob_1,Y_predict), between the base and scaled call.- max_rel_diff
The largest value in
by_field, orNAif there was no comparable output (e.g.horizon <= 0).- converged
TRUEifmax_rel_diff < tol,FALSEif not,NAif there was nothing to compare.- base_result
The full result of calling
predict_funatbase_bandcount.- scaled_result
The full result of calling
predict_funatscaled_bandcount.
Printing the object summarizes these fields.
Examples
data(pbc3)
data_survival_fitting = pbc3[!duplicated(pbc3$id), ]
survival_fit_all = survivalSub(data_survival_fitting, Surv(years, status3) ~ age + sex, NULL)
long_sub_fixed = serBilir ~ year + age + sex + (years) + (years) * year
long_sub_random = ~ year | id
long_fit_all = longitudinalSub(pbc3[pbc3$status3 == 1, ], long_sub_fixed, long_sub_random)
trans = survivalTrans(c(1, 3, 5, 7))
data_predict_all = pbc3[pbc3$id == 2 & pbc3$year <= 3, ]
# Check bandcount1/bandcount2 together: are they both already large enough?
check = checkBandcountConvergence(
predictRisk, data_predict_all, long_fit_all, survival_fit_all,
prediction_time = 3, horizon = 3, time_variable = "year",
trans$survival_variable_all, trans$survival_trans_function,
bandcount_args = list(bandcount1 = 10, bandcount2 = 10)
)
print(check)
Validate future_covariates
Description
Checks that it is a data frame with a non-missing time column, that its id column (if any) names patients being simulated, and that it does not set an outcome. Returns it ordered by time.
Usage
check_future_covariates(
future_covariates,
time_variable,
id,
patient_ids,
outcome_variables
)
Check the event-time transformations against the training data
Description
The longitudinal sub-models were fit on data whose
survival_variable_all columns the user computed from the event time;
at prediction time the same columns are recomputed with
survival_trans_function. The two are passed as separate lists, so a
function out of order (or a different one) used to give wrong predictions
silently. This applies each function to the event times stored with every
fit (lme()'s data, clmm()'s model frame) and stops if
the result differs from the column the model was fit on.
Usage
check_survival_trans_training(
long_fit_all,
survival_fit_all,
survival_variable_all,
survival_trans_function
)
Arguments
long_fit_all |
Output of |
survival_fit_all |
Output of |
survival_variable_all, survival_trans_function |
As in
|
Value
TRUE, invisibly; an error on a mismatch.
Plot cumulative incidence functions
Description
Plots the nonparametric cumulative incidence of each event type over
time, estimated by the Aalen–Johansen estimator (via
survfit on a multi-state Surv outcome).
With a single event type this equals one minus the Kaplan–Meier
survival curve; with competing risks each event type gets its own curve,
correctly accounting for the other event types instead of treating them
as censoring. An optional group_variable draws separate curves per
group (e.g. treatment arm).
Usage
cifPlot(
data_plot_all,
survival_variable,
event_type_variable,
id_variable = "id",
censor_value = 0,
event_labels = NULL,
group_variable = NULL,
conf_int = TRUE
)
Arguments
data_plot_all |
A |
survival_variable |
Name of the time-to-event variable. |
event_type_variable |
Name of the variable holding each subject's event type, or censoring status. |
id_variable |
Name of the subject ID column. Default is |
censor_value |
The value of |
event_labels |
Optional legend labels for the event types: either a
character vector named by the event type values (e.g.
|
group_variable |
Optional name of a subject-level variable to stratify
the curves by, shown by line type (and also by color when there is a
single event type). |
conf_int |
Logical; draw pointwise 95\
|
Value
A ggplot object.
Examples
data(pbc3)
# competing risks: death (1) and transplantation (2); 0 = censored
cifPlot(pbc3, survival_variable = "years", event_type_variable = "status5",
event_labels = c("1" = "Death", "2" = "Transplant"))
# the same events coded as in status4 (censored subjects are NA)
cifPlot(pbc3, survival_variable = "years", event_type_variable = "status4",
censor_value = NA, event_labels = c("0" = "Death", "1" = "Transplant"))
# single event type, by treatment arm
cifPlot(pbc3, survival_variable = "years", event_type_variable = "status2",
group_variable = "drug", conf_int = FALSE)
Legend labels for the event types in cifPlot()
Description
Legend labels for the event types in cifPlot()
Usage
cif_event_labels(event_values, event_labels)
Arguments
event_values |
Sorted event type values, as character. |
event_labels |
The user's |
Value
A character vector of labels, one per event type.
Expand cumulative incidence confidence limits into a step-shaped ribbon
Description
geom_ribbon() joins points with straight lines; to
follow the step curves, each jump time gets two rows, one with the
previous limits and one with the new limits.
Usage
cif_step_ribbon(curves)
Arguments
curves |
The curve |
Value
A data.frame with columns time, lower,
upper, hue, curve.
Normalize a risk-probability ratio into a valid probability
Description
Shared helper for predictRisk and
dynamicPredictionBio: divides summed predicted-event mass by
summed total mass and clamps the result to [0, 1].
Usage
clamp_risk_prob(numerator_sum, denominator_sum)
Value
A numeric vector of risk probabilities in [0, 1].
Plot conditional mean trajectories (CMT)
Description
This function generates the Conditional Mean Trajectories (CMT)
plot: the mean biomarker trajectory of the patients whose event happened in
the window (condi_time2event - event_window, condi_time2event]. The
time from the first measurement up to condi_time2event is cut into
consecutive intervals of length interval_time; each point is the mean
of those patients' biomarker values measured within one interval, plotted at
the interval's end, and a straight line is fitted through the points. With
competing risks there is one trajectory per event type.
All times – time_variable, survival_variable,
condi_time2event, event_window and interval_time – are
in the units of the data (years, months, days, ...), and the time axis shows
time_variable on that scale, labelled with its name.
Only patients with an observed event enter: with competing risks, those
whose event_type_variable equals one of event_type; without,
those whose status_variable is not 0. Without competing risks and
with status_variable = NULL, patients are selected by their survival
time alone, so censored ones are included too, with a warning. An interval
without any measurement is left out rather than filled in.
Usage
cmtPlot(
data_plot_all,
condi_time2event,
event_type_variable,
event_type,
bio_variable,
time_variable,
survival_variable,
interval_time = 1/12,
id_variable = "id",
status_variable = NULL,
event_window = 1
)
Arguments
data_plot_all |
A |
condi_time2event |
Event time the patients are selected around. If
|
event_type_variable |
Competing risks variable indicator name. Set to NULL if there are no competing risks. |
event_type |
A vector containing the values of |
bio_variable |
Name of the biomarker variable used for plotting. |
time_variable |
The name of time variable in linear mixed model. |
survival_variable |
Name of the time-to-event outcomes variable. |
interval_time |
Length of the intervals the follow-up is cut into, in the
units of |
id_variable |
Name of the patient ID column in |
status_variable |
Without competing risks, the name of the event indicator (0 = censored), so that only patients with an event are used. Ignored with competing risks. |
event_window |
Width of the window of event times before
|
Value
Conditional mean trajectories plot.
Examples
# example without competing risks
data(pbc3)
pbc.cmt <- cmtPlot(data_plot_all = pbc3, condi_time2event = 5,
event_type_variable = NULL, event_type = NULL,
bio_variable = "serBilir", time_variable = "year",
survival_variable = "years",
interval_time = 1/12, status_variable = "status3"
)
pbc.cmt
# example with competing risks
data(pbc3)
pbc.cmt.cr <- cmtPlot(pbc3, condi_time2event = 5,
event_type_variable = 'status4', event_type = c("0", "1"),
bio_variable = "albumin", time_variable = "year",
survival_variable = "years",
interval_time = 1/4
)
pbc.cmt.cr
A patient's history rows usable for conditioning
Description
The rows of one biomarker's data with the biomarker and every covariate of its sub-model observed (the outcome columns, which are set at each candidate event time, excepted). May have no rows.
Usage
complete_history_rows(d, i, long_fit_all, survival_variable, other_outcomes)
Bio_i-specific computation for dynamicPredictionBio()
Description
Internal helper for the part of dynamicPredictionBio()'s
pipeline that does depend on which biomarker is being predicted: building
the candidate-value grid Y_all, evaluating the numerator
conditional density on it, and assembling/normalizing Y_density
and Y_predict. Takes the output of compute_bio_shared_step()
so the bio_i-independent pieces are not recomputed.
Usage
compute_bio_marker_step(
shared,
bio_i,
long_fit_all,
survival_fit_all,
prediction_time,
horizon,
time_variable,
survival_variable_all,
survival_trans_function,
bandcount3
)
Arguments
shared |
Output of |
bio_i |
See |
long_fit_all |
See |
survival_fit_all |
See |
prediction_time |
See |
horizon |
See |
time_variable |
See |
survival_variable_all |
See |
survival_trans_function |
See |
bandcount3 |
See |
Value
A list with Y_predict, Y_density, Y_all
(matching dynamicPredictionBio()'s return value fields). For an
ordinal bio_i, Y_all/Y_predict hold integer
category codes (1:K, in threshold order) rather than a numeric
grid – see Details below – and Y_all additionally carries a
"category_labels" attribute with the matching level-label strings.
Bio_i-independent shared computation for dynamicPredictionBio()
Description
Internal helper factoring out the part of
dynamicPredictionBio()'s pipeline that does not depend on which
biomarker (bio_i) is being predicted: restricting to at-risk
patients, building the survival-side integration grid over
T > prediction_time + horizon out to upper_bound
(the prediction is conditional on survival through the time the biomarker
is predicted at), dispatching to the Gaussian-copula conditional-density
variants when needed (see predictRisk()), and evaluating the
denominator conditional density (conditionalYT/conditionalYDT).
dynamicPredictionBioAll() computes this once and reuses it across
every requested biomarker, instead of recomputing it once per biomarker
(including, under the copula path, its mvtnorm::pmvnorm() calls)
the way calling dynamicPredictionBio() once per biomarker would.
Usage
compute_bio_shared_step(
data_predict_all,
long_fit_all,
survival_fit_all,
prediction_time,
horizon,
time_variable,
survival_variable_all,
survival_trans_function,
bandcount2
)
Arguments
data_predict_all |
See |
long_fit_all |
See |
survival_fit_all |
See |
prediction_time |
See |
horizon |
See |
time_variable |
See |
survival_variable_all |
See |
survival_trans_function |
See |
bandcount2 |
See |
Value
A list with the pieces compute_bio_marker_step() needs:
data_predict_all (at-risk-filtered), survival_variable,
predict.time.infinity, S_T_all_infinity, has_cr,
D_T_all_infinity (NULL if no competing risk),
f_y_D_all_infinity, and the dispatched
conditionalYTBio_fun/conditionalYDTBio_fun functions.
conditional distribution of D|T
Description
This function computes the conditional probability density function of competing risk event type D, given the survival time T.
Usage
conditionalDT(data_predict_all, long_fit_all, survival_fit_all, l_i)
Arguments
data_predict_all |
This involves a collection of |
long_fit_all |
Outputs from the model fitting process using the |
survival_fit_all |
Results and parameters generated from the model fitting
procedure, utilizing the |
l_i |
A vector of time points to calculate the conditional probability. |
Value
Probability matrices of competing risk event type D conditional on survival outcome T.
conditional distribution of Y|D, T, if with competing risk
Description
This function computes the conditional probability density function of longitudinal variable Y, given the survival time T with competing risk D.
Usage
conditionalYDT(
data_predict_all,
long_fit_all,
survival_fit_all,
l_i,
survival_variable,
time_variable,
survival_variable_all,
survival_trans_function
)
Arguments
data_predict_all |
This involves a collection of |
long_fit_all |
Outputs from the model fitting process using the |
l_i |
A vector of time points to calculate the conditional probability. |
survival_variable |
Time-to-event outcomes variable name. |
time_variable |
The name of time variable in linear mixed model. |
survival_variable_all |
The name of the transformed time-to-event outcomes variable. |
survival_trans_function |
The transformation function used for time-to-event outcomes,
in the order of |
Value
The output is a list containing probability matrices of log densities
(so that they neither overflow nor underflow; see predictRisk() for how they
are exponentiated). In the presence of
competing risks, this list includes two elements; otherwise,
it contains only one element. Each element within the list is a probability matrix,
with the number of rows (l_i) corresponding to specific time points and
columns representing different patients. Every matrix element represents
the conditional probability derived from the conditional distribution
of longitudinal variable Y given the survival time T with competing risk D
for a particular patient at a specific time point.
conditional distribution of Y|D, T, if with competing risk
Description
This function computes the conditional probability density function of longitudinal variable Y, given the survival time T with competing risk D.
Usage
conditionalYDTBio(
Y_all,
time_new,
bio_i,
data_predict_all,
long_fit_all,
survival_fit_all,
l_i,
survival_variable,
time_variable,
survival_variable_all,
survival_trans_function
)
Arguments
data_predict_all |
This involves a collection of |
long_fit_all |
Outputs from the model fitting process using the |
l_i |
A vector of time points to calculate the conditional probability. |
survival_variable |
Time-to-event outcomes variable name. |
time_variable |
The name of time variable in linear mixed model. |
survival_variable_all |
The name of the transformed time-to-event outcomes variable. |
survival_trans_function |
The transformation function used for time-to-event outcomes,
in the order of |
Value
The output is a list containing probability matrices of log densities
(so that they neither overflow nor underflow; see predictRisk() for how they
are exponentiated). In the presence of
competing risks, this list includes two elements; otherwise,
it contains only one element. Each element within the list is a probability matrix,
with the number of rows (l_i) corresponding to specific time points and
columns representing different patients. Every matrix element represents
the conditional probability derived from the conditional distribution
of longitudinal variable Y given the survival time T with competing risk D
for a particular patient at a specific time point.
Conditional distribution of Y|D,T for a mixed continuous/ordinal (Gaussian-copula) joint model, if with competing risk – biomarker-value prediction
Description
Copula-aware counterpart to conditionalYDTBio(), used
by dynamicPredictionBio() whenever long_fit_all$biomarker_type
contains at least one "ordinal" biomarker and a competing-risk
survival_fit_all is supplied. Both a continuous and an
ordinal bio_i target are supported: for an ordinal
bio_i, Y_all is expected to already be the vector of
candidate category labels (the fitted factor's levels(), in
threshold order) rather than a numeric grid – see
compute_bio_marker_step(), which builds that vector and translates
the result back into integer category codes for its caller. See
conditionalYTBioCopula() for why no branching is actually needed
here – build_conditional_design_copula() already looks up each
row of biomarker bio_i generically via
long_fit_all$biomarker_type[bio_i], whether that row is historical
or the candidate row assigned below – and for why this function
evaluates mvtnorm::pmvnorm() once per patient/l_i/
Y_all candidate/event-type branch, rather than one vectorized
dmvnorm() call per patient/l_i as conditionalYDTBio()
does.
Usage
conditionalYDTBioCopula(
Y_all,
time_new,
bio_i,
data_predict_all,
long_fit_all,
survival_fit_all,
l_i,
survival_variable,
time_variable,
survival_variable_all,
survival_trans_function
)
Arguments
data_predict_all |
This involves a collection of |
long_fit_all |
Outputs from the model fitting process using the |
l_i |
A vector of time points to calculate the conditional probability. |
survival_variable |
Time-to-event outcomes variable name. |
time_variable |
The name of time variable in linear mixed model. |
survival_variable_all |
The name of the transformed time-to-event outcomes variable. |
survival_trans_function |
The transformation function used for time-to-event outcomes,
in the order of |
Conditional distribution of Y|D,T for a mixed continuous/ordinal (Gaussian-copula) joint model, if with competing risk
Description
Copula-aware counterpart to conditionalYDT(), used by
predictRisk() whenever long_fit_all$biomarker_type
contains at least one "ordinal" biomarker (see
longitudinalSubCopula()). See conditionalYTCopula() for the
mixed continuous/ordinal density/probability construction shared by both
event-type branches (w0/w1) here.
Usage
conditionalYDTCopula(
data_predict_all,
long_fit_all,
survival_fit_all,
l_i,
survival_variable,
time_variable,
survival_variable_all,
survival_trans_function
)
Arguments
data_predict_all |
This involves a collection of |
long_fit_all |
Outputs from the model fitting process using the |
l_i |
A vector of time points to calculate the conditional probability. |
survival_variable |
Time-to-event outcomes variable name. |
time_variable |
The name of time variable in linear mixed model. |
survival_variable_all |
The name of the transformed time-to-event outcomes variable. |
survival_trans_function |
The transformation function used for time-to-event outcomes,
in the order of |
conditional distribution of Y|T, if no competing risk;
Description
This function computes the conditional probability density function of longitudinal variable Y, given the survival time T without competing risk D.
Usage
conditionalYT(
data_predict_all,
long_fit_all,
l_i,
survival_variable,
time_variable,
survival_variable_all,
survival_trans_function
)
Arguments
data_predict_all |
This involves a collection of |
long_fit_all |
Outputs from the model fitting process using the |
l_i |
A vector of time points to calculate the conditional probability. |
survival_variable |
Time-to-event outcomes variable name. |
time_variable |
The name of time variable in linear mixed model. |
survival_variable_all |
The name of the transformed time-to-event outcomes variable. |
survival_trans_function |
The transformation function used for time-to-event outcomes,
in the order of |
Value
The output is a list containing probability matrices of log densities
(so that they neither overflow nor underflow; see predictRisk() for how they
are exponentiated). In the presence of
competing risks, this list includes two elements; otherwise,
it contains only one element. Each element within the list is a probability matrix,
with the number of rows (l_i) corresponding to specific time points and
columns representing different patients. Every matrix element represents
the conditional probability derived from the conditional distribution
of longitudinal variable Y given the survival time T without competing risk D
for a particular patient at a specific time point.
conditional distribution of Y|T, if no competing risk;
Description
This function computes the conditional probability density function of longitudinal variable Y, given the survival time T without competing risk D.
Usage
conditionalYTBio(
Y_all,
time_new,
bio_i,
data_predict_all,
long_fit_all,
l_i,
survival_variable,
time_variable,
survival_variable_all,
survival_trans_function
)
Arguments
time_new |
Prediction time add horizon |
bio_i |
Biomarker used to do prediction |
data_predict_all |
This involves a collection of |
long_fit_all |
Outputs from the model fitting process using the |
l_i |
A vector of time points to calculate the conditional probability. |
survival_variable |
Time-to-event outcomes variable name. |
time_variable |
The name of time variable in linear mixed model. |
survival_variable_all |
The name of the transformed time-to-event outcomes variable. |
survival_trans_function |
The transformation function used for time-to-event outcomes,
in the order of |
Value
The output is a list containing probability matrices of log densities
(so that they neither overflow nor underflow; see predictRisk() for how they
are exponentiated). In the presence of
competing risks, this list includes two elements; otherwise,
it contains only one element. Each element within the list is a probability matrix,
with the number of rows (l_i) corresponding to specific time points and
columns representing different patients. Every matrix element represents
the conditional probability derived from the conditional distribution
of longitudinal variable Y given the survival time T without competing risk D
for a particular patient at a specific time point.
Conditional distribution of Y|T for a mixed continuous/ordinal (Gaussian-copula) joint model, if no competing risk – biomarker-value prediction
Description
Copula-aware counterpart to conditionalYTBio(), used
by dynamicPredictionBio() whenever long_fit_all$biomarker_type
contains at least one "ordinal" biomarker (see
longitudinalSubCopula()). Both a continuous and an
ordinal bio_i target are supported: for an ordinal
bio_i, Y_all is expected to already be the vector of
candidate category labels (the fitted factor's levels(), in
threshold order) rather than a numeric grid – see
compute_bio_marker_step(), which builds that vector and translates
the result back into integer category codes for its caller. No branching
is actually needed here: build_conditional_design_copula() already
looks up each row of biomarker bio_i (historical and the
candidate row assigned below) generically via
long_fit_all$biomarker_type[bio_i], bracketing an ordinal row's
latent score between its category's cumulative-link thresholds instead of
matching it to an exact value – exactly the same mechanism already used
for every other ordinal biomarker's observed history in the joint
density. Assigning a candidate label into data_it_Y's ordinal
factor column (below) therefore evaluates the same
mixed_density_prob_copula() box probability as any other ordinal
row would, with no separate code path required.
Unlike conditionalYTBio() – which can evaluate
mvtnorm::dmvnorm() once per l_i across every Y_all
candidate in one vectorized call, because the multivariate-normal density
does not need re-normalizing per candidate – here each candidate value in
Y_all changes the conditional block's mean/covariance (see
mixed_density_prob_copula()) and therefore requires its own
mvtnorm::pmvnorm() evaluation. This makes the copula path in this
function O(patients * length(l_i) * length(Y_all)) Monte-Carlo box
probability evaluations, rather than one dmvnorm() call per
patient/l_i – keep bandcount3 modest for mixed fits with a
continuous bio_i (an ordinal bio_i's Y_all length is
fixed at its category count, not controlled by bandcount3 at all).
Usage
conditionalYTBioCopula(
Y_all,
time_new,
bio_i,
data_predict_all,
long_fit_all,
l_i,
survival_variable,
time_variable,
survival_variable_all,
survival_trans_function
)
Arguments
time_new |
Prediction time add horizon |
bio_i |
Biomarker used to do prediction |
data_predict_all |
This involves a collection of |
long_fit_all |
Outputs from the model fitting process using the |
l_i |
A vector of time points to calculate the conditional probability. |
survival_variable |
Time-to-event outcomes variable name. |
time_variable |
The name of time variable in linear mixed model. |
survival_variable_all |
The name of the transformed time-to-event outcomes variable. |
survival_trans_function |
The transformation function used for time-to-event outcomes,
in the order of |
Conditional distribution of Y|T for a mixed continuous/ordinal (Gaussian-copula) joint model, if no competing risk
Description
Copula-aware counterpart to conditionalYT(), used by
predictRisk() whenever long_fit_all$biomarker_type
contains at least one "ordinal" biomarker (see
longitudinalSubCopula()). Continuous biomarkers contribute an exact
Gaussian density factor, exactly as conditionalYT() computes for
every biomarker; ordinal biomarkers instead contribute a Gaussian-copula
box probability (via mixed_density_prob_copula()), since only the
cumulative-link category – not the exact underlying latent score – is
observed for them. All-continuous fits are unaffected: they are still
routed to conditionalYT() by predictRisk(), this
function is never called for them, and mixed_density_prob_copula()
reduces to (a constant multiple of) conditionalYT()'s own
computation when there are no ordinal markers – see that function's
documentation.
Usage
conditionalYTCopula(
data_predict_all,
long_fit_all,
l_i,
survival_variable,
time_variable,
survival_variable_all,
survival_trans_function
)
Arguments
data_predict_all |
This involves a collection of |
long_fit_all |
Outputs from the model fitting process using the |
l_i |
A vector of time points to calculate the conditional probability. |
survival_variable |
Time-to-event outcomes variable name. |
time_variable |
The name of time variable in linear mixed model. |
survival_variable_all |
The name of the transformed time-to-event outcomes variable. |
survival_trans_function |
The transformation function used for time-to-event outcomes,
in the order of |
Log densities of an empty longitudinal history
Description
Stand-ins for conditionalYT()/conditionalYDT()
for patients without any measurement: f(Y \mid T) = 1, i.e. a log
density of 0 at every time (and event type), so predictRisk()
gives the survival sub-model's risk.
Usage
conditionalYT_none(data_predict_all, long_fit_all, l_i, ...)
conditionalYDT_none(data_predict_all, long_fit_all, survival_fit_all, l_i, ...)
Value
A list of one (conditionalYT_none()) or two
(conditionalYDT_none()) zero matrices, one row per time in
l_i and one column per patient.
Per-patient pieces of the marginal survival model
Description
Shared by integration_upper_bound() and
prepare_infinity_grid(): each at-risk patient's Cox linear
predictor (reference = "zero", NA if a covariate is
missing), and the patients grouped by the baseline cumulative hazard
they use (one group, or one per stratum of a stratified model).
Usage
conditional_survival_setup(data_predict_all, long_fit_all, survival_fit_all)
Value
A list with lp, groups (each a list with
cum_basehaz and patients, indices into lp), and
last_time (the last time in the data survivalSub() was fit on).
Candidate-value grid for a continuous biomarker
Description
Helper for compute_bio_marker_step(): the grid of
candidate values (Y_all) a continuous biomarker's predictive
density is tabulated on, shared by all patients predicted. Built in two
passes:
A coarse grid over a deliberately wide range – 5 spans either side of the patients' observed values, the span being their range but at least the biomarker's SD in the training data – with a step of the biomarker's residual SD
\sigma, at least 30 and at most 100 points. A predicted future measurement includes its measurement error, so every predictive density has an SD of at least\sigma: within half a step of its mode it is still above 88\rel_tol, so no patient's density falls between coarse points.-
bandcount3+ 1 equally spaced points over the part of the coarse grid where some patient's density exceedsrel_toltimes that patient's maximum, widened by2\sigma(and at least one coarse step) on either side.
The wide range used to be the final grid, so most of the
bandcount3 points (about 90\
every density was practically 0. If the coarse pass finds no usable
density, the wide range is returned with bandcount3 + 1 points,
as before.
Usage
continuous_value_grid(
shared,
bio_i,
bio_i_name,
long_fit_all,
bandcount3,
density_fun,
rel_tol = 1e-06
)
Arguments
shared |
Output of |
bio_i |
Index of the biomarker. |
bio_i_name |
Its response variable name. |
long_fit_all |
Output of |
bandcount3 |
Number of intervals of the final grid. |
density_fun |
Function of a vector of candidate values returning the density matrix (one row per value, one column per patient). |
rel_tol |
Relative density below which a value is outside the grid. |
Value
A numeric vector of candidate values.
Copula EM with adaptive Gauss-Hermite quadrature
Description
Given the ordinal random effects b_o, the continuous
ones b_c have a Gaussian posterior with mean a + B b_o and
covariance A_c, and the continuous data contribute a Gaussian factor
in b_o; only b_o is integrated numerically. For each subject
the posterior mode of b_o (Newton, warm-started from the previous
iteration) and the curvature there place a product Gauss-Hermite grid,
which gives E[b b^T \mid y] block by block; their average over
subjects is the next D. Uses 7 nodes per dimension up to two ordinal
random effects and 5 beyond. The EM is accelerated by SQUAREM (Varadhan and
Roland, 2008), falling back to plain EM steps where an extrapolated
D is not positive definite; convergence is a change in D,
scaled by \sqrt{D_{jj} D_{kk}} so that it does not depend on the
biomarkers' units, below tol.em over one accelerated cycle (three
EM updates).
Usage
copula_D_aghq(D, setup, tol.em = 1e-04, max.iter = 500, verbose = FALSE)
Arguments
D |
Starting covariance. |
setup |
Output of |
tol.em, max.iter |
Convergence tolerance on |
verbose |
Print the change in |
Value
A list with D.
Copula EM by stochastic approximation (SAEM)
Description
Fallback of longitudinalSubVarCopula() for many ordinal
random effects. Each iteration draws every ordinal latent score from its
truncated normal given the subject's current random effects (one
dimension at a time, as they are independent given b), computes
E[b b^T \mid y, z] exactly, draws new random effects from their
Gaussian posterior, and updates D by stochastic approximation: step
1 for n_burnin iterations, then 1/k, averaging the next
n_average. The draws use a fixed seed, with the caller's random
number state restored, so a fit is reproducible.
Usage
copula_D_saem(D, setup, n_burnin = 100, n_average = 200, verbose = FALSE)
Arguments
D |
Starting covariance. |
setup |
Output of |
n_burnin, n_average |
Iterations with step 1 and with step |
verbose |
Print the change in |
Value
A list with D.
Per-subject pieces of the copula E-step that do not depend on D
Description
For every subject: the ordinal rows' fixed-effect linear
predictor, the thresholds bracketing each observed category and the
random-effects design of those rows; and the continuous rows' residuals
from the fixed effects, their random-effects design and residual
precisions. Also the positions ro/rc of the ordinal and
continuous biomarkers' random effects in D.
Usage
copula_estep_setup(thetaLong, l, biomarker_type, thresholds)
Arguments
thetaLong |
List with the stacked fixed effects |
l |
Per-subject design bookkeeping built by
|
biomarker_type, thresholds |
As stored in the fit. |
Value
A list with subjects (one entry per subject), ro
and rc.
A patient's copula log densities at every grid point
Description
Shared by conditionalYTCopula(),
conditionalYDTCopula(), conditionalYTBioCopula() and
conditionalYDTBioCopula(). The patient's covariance, observed
values and threshold bounds do not depend on the event time, so the
design (build_conditional_design_copula()) is built once per
candidate value of the predicted biomarker instead of once per grid
point, and the means at all grid points come from
copula_mean_grid(). All densities are evaluated at once.
Predicting a continuous biomarker with the Monte Carlo box probability
(two or more ordinal random effects, see
mixed_density_copula_prep()) needs it at every grid point for
every candidate value, which made this the slowest prediction in BJM.
The candidate value only shifts the ordinal latent scores' conditional
mean, so the log box probability is a smooth function of it: it is
computed at n_exact candidate values spread over the grid, and a
cubic spline gives it at the others, while the continuous
biomarkers' density is computed exactly at every value. In checks the
difference from computing every value was no larger than the Monte
Carlo error itself (around 10^{-4}), at 4-8 times the speed. With
one ordinal random effect, the quadrature is fast and every value is
computed exactly.
Usage
copula_logdens_grid(
data_num_i_list,
rep_num_i_list,
long_fit_all,
l_i,
survival_variable,
time_variable,
survival_variable_all,
survival_trans_function,
sigma.longitudinal,
event_type_variable = NULL,
bio = NULL,
n_exact = 15
)
Arguments
data_num_i_list, rep_num_i_list |
The patient's rows per biomarker. |
sigma.longitudinal |
Residual SDs (1 for an ordinal biomarker). |
event_type_variable |
|
bio |
NULL, or a list with |
n_exact |
Number of candidate values computed exactly when interpolating (see Details). |
Value
A list with one element per event type (1, then 0; just one
without competing risks), each a length(l_i) x (number of
candidate values, 1 without bio) matrix of log densities.
A biomarker's mean at every grid point, continuous or ordinal
Description
For the copula density functions: fixed_mean_grid()
for a continuous biomarker; for an ordinal one, the linear predictor
X(t)\beta of its clmm() fit (no intercept; the thresholds
take its place) from long_sub_fixed[[i]], with all grid points
stacked into one model.matrix() call. An ordinal formula with a
term whose values depend on the other rows (poly(),
splines::ns()/bs(), factor(), scale(),
cut(), which longitudinalSub() warns about) is evaluated
one grid point at a time, as before, so that its value does not change.
Usage
copula_mean_grid(
i,
data_i,
long_fit_all,
l_i,
survival_variable,
survival_variable_all,
survival_trans_function,
fixed_values = list()
)
Value
A nrow(data_i) x length(l_i) matrix.
Factor one patient's joint longitudinal covariance
Description
The stacked (across every biomarker) observation vector of
one patient has covariance
Sigma_all = A %*% D %*% t(A) + diag(r_diag): A is the
N x r random-effects design (random_effects_design()),
D the r x r random-effects covariance (Sigma_fit)
and r_diag each observation's residual variance. This factors it
once so that cov_logdens() can evaluate the Gaussian log-density
at any number of residual vectors.
With method = "woodbury", Sigma_all is never formed. Writing
D = G %*% t(G) and B = A %*% G, the matrix determinant
lemma and the Woodbury identity give
\log|\Sigma| = \sum \log r_j + \log|K|,\quad
e'\Sigma^{-1}e = e'R^{-1}e - u'K^{-1}u,
with K = I + t(B) %*% R^{-1} %*% B (r x r) and
u = t(B) %*% R^{-1} %*% e. Factoring costs
O(N r^2 + r^3) instead of O(N^3), each density
O(N r) instead of O(N^2), and memory O(N r) instead
of O(N^2). D^{-1} is never needed: G is
t(chol(D)), or, when D is only positive semi-definite (a
random-effect variance collapsing to 0 during EM), its eigenvectors
scaled by the square roots of the non-negative eigenvalues, which still
reproduces D exactly.
With method = "dense", Sigma_all is formed and Cholesky
factored, as mvtnorm::dmvnorm() does. "auto" picks
"woodbury" only when every residual variance is positive and
N >= 3 r: in timings the two break even around N = 2.5 r
(the Woodbury steps carry larger constants), and dense is faster below. Both give the same density up to
floating-point rounding.
Usage
cov_factor(A, D, r_diag, method = c("auto", "woodbury", "dense"))
Arguments
A |
Random-effects design matrix, |
D |
Random-effects covariance matrix, |
r_diag |
Residual variances, length |
method |
|
Value
An object for cov_logdens().
Gaussian log-density from a factored covariance
Description
Evaluates the N-variate normal log-density with
covariance factored by cov_factor() at each column of a residual
matrix (observation minus mean).
Usage
cov_logdens(fac, E)
Arguments
fac |
The result of |
E |
Residual matrix, |
Value
A length-m vector of log-densities. If the covariance is
not positive definite ("dense" only), -Inf, or Inf
for a zero residual, as mvtnorm::dmvnorm() returns.
Log densities of every candidate outcome vector at every grid point
Description
For conditionalYTBio()/conditionalYDTBio():
the log density of each row of x (one per candidate value of the
predicted biomarker) with mean mu[, k], for every grid point
k, in one cov_logdens() call.
Usage
cov_logdens_grid(fac, x, mu)
Arguments
fac |
The patient's factored covariance ( |
x |
Matrix, one row per candidate value, one column per stacked observation. |
mu |
Matrix of means, one row per stacked observation, one column per grid point. |
Value
A nrow(x) x ncol(mu) matrix.
Baseline cumulative hazard at a set of time points
Description
Helper for marginalT and the integration-grid
helpers: for each element of l_i, the Breslow cumulative hazard
as the right-continuous step function it is – the value at the last
tabulated time <= l_i, or 0 before the first. Past the table's
last time it continues from the last tabulated value with the slope of
the least-squares line through the whole table (a linear extrapolation
of the cumulative hazard, i.e. a constant hazard). It used to switch to
that line itself, which does not pass through the last tabulated value:
the cumulative hazard jumped there, and when it jumped down (a convex
cumulative hazard, i.e. an increasing hazard) the interval spanning the
last time got a negative probability mass. The line
used to be tabulated
on a fixed 0.005 grid out to twice the integration upper limit and then
looked up, which assumed time was measured in years (with time in days
the table ran to millions of rows) and failed with "wrong sign in 'by'"
when the upper limit was below the last training time; it is now
evaluated directly.
The table used to be read at the nearest tabulated time, which
for a point just before an event time took that event's jump early.
Usage
cumulative_baseline_at(cum_basehaz, l_i)
Arguments
cum_basehaz |
A data frame with columns |
l_i |
Time points. |
Value
A numeric vector, one cumulative hazard per element of l_i.
Default covariate update for the episodes after the open one
Description
Adds k to the episode counter, but never past
cap: the largest episode number in the data the survival
sub-model was fit on. Beyond it the model's effect of the counter is an
extrapolation, and a hazard that rises with each earlier recurrence, run
out to episode 30, multiplies the hazard many hundredfold, so paths drag
on through ever shorter episodes and the counts are inflated.
Usage
default_next_covariates(episode_variable, cap = Inf)
Mode of a density tabulated on a grid
Description
Helper for compute_bio_marker_step(): the grid point
where density peaks, refined (when refine = TRUE) by fitting
a parabola through the log-density at that point and its two neighbours.
For a smooth, near-normal predictive density this recovers the mode to
well within one grid step, instead of snapping to the grid: the
unrefined mode moved in whole grid steps as bandcount3 changed,
which for a predicted value near 0 is a large relative change, so
"auto" tuning of bandcount3 reported non-convergence even on
a fine grid. No refinement at the grid's ends, for an ordinal biomarker
(whose grid is its categories), or if a neighbour's density is 0.
Usage
density_mode(Y_all, density, refine = TRUE)
Arguments
Y_all |
The (equally spaced, for continuous biomarkers) grid. |
density |
The density at each grid point. |
refine |
Whether to refine between grid points. |
Value
A single number, or NA if density has no finite maximum.
Build a linear variance-preserving diffusion (DDPM) noise schedule
Description
Internal helper for imputeLongitudinal's
method = "diffusion" backend. Returns the standard Ho et al.
(2020) linear beta schedule and the derived alpha/
alpha_bar sequences, as plain numeric vectors (not torch tensors)
so diffusion_impute can index them by a plain R integer
timestep inside its reverse-sampling loop without repeated
tensor<->R round trips.
Usage
diffusion_beta_schedule(diffusion_steps, beta_start = 1e-04, beta_end = 0.02)
Draw conditional reverse-diffusion completions
Description
Internal helper for imputeLongitudinal's
method = "diffusion" backend. The missing entries of every row are
the targets and all its observed entries the conditions: starting from
pure noise on the targets, the DDPM reverse process (Ho et al., 2020) is
run with the network conditioned on the observed values at every step.
Observed entries are returned unchanged.
Usage
diffusion_impute(
net,
X_cov,
X_resp,
X_mask,
resp_mean,
resp_sd,
schedule,
diffusion_steps,
time_emb_dim,
n_imputations
)
Arguments
net |
Trained network. |
X_cov, X_resp, X_mask |
As in |
resp_mean, resp_sd |
Means and standard deviations that undo the responses' standardization. |
schedule, diffusion_steps, time_emb_dim |
As used in training. |
n_imputations |
Number of completions to draw. |
Value
A list of n_imputations completed response matrices on the
original scale.
Drop longitudinal measurements taken after the prediction time
Description
Shared helper for predictRisk,
dynamicPredictionBio, and dynamicPredictionBioAll: a dynamic
prediction at prediction_time may only condition on the history
observed up to prediction_time, so rows of data_predict_all
whose time_variable is later than that are removed, with a warning
saying how many. (predictPlot/riskPlot already truncate per
landmark time before calling these, so they never trigger the warning.)
Rows with a missing time_variable are kept, as before.
It is an error if no row is left: there is no history to condition on.
With allow_empty = TRUE (predictRisk(), which predicts a
patient without measurements from the survival sub-model alone) it is an
error only if data_predict_all had no row to begin with.
Usage
drop_after_prediction_time(
data_predict_all,
time_variable,
prediction_time,
allow_empty = FALSE
)
Value
data_predict_all, filtered per element.
Drop prediction rows with a missing biomarker value or covariate
Description
Shared helper for predictRisk,
dynamicPredictionBio, and dynamicPredictionBioAll: a
patient's conditional density only involves the measurements actually
observed, so for each biomarker, rows of its data_predict_all
element with a missing response, or a missing covariate/time/ID used by
that biomarker's long_sub_fixed/long_sub_random formulas,
are removed. Previously such a row put an NA into the stacked
outcome vector (or misaligned the design matrix) and made that patient's
whole prediction NA. The outcome variables
(survival_variable: the survival time and, with competing risks,
the event type – see outcome_variables()) and
survival_variable_all are not checked: they are overwritten with
each integration grid point / event type before use, and are unknown
(typically NA) for a patient still at risk. The event type used
to be checked, so with the event type in long_sub_fixed every
at-risk patient was dropped. A patient left with no rows for some
biomarker cannot be predicted; a warning names them, and it is an error
if that leaves no patient at all.
With no_measurement_ok = TRUE (predictRisk()), a patient
left with no rows for any biomarker is not reported: it is
predicted from the survival sub-model alone (see
prepare_risk_data()). It is then an error only if no patient has
measurements of every biomarker, none is left without any measurement,
and n_other (the patients without any row up to
prediction_time, already removed) is 0.
Usage
drop_missing_longitudinal(
data_predict_all,
long_fit_all,
survival_variable,
survival_variable_all,
no_measurement_ok = FALSE,
n_other = 0
)
Value
data_predict_all, filtered per element.
Predict a future biomarker value from fitted sub-models, for a single biomarker
Description
Internal single-biomarker engine behind
predictLongitudinal – call predictLongitudinal()
directly instead (it dispatches here automatically when bio_i
names exactly one biomarker, and to dynamicPredictionBioAll
otherwise). Kept as a separate internal function – rather than folded
into predictLongitudinal() – because predictPlot
and checkBandcountConvergence also call it directly for a
single biomarker at a time.
Companion to predictRisk, using the same fitted
longitudinal (longitudinalSub) and survival
(survivalSub) sub-models, but instead of an event-risk
probability this returns a predictive density for a future value of one
chosen biomarker (bio_i) at prediction_time + horizon,
conditional on the subject's observed longitudinal history up to
prediction_time and on survival (being event-free) through
prediction_time + horizon, the time the biomarker is predicted at.
The density (Y_density, evaluated over a candidate-value grid
Y_all) is obtained by integrating the biomarker's predictive
distribution against the survival sub-model's hazard, using the subject's
empirical-Bayes random-effects update from their observed history; its
mode (Y_predict) is reported as the point prediction. As in
predictRisk, the survival-side integrals are
evaluated on numerical grids controlled by bandcount2, and the
density itself is evaluated on a grid controlled by bandcount3;
see Details.
bio_i may refer to either a continuous or an
ordinal biomarker (see longitudinalSubCopula);
for an ordinal bio_i, Y_all and Y_predict hold
integer category codes (1:K, in the fitted factor's
levels()/threshold order) rather than a numeric grid, with
Y_all additionally carrying a "category_labels" attribute
giving the matching level-label strings; bandcount3 is ignored in
that case, since the candidate grid is fixed at the biomarker's category
count (see Details).
The prediction is conditional on the longitudinal history observed up to
prediction_time: rows of data_predict_all whose
time_variable is later than prediction_time are dropped,
with a warning, before predicting.
Usage
dynamicPredictionBio(
bio_i,
data_predict_all,
long_fit_all,
survival_fit_all,
prediction_time,
horizon,
time_variable,
survival_variable_all,
survival_trans_function,
bandcount2 = "auto",
bandcount3 = "auto"
)
Arguments
bio_i |
Biomarker used to do prediction. May be continuous or ordinal (see Details). |
data_predict_all |
This involves a collection of |
long_fit_all |
Outputs from the model fitting process using the |
survival_fit_all |
Results and parameters generated from the model fitting
procedure, utilizing the |
prediction_time |
Time used to make the prediction |
horizon |
Prediction horizon |
time_variable |
The name of time variable in linear mixed model. |
survival_variable_all |
The name of the transformed time-to-event outcomes variable. |
survival_trans_function |
The transformation function used for time-to-event outcomes,
in the order of |
bandcount2 |
The number of grid points spanning
|
bandcount3 |
The number of points in the candidate-biomarker-value
grid ( |
Details
There is no universal correct value for bandcount2/bandcount3:
as a practical check, double both and confirm the results barely change;
if they do, keep doubling. By default (bandcount2 = "auto",
bandcount3 = "auto"), this doubling check is done for you:
starting from small built-in values, both are doubled together, and the
result is compared to the previous round, until the largest relative
change in Y_predict drops below 1%, or 2 doublings have been
tried (so at most 3 calls' worth of work). If it still has not converged
by then, a warning reports this and the result at the largest value
tried is returned anyway (not an error), so this never silently loops
for an unbounded amount of time. Pass an explicit number for either
argument to skip auto-tuning it and use a fixed value instead (as in
previous package versions), or call checkBandcountConvergence()
directly for more control over the tolerance and doubling count. See
also vignette("BJM-intro", package = "BJM") for a worked example.
Value
An object of class "predictLongitudinal.BJM", a named list with elements:
- Y_predict
A vector, one entry per at-risk patient (named by patient id), giving the MAP (most likely) predicted value of biomarker
bio_iatprediction_time + horizon. For an ordinalbio_i, this is an integer category code (see Details), not a raw value.- Y_density
A probability matrix whose rows correspond to the candidate biomarker values in
Y_alland whose columns correspond to individual patients; each entry is the dynamically predicted density of the biomarker taking that value. For an ordinalbio_i, each row is instead that category's predicted probability.- Y_all
The grid of candidate biomarker values used to build
Y_density. For an ordinalbio_i, this is the vector of integer category codes1:K, carrying a"category_labels"attribute with the matching level-label strings (see Details).
Predict multiple future biomarker values from fitted sub-models
Description
Internal multi-biomarker engine behind
predictLongitudinal – call predictLongitudinal()
directly instead (it dispatches here automatically when bio_i
names more than one biomarker, or is left NULL).
Batch counterpart to dynamicPredictionBio:
predicts every requested biomarker in bio_i from the same fitted
longitudinal/survival sub-models and the same data_predict_all, but
computes the bio_i-independent part of the pipeline (restricting
to at-risk patients, the survival-side integration grid, and the
denominator conditional density – see compute_bio_shared_step())
only once and reuses it across every biomarker, instead of
recomputing it once per biomarker the way calling
dynamicPredictionBio() in a loop would. This matters most under
the Gaussian-copula path (see longitudinalSubCopula), where
that denominator involves mvtnorm::pmvnorm() Monte-Carlo
evaluations that are otherwise the dominant cost of a multi-biomarker
prediction run.
bandcount2 (controlling the shared survival-integration grid) and
bandcount3 (controlling each biomarker's own candidate-value grid)
are auto-tuned separately when left at their default "auto": if
bandcount2 = "auto", it is resolved once, using a single
representative biomarker (the first one in bio_i), via the same
doubling-until-stable check dynamicPredictionBio uses; the
shared step is then built once at that resolved value. If
bandcount3 = "auto", it is then resolved independently for
every biomarker in bio_i (their candidate-value grids
need not converge at the same resolution), reusing the once-computed
shared step for every doubling round rather than rebuilding it – except
for any ordinal biomarker, for which bandcount3 tuning is
always skipped (its candidate grid is fixed at its category count; see
dynamicPredictionBio) and "bandcount3" is recorded
as NA for that biomarker. See Details in
dynamicPredictionBio for the general auto-bandcount
rationale.
Usage
dynamicPredictionBioAll(
bio_i = NULL,
data_predict_all,
long_fit_all,
survival_fit_all,
prediction_time,
horizon,
time_variable,
survival_variable_all,
survival_trans_function,
bandcount2 = "auto",
bandcount3 = "auto"
)
Arguments
bio_i |
Integer vector of biomarkers to predict. May include
continuous and/or ordinal biomarkers (see
|
data_predict_all |
See |
long_fit_all |
See |
survival_fit_all |
See |
prediction_time |
See |
horizon |
See |
time_variable |
See |
survival_variable_all |
See |
survival_trans_function |
See |
bandcount2 |
See |
bandcount3 |
See |
Value
A named list of "predictLongitudinal.BJM" objects (see
dynamicPredictionBio), one per requested biomarker, named
by that biomarker's response-variable name; with attributes
"bandcount2" (the single resolved/used bandcount2) and
"bandcount3" (a named numeric vector of the resolved/used
bandcount3 for each biomarker, or NA for an ordinal
biomarker). Classed "predictLongitudinalAll.BJM".
Interval edges holding equal conditional survival mass
Description
Helper for prepare_infinity_grid(): n_intervals
+ 1 edges from prediction_time to upper_bound such that the
average, over at-risk patients, of S(t \mid x) / S(s \mid x) drops
by the same amount across every interval. Falls back to equally spaced
edges if no patient has a usable linear predictor.
Usage
equal_mass_edges(
data_predict_all,
long_fit_all,
survival_fit_all,
prediction_time,
upper_bound,
n_intervals
)
Value
A strictly increasing numeric vector of length n_intervals + 1.
Posterior distribution of the event time at a landmark
Description
For the model-based performance measures: each patient's
distribution of T (and event type) given their history up to the
landmark s and T > V, their last visit – the same quantity
predictRisk() integrates – tabulated on the cells of
predictRisk()'s grids: (V, s] (interval_gap_grid())
and (s, upper] (prepare_infinity_grid()).
Usage
event_time_posterior(
data_predict_all,
long_fit_all,
survival_fit_all,
s,
time_variable,
survival_variable_all,
survival_trans_function,
n_inf = 100,
n_gap = 20
)
Arguments
data_predict_all |
Landmark data (history up to |
long_fit_all, survival_fit_all |
The fitted sub-models. |
s |
The landmark. |
time_variable, survival_variable_all, survival_trans_function |
As for
|
n_inf, n_gap |
Grid cells after and before |
Value
A named list (patient ids) of lists with edges (cell
boundaries, from V) and mass (cells x event types,
summing to 1).
Vertical line at the end of a patient's follow-up
Description
Helper for riskPlot(): a vertical line at the
patient's survival time, labelled in a legend – solid, coloured by event
type (as the risk curves: black for the first, red for the second), for an
observed event, and dashed grey for a censored follow-up. Nothing is drawn
when the time or the status is unknown, as for a new patient or an
interval-censored fit. The line used to be drawn the same way (red, with no
legend) for a censored patient as for an event.
Usage
event_time_vline(
data_predict_all_pre,
survival_fit_all,
event_type_variable = NULL
)
Arguments
data_predict_all_pre |
The patient's prediction data (list of data frames). |
survival_fit_all |
The survival sub-model, whose |
event_type_variable |
Name of the event-type variable, or |
Value
A list of ggplot2 components (possibly empty).
Exponentiate log densities after a per-patient shift
Description
Exponentiate log densities after a per-patient shift
Usage
exp_shifted(log_density, shift)
Arguments
log_density |
A matrix of log densities, columns = patients. |
shift |
Per-patient shifts, from |
Value
exp(log_density - shift), column-wise.
Slope of the extrapolated baseline cumulative hazard
Description
The constant hazard cumulative_baseline_at() uses
past the last tabulated time: the slope of the least-squares line through
the whole table, or 0 if that slope is negative or undefined (a table with
a single time), so the cumulative hazard never decreases.
Usage
extrapolation_slope(cum_basehaz)
Arguments
cum_basehaz |
A data frame with columns |
Value
A single non-negative number.
Write event times (and their transforms) into longitudinal data
Description
Write event times (and their transforms) into longitudinal data
Usage
fill_event_time(
data_fit_all,
id,
T_values,
event_time,
survival_variable_all = NULL,
survival_trans_function = NULL
)
Fit the backward joint model to interval-censored event times
Description
Experimental. With interval censoring nobody's
event time T is known exactly – only L < T \le R – so the
longitudinal sub-model f(Y \mid T), which has T as a
covariate, cannot be fit on the subjects with an observed event the way
it is for right-censored data. fitIntervalBJM() fills in T
by stochastic EM, then by multiple imputation:
fit the marginal survival model to the intervals (
survivalSub()withSurv(L, R, type = "interval2")); it does not depend onT, so it is fit once;start every interval-censored
Tat its interval midpoint and fitlongitudinalSub();draw each subject's
Tfromf(T \mid L < T \le R, Y, X) \propto f(Y \mid T, X) f(T \mid X)on(L, R](imputeEventTime()), refitlongitudinalSub(), and repeat forn_burniniterations;keep iterating, saving every
n_thin-th draw and itslongitudinalSub()fit untiln_imputationsare saved.
Step 3 uses the backward model's own factorization: f(Y \mid T)
on a grid of T is exactly what dynamic prediction already
computes (conditionalYT()), so each draw costs about one
predictRisk() call per subject.
For a subject whose event was detected, only the longitudinal
measurements up to the last visit without the event (L) are used:
the one at the detecting visit R (and any later one) is taken after
the event, which f(Y \mid T) does not describe, and they are left out
with a message. As in the rest of the interval-censoring support, the
biomarker visits are taken to be the inspection visits, so no measurement
lies between L and R.
By default only subjects whose event was observed (finite R) enter
the longitudinal fit, mirroring the complete-case fit used for
right-censored data, which is valid when censoring is independent of
(T, Y) given the covariates. include_right_censored = TRUE
also imputes T \in (L, \infty) for right-censored subjects and
uses them; their draws then rely on the survival model's extrapolated
tail.
The saved draws come from one chain, so successive draws are correlated;
saving only every n_thin-th iteration makes them closer to
independent. They are drawn with the model parameters at their point
estimates, not from their posterior, so the between-imputation variance
in pooled can still be somewhat understated.
For prediction, long_fit_all combines the imputations: its fixed
effects are the Rubin's-rules pooled estimates and its residual and
random-effects variances the averages over the imputations (see
poolLongitudinalSub), so a prediction does not depend on
which single imputation happened to come first. With competing risks the
event-type model of survival_fit_all likewise has its
coefficients averaged over the imputations.
Usage
fitIntervalBJM(
data_survival,
data_fit_all,
form_marginal_surv,
event_time,
long_sub_fixed,
long_sub_random,
time_variable,
survival_variable_all = NULL,
survival_trans_function = NULL,
form_conditional_cr = NULL,
n_burnin = 10,
n_imputations = 5,
n_thin = 5,
n_grid = 40,
baseline = c("spline", "piecewise"),
df = NULL,
include_right_censored = FALSE,
seed = NULL
)
Arguments
data_survival |
One row per subject: the interval endpoints, the survival covariates and the subject id. |
data_fit_all |
As in |
form_marginal_surv |
|
event_time |
Name of the event-time column used in
|
long_sub_fixed, long_sub_random |
As in |
time_variable |
Name of the visit-time column. |
survival_variable_all, survival_trans_function |
Transformed event-time
columns, as in |
form_conditional_cr |
Optional competing-risks (event-type) model, as
in |
n_burnin |
Stochastic-EM iterations before draws are saved. |
n_imputations |
Number of saved draws (and longitudinal fits). |
n_thin |
Iterations between saved draws (1 saves every iteration
after the burn-in). Larger values make the saved draws less correlated
at the cost of |
n_grid |
Grid points per subject interval. |
baseline, df |
Baseline hazard of the survival model (see
|
include_right_censored |
See Description. |
seed |
Optional integer seed (the global RNG state is restored). |
Value
An object of class "fitIntervalBJM", a list with
survival_fit_all (with competing risks, its event-type model has
the coefficients averaged over the imputations, while the rest of that
glm object, e.g. its standard errors, is the first imputation's
fit; survival_fit_all_list has one fit per imputation);
long_fit_all_list (one longitudinalSub() fit per
imputation); long_fit_all (the imputations combined, see
Description, for use with predictRisk() and friends);
pooled (poolLongitudinalSub of the fits, when
n_imputations > 1); imputed_T (a subjects x imputations
matrix); trace (each iteration's fixed effects, to check
the chain has settled); and settings (the iteration settings).
print() shows a short summary.
Examples
# simulated study: visits every 1-3 years, an event known only to lie
# between the last visit without it (L) and the first visit with it (R)
set.seed(1)
n <- 60
x <- rnorm(n)
event_time <- (-log(runif(n)) / (0.05 * exp(0.5 * x)))^(1 / 1.5)
surv <- data.frame(id = seq_len(n), x = x, L = NA_real_, R = NA_real_)
long <- list()
for (i in seq_len(n)) {
visits <- c(0, cumsum(runif(30, 1, 3)))
visits <- visits[visits < 12]
if (event_time[i] > max(visits)) {
surv$L[i] <- max(visits)
seen <- visits
} else {
k <- findInterval(event_time[i], visits)
surv$L[i] <- visits[k]
surv$R[i] <- visits[k + 1]
seen <- visits[visits <= event_time[i]]
}
long[[i]] <- data.frame(id = i, year = seen, x = x[i],
y = 1 + 0.3 * seen - 0.2 * event_time[i] + 0.4 * x[i] +
rnorm(1, 0, 0.5) + rnorm(length(seen), 0, 0.3))
}
long <- do.call(rbind, long)
fit <- fitIntervalBJM(surv, long, Surv(L, R, type = "interval2") ~ x,
event_time = "Tev", long_sub_fixed = y ~ year + Tev + x,
long_sub_random = ~ year | id, time_variable = "year",
n_burnin = 2, n_imputations = 2, n_thin = 1, n_grid = 15,
seed = 1)
fit$pooled
# the fit plugs into the prediction functions; Tev is unknown for a new patient
history <- long[long$id == 1 & long$year <= 2, ]
history$Tev <- NA_real_
predictRisk(history, fit$long_fit_all, fit$survival_fit_all,
prediction_time = 2, horizon = 2, time_variable = "year",
survival_variable_all = NULL, survival_trans_function = NULL,
bandcount1 = 10, bandcount2 = 20)
Event-type model for an interval-censored outcome
Description
The competing-risks part of survivalSub(): a logistic
model for the event type among subjects whose event was observed (finite
R). The event type is taken as known once the event is detected,
but the event time is not, so a model that uses it needs the
event_time column filled for those subjects (fitIntervalBJM()
fills it with imputed times and refits this model every iteration).
Usage
fit_event_type_interval(
data,
form_marginal_surv,
form_conditional_cr,
event_time
)
Fit the marginal model for a single ordinal/categorical biomarker
Description
Fits a cumulative link mixed model (probit link) via
ordinal::clmm(), used as the marginal model for a categorical
biomarker in the Gaussian-copula extension of longitudinalSub()
(see longitudinalSubCopula()). The probit link is used
specifically (rather than the more common logit) because it integrates
directly with the latent-Gaussian-score representation the copula
extension relies on: the underlying continuous latent variable implied by
a probit cumulative link model is, by construction, Gaussian, which is
exactly the representation needed to fold a categorical biomarker into
the same random-effects covariance structure as the continuous
biomarkers.
This follows the classical two-stage IFM (Inference Functions for
Margins; Joe 2005) strategy already used throughout BJM: fit each
biomarker's marginal model separately first (here, exactly as
nlme::lme() does for continuous biomarkers), then re-estimate the
joint random-effects covariance across all biomarkers afterwards (see
longitudinalSubVarCopula()).
Usage
fit_marginal_ordinal(fixed_formula, random_formula, data)
Value
A fitted ordinal::clmm object.
A biomarker's fixed-effects mean at every integration grid point
Description
The longitudinal mean X(t)\beta depends on the event
time t only through the survival-time column and its
survival_variable_all transformations. This evaluates it for all
rows of data_i (all patients) at all grid points l_i with
one model.frame()/model.matrix() call on the rows stacked
once per grid point, instead of one call per patient and grid point,
whose fixed overhead dominated the run time of the prediction functions.
The terms, factor levels and contrasts are those of the fit, as in
predict.lme(), so the design matrix is the one the model was fit
with, including data-dependent bases such as poly() and
splines::ns(). Transformed terms such as log(years) are
evaluated at each grid point. Rows are processed in chunks of at most
max_rows stacked rows.
Usage
fixed_mean_grid(
data_i,
lfit_i,
xlev_i,
l_i,
survival_variable,
survival_variable_all,
survival_trans_function,
fixed_values = list(),
beta = lfit_i$coefficients$fixed,
max_rows = 2e+05
)
Arguments
data_i |
Prediction rows for the biomarker. |
lfit_i |
The biomarker's fitted model ( |
xlev_i |
Its factor levels, or |
l_i |
Grid of event times. |
fixed_values |
Named list of further columns to set (e.g. the event type). |
beta |
Fixed effects; by default those of |
max_rows |
Largest number of stacked rows per chunk. |
Value
A nrow(data_i) x length(l_i) matrix; a row with a
missing covariate is NA.
Gauss-Hermite quadrature rule for the standard normal
Description
Nodes and weights (summing to 1) of the k-point rule
for E f(X), X \sim N(0, 1), by the Golub-Welsch eigenvalue
method.
Usage
gauss_hermite_normal(k)
Arguments
k |
Number of nodes. |
Value
A list with x (nodes) and w (weights).
Gaussian log density from a Cholesky factor
Description
Zero-mean multivariate normal log density of each column of
E, with covariance R^T R given by its upper Cholesky factor
R.
Usage
gaussian_logdens_chol(R, E)
What a subject's other visits say about each biomarker
Description
Inputs for the deep imputation models (history = TRUE
in imputeLongitudinal): without them each visit is
completed from that visit alone, so an imputed value does not carry the
subject's own level, which shrinks the estimated variation between
subjects. For each visit and biomarker: the mean of the subject's
other observed values of the biomarker, how many there are
(log1p), and the value at the nearest other observed visit and its
distance in time (log1p). The visit's own value is left out, so
the model cannot simply copy it while training. A subject with no other
observed value gets the biomarker's overall mean, a count of 0 and the
largest distance.
Usage
history_features(aligned, id, time_variable, resp_mat, mask)
Value
A matrix with four columns per biomarker, one row per visit.
Proportional hazards model for interval-censored event times
Description
Internal fitter behind survivalSub() when the
outcome is interval-censored (Surv(L, R, type = "interval2")).
coxph cannot fit such data, so the marginal model
is a proportional hazards model H(t \mid x) = H_0(t) e^{x\beta}
with a parametric baseline, fit by maximum likelihood. Each subject
contributes S(L) - S(R) for L < T \le R, S(L) when
right-censored (R = \infty), 1 - S(R) when left-censored, and
h(T) S(T) for an exactly observed T. Two baselines:
"spline"(default)Royston–Parmar:
\log H_0(t)is a natural cubic spline in\log twithdfdegrees of freedom (df - 1internal knots at quantiles of the log interval endpoints, boundary knots at their extremes).df = 1is a Weibull model."piecewise"a piecewise-constant hazard on
dfpieces cut at quantiles of the interval endpoints.
The spline is the default because interval-censored data say little
about the shape of the hazard within a visit interval, and that
shape is exactly what imputeEventTime() draws T from: the
piecewise-constant hazard is flat there, which in simulations biased the
longitudinal sub-model fit by fitIntervalBJM() when the true
hazard was increasing, while the smooth spline carries the trend across
neighbouring intervals into each one.
The baseline cumulative hazard is then tabulated on a fine grid in the
same hazard/time layout as basehaz,
so the prediction code can use it exactly as it uses the Breslow
estimate of a right-censored fit (see survival_accessors).
Usage
icphFit(
formula,
data,
event_time,
baseline = c("spline", "piecewise"),
df = 3,
lower_tail = c("spline", "weibull")
)
Arguments
formula |
|
data |
One row per subject. |
event_time |
Name of the event-time column the longitudinal sub-model uses. |
baseline |
|
df |
Spline degrees of freedom, or number of pieces. |
lower_tail |
Spline only: how |
Value
An object of class "icph.BJM".
Baselines for icphFit()
Description
Each returns a list with n_par, cumhaz(t, g)
(baseline cumulative hazard, 0 at t <= 0, Inf at
Inf), loghaz(t, g) (log baseline hazard, for exactly
observed times), monotone(g) (whether cumhaz is
increasing), start(rate0) (parameters of a constant hazard
rate0) and knot_times.
Usage
rp_baseline(endpoints, df, tail_slope = NULL)
piecewise_baseline(endpoints, df)
Arguments
endpoints |
Finite, positive interval endpoints. |
df |
Spline degrees of freedom / number of pieces. |
Functions
-
rp_baseline(): Royston–Parmar spline for\log H_0in\log t. Parameters: intercept, then one per basis column; the basis columns are scaled by their standard deviation overendpointsso the optimizer sees comparable scales. -
piecewise_baseline(): Piecewise-constant hazard; parameters are the log hazards of the pieces.
Linear predictor of an interval-censored PH fit
Description
Linear predictor of an interval-censored PH fit
Usage
icph_lp(fit, newdata)
Arguments
fit |
An |
newdata |
One row per patient. |
Stratum of each patient for a stratified interval-censored fit
Description
Stratum of each patient for a stratified interval-censored fit
Usage
icph_strata(fit, newdata)
Arguments
fit |
An |
newdata |
One row per patient. |
Value
Character labels in the fit's strata() format, or
NULL when the fit is not stratified.
Covariates of the imputation model
Description
The right-hand-side variables of long_sub_fixed
other than the subject id, including the visit time
time_variable when a formula uses it. The visit time is also the
key that aligns rows across biomarkers, and it used to be left out of the
model along with the id, so values were imputed without knowing when the
measurement was taken (pulling a trending biomarker towards its overall
level).
Usage
imputation_covariates(covariate_vars, id)
Arguments
covariate_vars |
List of each biomarker's right-hand-side variables. |
id |
Name of the id column. |
Value
Character vector of covariate names.
Draw interval-censored event times given the biomarker history
Description
Experimental. One draw of each subject's event time
from f(T \mid L < T \le R, Y, X) \propto f(Y \mid T, X) f(T \mid
X): f(Y \mid T) from the longitudinal sub-model on a grid of
n_grid intervals tiling (L, R] (conditionalYT()),
times the survival model's probability of each grid interval
(marginalT()); an interval is drawn with those weights and
T uniformly within it. A right-censored subject (R = \infty)
uses the same equal-probability grid from L out to the survival
model's tail as predictRisk(). A subject with L = R keeps
T = L. Used by fitIntervalBJM.
Usage
imputeEventTime(
data_fit_all,
long_fit_all,
survival_fit_all,
bounds,
time_variable,
survival_variable_all = NULL,
survival_trans_function = NULL,
n_grid = 40,
n_draws = 1
)
Arguments
data_fit_all |
Longitudinal data, one data frame per biomarker. |
long_fit_all |
Output of |
survival_fit_all |
Interval-censored output of |
bounds |
Data frame with columns |
time_variable, survival_variable_all, survival_trans_function |
As in
|
n_grid |
Grid intervals per subject. |
n_draws |
Independent draws per subject (the weights are computed once and reused). |
Value
A named numeric vector of draws, one per row of bounds;
with n_draws > 1, a matrix with one column per draw.
Impute missing longitudinal biomarker values
Description
Fills in missing biomarker values before
longitudinalSub() is fit, assuming they are missing at random
(MAR): whether a value is missing depends only on what is observed.
It is often not needed. longitudinalSub() fits each
biomarker's linear mixed model by maximum likelihood on all of that
biomarker's observed values, which is already valid under MAR; only the
joint covariance of the random effects across biomarkers,
Sigma_fit, is estimated from the subjects with some value of
every biomarker. In simulations on pbc3 (see the "Missing data
imputation" article on the package website), no imputation method made
the predictions closer to those from the complete data than this
complete-case analysis when visits were missing values at random; with
half of the subjects never measured for a biomarker, multiple
imputation with method = "mixed" was slightly closer. Imputation
can also help when the missingness depends on variables outside the
model, if they are added to the formulas used here.
Three methods are available, selected with method:
-
"mixed"(default) draws each subject's missing values from the multivariate linear mixed modellongitudinalSub()fits, given everything observed for that subject – all visits and all biomarkers – with the model's parameters drawn from an approximation to their posterior for each completion (proper multiple imputation; seemixed_model_imputations()). It needs no extra package, keeps the variation between subjects, and was the only method in the simulations that left the random-effects variances and the event-time coefficients unbiased. -
"miwae"fits a MIWAE (Missing data Importance-Weighted AutoEncoder; Mattei & Frellsen, 2019) – a variational autoencoder whose training objective only scores reconstruction on the observed entries – jointly across all supplied biomarkers, and uses self-normalized importance resampling from the fitted decoder to draw one or more plausible completions of the missing cells. -
"diffusion"fits a conditional denoising diffusion probabilistic model (DDPM; Ho, Jain & Abbeel, 2020) over the same per-row biomarker vector, trained as in CSDI (Tashiro et al., 2021): in every mini-batch, each row's observed entries are split at random into targets and conditions, and the network learns to denoise the targets given the conditions' values, the covariates and the observation mask, so the loss only ever involves observed entries. Missing cells are then filled by running the reverse diffusion with the missing entries as targets and all observed ones as conditions; observed entries are returned unchanged.
The two deep generative methods need the torch package. They
complete each visit from that visit's covariates and observed biomarkers
(and, with history = TRUE, a summary of the subject's other
visits), and in the simulations they shrank the estimated variation
between subjects, which every prediction uses; with history = TRUE
they also biased the event-time coefficients when subjects lacked a
biomarker entirely. The completed data.frame(s) of any method can be
passed directly to longitudinalSub() in place of the original
data_fit_all.
Usage
imputeLongitudinal(
data_fit_all,
long_sub_fixed,
long_sub_random,
time_variable,
n_imputations = 5,
latent_dim = 8,
hidden_units = c(64, 32),
epochs = 300,
importance_samples = 20,
diffusion_steps = 100,
method = c("mixed", "miwae", "diffusion"),
impute = c("single", "multiple"),
history = FALSE,
seed = NULL
)
Arguments
data_fit_all |
As in |
long_sub_fixed |
As in |
long_sub_random |
As in |
time_variable |
Name of the visit-time column used, together with the id variable, to align rows across biomarkers. |
n_imputations |
Number of stochastic completions to draw. Also
controls how many completed datasets are returned when
|
latent_dim |
Dimension of the VAE's latent space. Only used when
|
|
Integer vector giving the hidden-layer sizes of the
network: the encoder (and, reversed, the decoder) when
| |
epochs |
Number of training epochs: full-batch gradient steps for
|
importance_samples |
Number of importance samples ( |
diffusion_steps |
Number of forward/reverse diffusion timesteps.
Only used when |
method |
|
impute |
Either |
history |
For the deep generative methods: whether each visit's
inputs include a summary of the same subject's other visits (see
|
seed |
Optional integer seed for reproducibility. It seeds both torch and R's random number generator for the duration of the call; R's random number state from before the call is restored afterwards. |
Details
Only the biomarker (response) columns named on the left-hand
side of long_sub_fixed are imputed, and they must be numeric
(continuous): an ordinal biomarker cannot be imputed with this function,
and gives an error. Every covariate referenced on
the right-hand side of long_sub_fixed/long_sub_random is
assumed fully observed (an error is raised if any covariate has missing
values). The deep generative methods model
p(biomarkers | covariates, missingness mask) row by row (one row
per subject-visit; the covariates include the visit time
time_variable when long_sub_fixed uses it), and use
long_sub_random only for the id variable; "mixed" uses the
full random-effects structure.
Rows are aligned across biomarkers by (id, time_variable): when
data_fit_all is a list of different data.frames with different
visit schedules per biomarker (rather than one data.frame shared by
every biomarker), the union of every (id, time_variable) pair seen
in any of them is used, so a visit at which one biomarker was measured
but another was not still gets a value imputed for the missing one.
Because the deep generative methods need enough data to fit
reliably; the diagnostics element of the return value also reports
a classical single regression imputation (each biomarker regressed on
the covariates, fit on its observed rows) as a lightweight baseline, so
the two can be compared on a given dataset before trusting the deep
model's completions.
Value
A named list with elements:
- data_fit_all
A list with one completed data frame per biomarker (also when
data_fit_allwas a single data frame), with every missing biomarker cell filled. Each holds one row per (id,time_variable) pair seen for any biomarker, and only the columns its model uses: the id,time_variable, the biomarker and the covariates of itslong_sub_fixedformula – other columns of the input are not carried over. Ready to pass straight tolongitudinalSubwith the same formulas.- data_fit_all_list
When
impute = "multiple", a list ofn_imputationscompleted versions ofdata_fit_all, each from its own draw;NULLwhenimpute = "single".- diagnostics
A named list, one element per biomarker (its
deep_generativeentry describes the completions of whichevermethodwas used), each with the number of missing cells and observed-vs-imputed mean/sd for both the deep-generative and classical-baseline completions.
References
Mattei, P.-A. and Frellsen, J. (2019). MIWAE: Deep Generative Modelling and Imputation of Incomplete Data Sets. Proceedings of the 36th International Conference on Machine Learning, PMLR 97:4413-4423.
Ho, J., Jain, A., and Abbeel, P. (2020). Denoising Diffusion Probabilistic Models. Advances in Neural Information Processing Systems, 33:6840-6851.
Tashiro, Y., Song, J., Song, Y., and Ermon, S. (2021). CSDI: Conditional Score-based Diffusion Models for Probabilistic Time Series Imputation. Advances in Neural Information Processing Systems, 34:24804-24816.
Examples
data(pbc3)
data_fit_all <- pbc3[pbc3$status3 == 1, ]
# pbc3 has no missing serBilir/albumin values; delete some at random
# (MCAR, a special case of MAR) to have something to impute
set.seed(1)
n <- nrow(data_fit_all)
data_fit_all$serBilir[sample.int(n, floor(0.1 * n))] <- NA
data_fit_all$albumin[sample.int(n, floor(0.1 * n))] <- NA
long_sub_fixed <- list(
"long1" = serBilir ~ year + age + sex + years,
"long2" = albumin ~ year + age + sex + years)
long_sub_random <- list("long1" = ~ year | id, "long2" = ~ year | id)
# multiple imputation from the mixed model, pooled with Rubin's rules
imputed <- imputeLongitudinal(data_fit_all, long_sub_fixed, long_sub_random,
time_variable = "year", impute = "multiple", seed = 1)
fits <- lapply(imputed$data_fit_all_list, longitudinalSub,
long_sub_fixed = long_sub_fixed, long_sub_random = long_sub_random)
pooled <- poolLongitudinalSub(fits)
# a deep generative method instead (needs torch)
if (requireNamespace("torch", quietly = TRUE) && torch::torch_is_installed()) {
imputed_miwae <- imputeLongitudinal(data_fit_all, long_sub_fixed, long_sub_random,
time_variable = "year", method = "miwae",
epochs = 50, seed = 1)
}
Upper limit of the prediction-to-infinity integration grid
Description
Shared helper for predictRisk and
dynamicPredictionBio. The denominator of a dynamic prediction
integrates over every event time after prediction_time, out to
infinity; the grid has to stop somewhere, and it should stop where the
probability left beyond it is negligible. So the upper limit is the
earliest time by which every at-risk patient's model-based conditional
survival S(t \mid x) / S(s \mid x), s = prediction_time,
has dropped below tail_prob, using the same (linearly
extrapolated, per-stratum) baseline cumulative hazard as
marginalT(). It never falls below min_upper.
This replaces twice the largest observed survival time of the
patients being predicted, which used each patient's own future outcome
(not available at prediction_time), stopped the integral early –
and inflated the risk – for a patient whose event came soon after
prediction_time, and failed when that time was missing.
Beyond the last time in the data survivalSub() was fit on, the
baseline hazard is an extrapolation; if it is so flat that the tail
criterion is not met by max_multiple times that last time, the
bound is capped there, with a warning if some patient's conditional
survival at the cap is still above warn_prob.
Usage
integration_upper_bound(
data_predict_all,
long_fit_all,
survival_fit_all,
prediction_time,
min_upper = prediction_time,
tail_prob = 1e-04,
max_multiple = 20,
warn_prob = 0.01
)
Arguments
data_predict_all |
At-risk prediction data (list of data frames). |
long_fit_all |
Output of |
survival_fit_all |
Output of |
prediction_time |
The prediction (landmark) time. |
min_upper |
The bound is at least this (e.g. |
tail_prob |
Remaining conditional survival probability treated as negligible. |
max_multiple |
Cap, as a multiple of the last training time. |
warn_prob |
Warn when the cap leaves more than this conditional survival probability unintegrated for some patient. |
Value
A single number.
Spline interpolation of a log box probability over candidate values
Description
For copula_logdens_grid(): a cubic spline (end
conditions from the end points' own cubic, not zero curvature, which a log
probability, curving like a parabola in its tails, does not have) through
the finite log probabilities at the exactly computed values
x, evaluated at x_out; outside the range of the finite ones
(where the probability underflowed to 0) the log probability is
-Inf.
Usage
interpolate_log_box(x, y, x_out)
Value
A vector, one value per element of x_out.
Interval endpoints from an interval-type Surv object
Description
L < T \le R for every subject: R = \infty when
right-censored, L = 0 when left-censored, L = R when T
is observed exactly.
Usage
interval_bounds(y)
Arguments
y |
A |
Value
A list with numeric vectors L and R.
Integration grid between the last negative visit and the prediction time
Description
Interval-censored fits only. With interval censoring an
event is only detected at the next visit, so at prediction_time
s a patient is known to be event-free only up to their last visit
V \le s (taken to be the last time in their biomarker history up to
s), not up to s itself. Conditioning on T > V instead of
T > s adds the stretch (V, s] to the denominator of every
prediction. This builds, for each patient, n equal intervals
tiling their own (V, s] – the stretch differs per patient, so the
grid does too – and the survival model's probability of each.
Returns NULL for a right-censored fit (an event is observed when it
happens, so V = s) and when every patient was seen at s, so
callers keep their right-censored code path unchanged.
Usage
interval_gap_grid(
data_predict_all,
long_fit_all,
survival_fit_all,
prediction_time,
time_variable,
n
)
Arguments
data_predict_all |
At-risk prediction data (list of data frames). |
long_fit_all |
Output of |
survival_fit_all |
Output of |
prediction_time |
The prediction time |
time_variable |
Visit-time column. |
n |
Grid intervals per patient. |
Value
NULL, or a list with last_visit (V, one per
patient), mids and S (n x patients matrices of
interval midpoints and probabilities; zero probability for a patient
with V = s), and eval(fun), which calls fun(data_j,
l_i) once per patient with a gap (data_j the patient's rows,
l_i their midpoints) and binds the resulting one-column results
into an n x patients matrix (filled with fill, -Inf
by default for log densities, for patients without a gap). fun
may also return a (nested) list of such columns, in which case
eval() returns the matching list of matrices.
Is the outcome of a survival formula interval-censored?
Description
Is the outcome of a survival formula interval-censored?
Usage
is_interval_surv(form_marginal_surv, data)
Predicted risks and observed outcomes at each landmark time
Description
Shared by performancePlot() and
calibrationPlot(): validates their common arguments and, for each
landmark s, calls predictRisk() on the subjects
event-free at s, with their history up to s and their
outcome hidden.
Usage
landmark_predictions(
data_predict_all,
long_fit_all,
survival_fit_all,
prediction_time,
horizon,
time_variable,
survival_variable_all,
survival_trans_function,
bandcount1,
bandcount2,
posterior = FALSE
)
Arguments
data_predict_all |
The evaluation data, in the same format as for
|
long_fit_all |
A |
survival_fit_all |
A |
prediction_time |
A vector of landmark times. |
horizon |
The prediction horizon (a single positive number): the window after each landmark in which events are counted. |
time_variable |
The name of the time variable in the linear mixed models. |
survival_variable_all, survival_trans_function |
As for
|
bandcount1, bandcount2 |
As for |
Value
A list with one element per landmark that had subjects to
predict, each a list with landmark, outcome (see
performance_outcome(), rows matching the predictions) and
risks (a list with one vector of predicted risks per event type).
Rank-one factor of the ordinal latent scores' covariance
Description
Writes cov as l l^T + I when cov - I has
rank at most one: a single ordinal random effect, as seen through the
patient's ordinal rows. Returns NULL (use pmvnorm())
otherwise, including when cov - I is not positive semi-definite
(the latent residual variance is then not 1).
Usage
latent_factor(cov)
Value
The vector l (all zero for rank zero), or NULL.
Fit nlme::lme(), retrying with more optimizer iterations
Description
lme() with opt = "optim" stops after
msMaxIter (default 50) optimizer iterations and then fails with
"optim problem, convergence error code = 1". That happens when a
random-effect variance is close to zero – e.g. a random slope in
long_sub_random for data with little slope variation, more often
with few measurements per subject. The call is evaluated as given first,
so a fit that succeeds is exactly what it was before (call included);
only if it fails to converge is it refit with msMaxIter = 1000,
with a warning. If that fails too, the error suggests a simpler
random-effects formula. Errors that are not about convergence are passed
on unchanged, without a retry.
Usage
lme_with_retry(call_expr, label)
Arguments
call_expr |
The |
label |
The biomarker's name, for messages. |
Value
The lme fit.
Seed R's RNG until the calling function returns
Description
Calls set.seed(seed) and registers, in the calling
function's frame, an exit handler that puts back the
.Random.seed that existed before (or removes it if there was
none), so a seed argument does not change the caller's random
number stream.
Usage
local_r_seed(seed, frame = parent.frame())
Arguments
seed |
Integer seed. |
frame |
The frame whose exit restores the RNG state. |
Fit a multivariate longitudinal sub-model
Description
Fits one linear mixed-effects model per longitudinal biomarker separately
(via lme), keeping each biomarker's own fixed-effects
and residual-variance estimates from that separate fit. The M
separate fits are then combined into a single multivariate model by
re-estimating the full random-effects variance-covariance matrix jointly
across all M biomarkers, via an EM algorithm initialized from the
block-diagonal covariance implied by the separate fits – so correlation
between biomarkers' random effects is captured, even though the fixed
effects and residual variances themselves are not re-estimated jointly
and remain exactly what each biomarker's own lme() fit produced.
This lets you assess the effect of predictor variables on several
longitudinal outcomes at once while accounting for both population-level
(fixed) effects and subject-level (random) variability, and for how the
outcomes covary within a subject. The result is one of the two sub-models
– together with survivalSub – that
predictRisk/dynamicPredictionBio
combine to produce dynamic risk/biomarker predictions.
Usage
longitudinalSub(
data_fit_all,
long_sub_fixed,
long_sub_random,
biomarker_type = NULL
)
Arguments
data_fit_all |
This process requires a set of |
long_sub_fixed |
This refers to a collection of formulas detailing the
fixed effects portion for each longitudinal outcome. On the left side of each formula,
the response variable is defined, while the right side outlines
the fixed effect terms. Should only a single formula be provided - whether
as a list with one item or as a standalone formula - it is inferred that
a conventional univariate joint model is being constructed.
Terms whose basis/contrasts depend on the data they are computed from –
|
long_sub_random |
A list of one-sided formulas that define the model for the
random effects of each longitudinal outcome.
The number of items in this |
biomarker_type |
Optional character vector, one entry per
longitudinal outcome, each either |
Value
An object of class "longitudinalSub.BJM", a named list with elements:
- lfit
A list of fitted univariate linear mixed models, one per longitudinal outcome, each obtained via
lme.- Sigma_fit
The estimated variance-covariance matrix of the random effects in the multivariate linear mixed model.
- long_sub_fixed
The
long_sub_fixedargument, as supplied.- long_sub_random
The
long_sub_randomargument, as supplied.- xlevels
A list, one element per longitudinal outcome, of the factor levels observed in the full training data for that outcome. Used internally at prediction time so that
poly()/splines::ns()/splines::bs()/factor()terms inlong_sub_fixedreuse the basis/contrasts fit at training time instead of recomputing one from a small, patient-specific slice of data.
Examples
long_sub_fixed = list(
"long1" = serBilir ~ year + age + sex + (years) + (years) * year,
"long2" = prothrombin ~ year + age + sex + (years) + (years) * year,
"long3" = albumin ~ year + age + age * year + sex + (years) + (years) * year,
"long4" = alkaline ~ year + age + sex + (years) + (years) * year,
"long5" = SGOT ~ year + age + sex + (years) + (years) * year,
"long6" = platelets ~ year + age + sex + (years) + (years) * year)
long_sub_random =list(
"long1" = ~ year| id,
"long2" = ~ year| id,
"long3" = ~ year| id,
"long4" = ~ year| id,
"long5" = ~ year| id,
"long6" = ~ year| id)
# Complete case analysis
data_fit_all = list()
for(i in seq_len(length(long_sub_fixed))){
data_fit_all[[i]] = pbc3[pbc3$status3 == 1, ]
}
# fitting longitudinal submodel
long_fit_all = longitudinalSub(data_fit_all, long_sub_fixed, long_sub_random)
# poly() in its default orthogonal mode, splines::ns()/bs(), and
# factor() are safe in a continuous biomarker's long_sub_fixed: the
# terms/xlevels/contrasts fit on the full training data are cached and
# reused when estimating Sigma_fit and at prediction time, instead of
# being recomputed from each patient's small per-prediction slice.
long_fit_poly = longitudinalSub(
pbc3[pbc3$status3 == 1, ],
serBilir ~ year + poly(age, 2) + factor(sex) + years,
~ year | id)
# A few more nonlinear-term styles. None of these would trigger the
# warning even in long_sub_random, because their basis doesn't depend on the surrounding
# data at all -- there's simply nothing that could disagree between the
# full training data and the small per-patient slice used at prediction
# time.
data_fit_extra_terms = pbc3[pbc3$status3 == 1, ]
# A transformed-time column can also be built once with survivalTrans()
# and merged onto the fitting data ahead of time -- by the time
# long_sub_fixed sees it, it is just an ordinary numeric column, same as
# any of the other terms below.
data_fit_extra_terms$Tyears1 = survivalTrans(c(1, 3, 5, 7))$survival_trans_function[[1]](
data_fit_extra_terms$year)
long_fit_nonlinear = list(
longitudinalSub(data_fit_extra_terms, serBilir ~ year + I(year^2) + age + sex,
~ year | id),
longitudinalSub(data_fit_extra_terms, serBilir ~ year + I(year^2) * age + sex,
~ year | id),
longitudinalSub(data_fit_extra_terms, serBilir ~ year + log(year + 1) + age + sex,
~ year | id),
longitudinalSub(data_fit_extra_terms, serBilir ~ year + sqrt(year) + age + sex,
~ year | id),
longitudinalSub(data_fit_extra_terms, serBilir ~ poly(year, 2, raw = TRUE) + age + sex,
~ year | id),
longitudinalSub(data_fit_extra_terms, serBilir ~ year + Tyears1 + age + sex,
~ year | id)
)
Fit a multivariate longitudinal sub-model with mixed continuous/ordinal biomarkers (Gaussian-copula extension)
Description
Internal counterpart to longitudinalSubGaussian(),
used by the longitudinalSub dispatcher whenever at least one
biomarker is categorical/ordinal. Each biomarker is still fit on its own
first, exactly following longitudinalSubGaussian()'s IFM strategy:
continuous biomarkers via nlme::lme() (byte-identical code path to
longitudinalSubGaussian()), ordinal biomarkers via
fit_marginal_ordinal() (ordinal::clmm() with a probit link).
The resulting per-biomarker fixed effects, thresholds, and random-effects
covariance blocks seed the same D/beta/sigma2
bookkeeping longitudinalSubGaussian() builds, which is then handed
to longitudinalSubVarCopula() (rather than
longitudinalSubVar()) to jointly re-estimate D across all
biomarkers, including the ordinal ones' latent Gaussian scores.
Usage
longitudinalSubCopula(
data_fit_all,
long_sub_fixed,
long_sub_random,
biomarker_type,
M
)
Fit a multivariate longitudinal sub-model (all-continuous biomarkers)
Description
Internal workhorse for longitudinalSub when
every biomarker is continuous. This is the original longitudinalSub
implementation, kept verbatim and called with the caller's pristine,
unmodified arguments, so that the all-continuous case behaves exactly as
it always has – see longitudinalSub for the argument
documentation, and longitudinalSubCopula() for the mixed
continuous/ordinal extension used when at least one biomarker is
categorical.
Usage
longitudinalSubGaussian(data_fit_all, long_sub_fixed, long_sub_random)
Variance-covariance matrix
Reference: package "lmm" and package "joineRML" function mvlme.
Description
The EM is accelerated by SQUAREM (Varadhan and Roland, 2008), and stops
once the relative change in D over an accelerated cycle (three EM
updates) is at most tol.em, or after max.iter EM updates
with a warning.
Usage
longitudinalSubVar(thetaLong, l, tol.em, verbose, max.iter = 1000)
Joint random-effects covariance with ordinal biomarkers (Gaussian copula)
Description
Re-estimates the shared random-effects covariance D
across all biomarkers by maximum likelihood, holding each biomarker's fixed
effects, residual variance and (for an ordinal biomarker) thresholds at
their separate-fit values, as longitudinalSubVar() does for
continuous ones. An ordinal observation is the category of a latent score
Z = \eta + \epsilon, \epsilon \sim N(0, 1) (probit link,
Albert & Chib), observed only through the thresholds that bracket it, so
the posterior of the random effects given the data is not Gaussian and the
EM's E[b b^T \mid y] has no closed form. It is computed by
-
copula_D_aghq(), an EM with adaptive Gauss-Hermite quadrature over the ordinal biomarkers' random effects (the continuous biomarkers' random effects are integrated out exactly), when those number at mostmax_quadrature_dim; deterministic, with the usual convergence check; otherwise
copula_D_saem(), a stochastic-approximation EM (SAEM) that draws the latent scores, whose cost does not grow with that dimension.
Both target the same maximum-likelihood estimate, but with the default
number of iterations SAEM stopped short of it: for two ordinal biomarkers
with a random intercept and slope each (four ordinal random effects), the
random-effects variances were off by about 5\
quadrature (5^4 = 625 nodes per subject) removes; hence the default
max_quadrature_dim = 4. The earlier version
plugged in the mean of each latent score given its category, which ignored
the score's own variance and underestimated the ordinal biomarkers'
random-effects variances (by about 40\
for a random slope in simulations).
Usage
longitudinalSubVarCopula(
thetaLong,
l,
biomarker_type,
thresholds,
tol.em = 1e-04,
max.iter = 500,
max_quadrature_dim = 4,
verbose = FALSE
)
Arguments
thetaLong |
List with the stacked fixed effects |
l |
Per-subject design bookkeeping built by
|
biomarker_type, thresholds |
As stored in the fit. |
tol.em, max.iter |
Convergence tolerance on |
max_quadrature_dim |
Largest number of ordinal random effects handled by quadrature. |
verbose |
Print the change in |
Value
A list with the estimate D.
Marginal distribution of T
Description
Marginal distribution of T
Usage
marginalT(data_predict_all, long_fit_all, survival_fit_all, l_i)
Arguments
data_predict_all |
This involves a collection of |
long_fit_all |
Outputs from the model fitting process using the |
survival_fit_all |
Results and parameters generated from the model fitting
procedure, utilizing the |
l_i |
A vector of time points to calculate the conditional probability. |
Value
Marginal density probability of the survival variable T is represented as a probability matrix. In this matrix, the rows (l_i) are aligned with specific time points, while the columns correspond to individual patients. Each entry in the matrix denotes the marginal density probability of survival for a given patient at a particular time point.
Point estimates, standard errors and complete-data df of one fit
Description
Helper for poolLongitudinalSub(). For a continuous
biomarker's nlme::lme() fit: the fixed effects, their standard
errors, and lme()'s own df per coefficient (fixDF$X) – a
between-subject covariate such as age has far fewer than the
within-subject time terms, so applying the intercept's df to every
coefficient would overstate the df of between-subject effects. For an
ordinal biomarker's ordinal::clmm() fit: the thresholds followed by
the slopes, their Wald standard errors, and an infinite complete-data df
(clmm()'s inference is asymptotic). Previously only lme()
fits were handled, and pooling a fit with an ordinal biomarker failed.
Usage
marker_estimates(fit)
Arguments
fit |
One biomarker's fitted marginal model. |
Value
A list with named vectors estimate, std_error and
dfcom.
Compare two prediction results' plain numeric-vector fields
Description
Shared comparison logic for checkBandcountConvergence()
and the "auto" bandcount support in predictRisk()/
dynamicPredictionBio(): only plain numeric vectors (no dim)
that have the same length in both results are compared. This naturally
skips fields whose size is itself controlled by the bandcount being
varied (e.g. dynamicPredictionBio()'s Y_density matrix and
Y_all grid, whose resolution is exactly what bandcount3
sets), while still comparing the actual per-patient estimates derived
from them (risk_prob_1/risk_prob_2, Y_predict).
Usage
max_relative_diff(result_a, result_b)
Value
A list with max (the largest relative change across all
comparable fields, or NA if none were comparable) and
by_field (a named numeric vector, one entry per comparable
field).
Draw K importance-weighted latent samples and decode them
Description
Internal helper shared by train_miwae and
miwae_impute: encodes each row once, draws K samples from
q(z|x_o) (reusing the same encoder mean/logvar for all K, as in the
original MIWAE), decodes all of them, and returns everything needed to
compute the importance-weighted log-likelihood terms.
Usage
miwae_forward(net, X_cov, X_resp, X_mask, K)
Draw self-normalized-importance-resampled completions from a fitted MIWAE
Description
Internal helper for imputeLongitudinal:
implements the MIWAE multiple-imputation procedure (Mattei & Frellsen,
2019, Section 3.2) – draw K decoder samples per row, importance-weight
them by their (masked-to-observed-entries) likelihood, then resample one
index per completion and use that draw's decoder mean plus Gaussian
noise as the imputed value. Each completion uses its own K samples, so
completions are independent draws. Already-observed cells are left
untouched by the caller; this only needs to return sensible values for
missing cells.
Usage
miwae_impute(net, X_cov, X_resp, X_mask, resp_mean, resp_sd, K, n_imputations)
mixed_density_prob_copula() for many means under one covariance
Description
Factors the parts of mixed_density_prob_copula()
that depend only on the covariance – the inverse and log-determinant of
the continuous block, and the conditional covariance of the ordinal
block – once, and returns a function that evaluates the log density for
many means, observed values and threshold bounds at once (one per
column), e.g. a patient's density at every integration grid point.
The ordinal box probability is computed by adaptive Gauss-Hermite
quadrature (box_prob_quadrature()) when the patient's ordinal rows
involve a single ordinal random effect (e.g. one ordinal biomarker with a
random intercept). Given the continuous values, the ordinal latent scores
then have covariance l l^T + I, l l^T from the random effect
and I from the latent residuals, so they are independent given the
random effect, and the probability is a one-dimensional integral of a
product of univariate normal probabilities. It used to be computed by
mvtnorm::pmvnorm()'s Monte Carlo algorithm, which dominated the run
time of every prediction with an ordinal biomarker, differed between runs
(relative error around 10^{-4}), and needed set.seed() to be
reproducible. With two or more ordinal random effects pmvnorm() is
still used: a product quadrature grid then needs so many nodes to match
its accuracy (a random intercept with a large variance makes the
integrand lopsided) that it is no faster. It is also used when the
covariance does not have this structure, and when box = "pmvnorm".
Usage
mixed_density_copula_prep(
Sigma_all,
cc_idx,
oo_idx,
box = c("quadrature", "pmvnorm")
)
Arguments
Sigma_all |
Full stacked covariance matrix for this patient (as
returned by |
cc_idx, oo_idx |
Integer index vectors (into the stacked row order) of the continuous- and ordinal-marker rows respectively. |
box |
|
Value
A function (mu_full, y_all_vec, alpha_lower_vec,
alpha_upper_vec) returning the log densities, with attribute
"monte_carlo": whether pmvnorm() is used. mu_full is a
vector or a matrix with one column per evaluation; the other three may
be vectors (the same for every column) or matrices of the same shape.
Joint density x probability for a mixed continuous/ordinal stacked vector
Description
Given one patient's full stacked mean vector and covariance
matrix (across all jointly-fit biomarkers' repeated measurements),
computes the exact Gaussian density at the observed values for the
continuous-marker rows, times the Gaussian-copula probability that the
unobserved ordinal-marker latent scores fall in the box implied by their
observed categories' cumulative-link thresholds – conditional on the
continuous rows' observed values (via the standard multivariate-normal
conditioning/Schur-complement formula). This is the quantity
conditionalYT()/conditionalYDT() compute directly as a
single Gaussian density when every marker is continuous (see the
quadratic-form comment in R/conditionalYT.R); setting
oo_idx = integer(0) here reduces to exactly that computation
(the full (2*pi)^{-n/2} normalizing constant is kept, not
dropped, even though it would cancel in predictRisk()'s own
ratio – dynamicPredictionBio() instead divides this function's
output by mvtnorm::dmvnorm()-normalized values from
conditionalYTBioCopula()/conditionalYDTBioCopula(), so the
constant must be correct in absolute terms, not just consistent within
one call, for Y_density to remain a genuine, correctly-scaled
conditional density – see R/conditionalYTBio.R's analogous
all-continuous computation, which keeps its own constant for the same
reason).
Returns the log of that density/probability, so that it neither
overflows nor underflows (see predictRisk() for how it is
exponentiated); -Inf if the ordinal box probability is 0.
Usage
mixed_density_prob_copula(
Sigma_all,
mu_full,
y_all_vec,
alpha_lower_vec,
alpha_upper_vec,
cc_idx,
oo_idx
)
Arguments
Sigma_all |
Full stacked covariance matrix for this patient (as
returned by |
mu_full |
Full stacked mean vector, same row order as |
y_all_vec |
Full stacked observed-value vector; entries at
|
alpha_lower_vec, alpha_upper_vec |
Full stacked threshold-bound
vectors; entries at |
cc_idx, oo_idx |
Integer index vectors (into the stacked row order) of the continuous- and ordinal-marker rows respectively. |
Multiple imputation from the multivariate linear mixed model
Description
method = "mixed" in imputeLongitudinal.
Fits the model longitudinalSub fits – each biomarker's
linear mixed model, with the random effects of all biomarkers jointly
normal with covariance Sigma_fit – and draws each subject's
missing values from their normal distribution given everything observed
for that subject: all of its visits and all biomarkers. For each
completion the model's parameters are first drawn from an approximation
to their posterior, so that the completions also reflect the uncertainty
in the fit (proper multiple imputation): the fixed effects from their
estimated sampling distribution, each residual variance from a scaled
inverse chi-squared distribution, and Sigma_fit from an inverse
Wishart distribution with as many degrees of freedom as subjects.
Usage
mixed_model_imputations(
aligned,
data_fit_all,
long_sub_fixed,
long_sub_random,
id,
resp_mat,
mask,
n_imputations
)
Value
A list of n_imputations matrices like resp_mat,
with the missing cells filled.
Variables a biomarker's sub-model uses
Description
The rows longitudinalSub() keeps for the joint EM
must be the rows lme() was fit on, i.e. complete in every
variable of both the fixed- and the random-effects formula (including
the id). Only the fixed-effects variables used to be checked, so a
missing value in a variable used only in long_sub_random left a
row in the fixed-effects design but not in the random-effects one, and
failed with "arguments imply differing number of rows".
Usage
model_vars(fixed_formula, random_formula)
Arguments
fixed_formula, random_formula |
The biomarker's
|
Value
A character vector of variable names.
Number of subjects the random-effects covariance is estimated from
Description
The EM update for Sigma_fit averages each subject's
E[b b^T] over the subjects that are actually in the fit – those with
at least one complete observation of every biomarker (yi has one
element per such subject). It used to divide by the number of subjects
in the first biomarker's raw data instead, which underestimated
Sigma_fit (badly, since EM compounds it over iterations) whenever
some subject was excluded, e.g. one never measured for some biomarker.
Usage
n_subjects(yi)
Arguments
yi |
The per-subject list of stacked responses. |
Value
The number of subjects.
Convert an nlme-style random-effects formula to an lme4-style bar term
Description
longitudinalSub()'s long_sub_random argument
uses nlme::lme()'s ~ terms | group formula convention.
ordinal::clmm() (used to fit the marginal model for a categorical/
ordinal biomarker – see fit_marginal_ordinal()) instead expects
random effects written as an lme4-style (terms | group) bar
term embedded directly in the model formula. This helper translates one
into the other so that the exact same long_sub_random argument
drives both nlme::lme() (continuous biomarkers) and
ordinal::clmm() (ordinal biomarkers), with no second random-effects
argument for the user to keep in sync.
Usage
nlme_random_to_lme4_bars(random_formula)
Value
A length-1 character string, e.g. "(year | id)".
Nonparametric estimate of the event-time distribution from interval data
Description
Self-consistency (EM) estimate in the spirit of Turnbull
(1976) for interval-censored event times (L, R], left-truncated at
V (each subject is only in the sample because T > V), with an
optional known event type per detected event (the competing-risks
estimate of Hudgens, Satten & Longini, 2001, Biometrics 57:74). Mass sits
on the cells between consecutive distinct time points (all L,
R, V and extra); within an observed interval it is not
identified, and the EM's choice there is what the estimate reports.
Usage
npmle_interval(
L,
R,
V,
cause = NULL,
extra = NULL,
tol = 1e-09,
max_iter = 5000
)
Arguments
L, R, V |
Interval and truncation time per subject ( |
cause |
Event type per subject (1..K, |
extra |
Additional time points to put cell boundaries at. |
tol, max_iter |
EM convergence settings. |
Value
A list with edges and mass (cells x event types,
the last cell running to Inf).
Latent-score means of an ordinal biomarker at every candidate event time
Description
X(l)\beta for an ordinal biomarker's rows, with the
design built the way copula_mean_grid() builds it
(the columns of lfit[[i]]$beta, which has no intercept: the
thresholds play that role).
Usage
ordinal_mean_on_grid(
data_i,
i,
long_fit_all,
xlev_i,
l_unique,
survival_variable,
survival_variable_all,
survival_trans_function
)
Value
A matrix with one row per row of data_i and one column per
element of l_unique.
Legend label for event type k in performancePlot()
Description
Legend label for event type k in performancePlot()
Usage
outcome_cause_label(survival_fit_all, k)
Outcome variables of the survival sub-model
Description
The survival-time variable of survival_fit_all and,
with competing risks, its event-type variable (the response of
form_conditional_cr). Both are unknown for a patient still at risk
at prediction_time; the prediction functions integrate over them.
Usage
outcome_variables(survival_fit_all)
Arguments
survival_fit_all |
Output of |
Value
A character vector of variable names.
Apply a function to every patient, on one core or several
Description
Used by the copula density functions, whose cost is per
patient. With getOption("BJM.n_cores") above 1 (set by the
n_cores argument of predictRisk() and
predictLongitudinal()) the patients are spread over that many
cores with parallel::mclapply() (serially on Windows). Each
patient is computed with its own fixed random-number seed, derived from
its id (patient_seed()), and the caller's random-number state is
restored afterwards, so the Monte Carlo box probabilities still used with
two or more ordinal random effects give the same result serially, in
parallel, and on every run, without set.seed().
Usage
patient_lapply(ids, FUN)
Arguments
ids |
Patient ids. |
FUN |
Function of one id. |
Value
A list, one element per id.
Per-patient shift for exponentiating log densities
Description
The conditional-density helpers (conditionalYT() and
relatives) return log densities, one column per patient. Every quantity
a prediction needs is a ratio of sums of those densities within one
patient, so any per-patient constant cancels: this returns, for each
patient, the largest finite log density across all the matrices given,
to subtract before exponentiating (see exp_shifted()). Without it
the densities – whose scale is the determinant of a covariance matrix
that grows with the number of observations and with the biomarkers'
units – underflowed to 0 or overflowed to Inf.
Usage
patient_log_shift(...)
Arguments
... |
Matrices of log densities, rows = grid points, columns = patients (all with the same columns). |
Value
A numeric vector, one shift per patient (0 for a patient
with no finite value).
One patient's stacked fixed-effects means at every grid point
Description
fixed_mean_grid() for each biomarker's rows of one
patient (data_num_i_list), stacked in biomarker order as in
build_conditional_design().
Usage
patient_mean_grid(
data_num_i_list,
long_fit_all,
l_i,
survival_variable,
survival_variable_all,
survival_trans_function,
fixed_values = list()
)
Value
A matrix, one row per stacked observation and one column per
grid point in l_i.
A fixed random-number seed for a patient
Description
Derived from the printed id, so that a patient gets the same seed whichever other patients are predicted alongside it.
Usage
patient_seed(id)
Value
An integer.
Mayo Clinic primary biliary cirrhosis data used as example code
Description
The dataset originates from the Mayo Clinic trial on primary biliary cirrhosis (PBC) of the liver, carried out from 1974 to 1984. It includes data from 424 PBC patients who were referred to the Mayo Clinic within this decade and met the eligibility requirements for a randomized placebo-controlled trial of D-penicillamine. However, only the initial 312 cases from the dataset were enrolled in the randomized trial. Thus, the dataset specifically pertains to these 312 patients, for whom the data is largely complete.
Usage
data(pbc3)
Format
A data frame with 1945 observations on the following 27 variables:
idpatients identifier; in total there are 312 patients.
yearsnumber of years between registration and the earlier of death, transplantation, or study analysis time.
statusa factor with levels
alive,transplantedanddead.druga factor with levels
placeboandD-penicil.ageat registration in years.
sexa factor with levels
maleandfemale.yearnumber of years between enrollment and this visit date, remaining values on the line of data refer to this visit.
ascitesa factor with levels
NoandYes.hepatomegalya factor with levels
NoandYes.spidersa factor with levels
NoandYes.edemaa factor with levels
No edema(i.e. no edema and no diuretic therapy for edema),edema no diuretics(i.e. edema present without diuretics, or edema resolved by diuretics), andedema despite diuretics(i.e. edema despite diuretic therapy).serBilirserum bilirubin in mg/dl.
serCholserum cholesterol in mg/dl.
albuminalbumin in mg/dl.
alkalinealkaline phosphatase in U/liter.
SGOTSGOT in U/ml.
plateletsplatelets per cubic ml/1000.
prothrombinprothrombin time in seconds.
histologichistologic stage of disease.
status2a numeric vector with the value 1 denoting if the patient was dead, and 0 if the patient was alive or transplanted.
status3a numeric vector with the value 1 denoting if the patient was dead or transplanted, and 0 if the patient was alive.
status4a numeric vector with the value 1 denoting if the patient was transplanted, and 0 if the patient was dead. Used for competing risks.
status5a numeric vector with the value 2 if the patient was transplanted, 1 denoting if the patient was dead, and 0 if the patient was alive. Used for competing risks with censored.
Tyears1a numeric vector with a transformed value of time-to-event outcome.
Tyears2a numeric vector with a transformed value of time-to-event outcome.
Tyears3a numeric vector with a transformed value of time-to-event outcome.
Tyears4a numeric vector with a transformed value of time-to-event outcome.
Source
pbc.
References
Fleming T, Harrington D. Counting Processes and Survival Analysis. 1991; New York: Wiley.
Therneau T, Grambsch P. Modeling Survival Data: Extending the Cox Model. 2000; New York: Springer-Verlag.
Inverse of a covariance matrix that may be close to singular
Description
By the eigendecomposition, with eigenvalues floored at
1e-10 times the largest, so a covariance estimate approaching the
boundary (e.g. a random-effect variance tending to 0) still gives a finite,
positive-definite precision.
Usage
pd_inverse(V)
Arguments
V |
Symmetric matrix. |
Value
Its (regularized) inverse.
Plot predictive performance across landmark times
Description
Evaluates how well the backward joint model's dynamic risk predictions
discriminate and are calibrated, at each of several landmark times. For
each landmark s in prediction_time, every subject in
data_predict_all still event-free at s gets a predicted risk
from predictRisk, using only their longitudinal history up
to s, of an event in (s, s + horizon]; this is compared with
what actually happened, by
- AUC
the time-dependent (cumulative/dynamic) area under the ROC curve: the probability that a subject with an event in the window has a higher predicted risk than one still event-free at
s + horizon. 0.5 is no better than chance, 1 is perfect.- Brier score
the mean squared difference between the predicted risk and the observed event indicator in the window. Lower is better.
Subjects censored within the window are handled by inverse probability of
censoring weighting (IPCW), with the censoring distribution among the
subjects at risk at s estimated by Kaplan–Meier. With competing
risks, both measures are computed separately for each event type: the
cases are the subjects with that event type in the window, and for the
Brier score a subject with the other event type in the window counts as
not having the event.
Evaluating on the data the sub-models were fit on gives an optimistic
(apparent) performance; pass held-out validation data in
data_predict_all when available.
Usage
performancePlot(
data_predict_all,
long_fit_all,
survival_fit_all,
prediction_time,
horizon,
time_variable,
survival_variable_all,
survival_trans_function,
bandcount1 = "auto",
bandcount2 = "auto",
interval_method = c("model", "ipcw")
)
Arguments
data_predict_all |
The evaluation data, in the same format as for
|
long_fit_all |
A |
survival_fit_all |
A |
prediction_time |
A vector of landmark times. |
horizon |
The prediction horizon (a single positive number): the window after each landmark in which events are counted. |
time_variable |
The name of the time variable in the linear mixed models. |
survival_variable_all, survival_trans_function |
As for
|
bandcount1, bandcount2 |
As for |
interval_method |
Only for an interval-censored
|
Value
A ggplot object with one panel per measure, the landmark
time on the horizontal axis. Its data element is a
data.frame with one row per landmark, measure (and event type), and
columns landmark, measure, cause, value,
n_at_risk (subjects predicted) and n_cases (events of that
type observed in the window).
Interval-censored fits (experimental)
The event is then only
known to lie between the last negative visit L and the visit
R that detected it. A subject is at risk at s when followed
up past s without a detected event, and the predicted risk is
that of predictRisk() (conditional on being event-free at the last
visit). Following Yang, Rizopoulos, Newcomb and Erler (2026, Biometrical
Journal 68:e70108), interval_method chooses how subjects whose
interval straddles s or s + horizon are handled:
-
"model"(default): every subject at risk counts, as a case or a control with the probability the fitted model gives their event time given their observed interval. The model only splits the interval observed for each subject, so in our simulations (visits every 1–3 years, 2-year window) both measures were within about 0.003 of their true values, also with a survival sub-model that left out a covariate. The model does grade itself, though: for a deliberately miscalibrated model the Brier score came out slightly too good (by about 0.003), so judge calibration withcalibrationPlot, which does not use the model. -
"ipcw": only subjects whose status is certain are used – cases detected inside the window after a negative visit at or afters, controls with a negative visit at or afters + horizon– weighted by the inverse Kaplan–Meier probability of still being followed up. It does not use the model, but these weights do not account for how likely the status of a subject is to be certain, which depends on the visit schedule: in the same simulations only about 5 of 60 cases were certain, the Brier score came out at about 0.03 instead of 0.13, and the AUC was about 0.01 too high with three times the error of"model". With visits every 3–9 months the Brier score was still about 25% too low. Use it as a model-free check of the AUC.
Examples
data(pbc3)
survival_fit_all <- survivalSub(pbc3[!duplicated(pbc3$id), ],
Surv(years, status3) ~ age + sex, NULL)
long_sub_fixed <- list("long1" = serBilir ~ year + age + sex + years)
long_sub_random <- list("long1" = ~ year | id)
long_fit_all <- longitudinalSub(list(pbc3[pbc3$status3 == 1, ]),
long_sub_fixed, long_sub_random)
trans <- survivalTrans(c(1, 3, 5, 7))
# apparent performance on the fitting data, 2-year window
performancePlot(pbc3, long_fit_all, survival_fit_all,
prediction_time = c(1, 3, 5), horizon = 2, time_variable = "year",
trans$survival_variable_all, trans$survival_trans_function)
IPCW time-dependent AUC and Brier score at one landmark
Description
IPCW time-dependent AUC and Brier score at one landmark
Usage
performance_metrics(risk, time, status, cause, k, s, horizon)
Arguments
risk |
Predicted risks of event type |
time, status, cause |
Observed outcome of the same subjects (see
|
k |
Event type evaluated (ignored when |
s, horizon |
Landmark and window length. |
Value
A list with auc, brier, n_at_risk,
n_cases.
IPCW AUC and Brier score for interval-censored outcomes at one landmark
Description
Uses only subjects whose status in (s, e] is certain:
cases of type k (last negative visit L \ge s, event detected
at R \le e), the other event type detected the same way (a non-case
in the Brier score), and controls (L \ge e). They are weighted by the
inverse probability of still being followed up – at R for an
event, at e for a control – from the Kaplan–Meier estimate of the
end of follow-up among the subjects at risk at s, as in Yang et al.
(2026), Biometrical Journal 68:e70108. Subjects whose interval straddles
s or e get weight 0.
Usage
performance_metrics_interval_ipcw(risk, L, R, followup, cause, k, s, horizon)
Arguments
risk |
Predicted risks of event type |
L, R, followup, cause |
Observed outcome (see
|
k, s, horizon |
Event type, landmark and window length. |
Value
As performance_metrics(), plus n_uncertain.
Model-based AUC and Brier score for interval-censored outcomes
Description
Every subject at risk counts, as a case with probability
p_case and a control with probability p_control
(window_status_probs()): the expected squared error for the Brier
score, and case/control pairs of different subjects weighted by
p_i q_j for the AUC (Yang et al., 2026). These weights come from the
model being evaluated, so the measures lean optimistic when that model is
misspecified.
Usage
performance_metrics_interval_model(risk, p_case, p_control)
Arguments
risk |
Predicted risks of the event type evaluated. |
p_case, p_control |
Case and control probabilities per subject. |
Value
As performance_metrics(), with n_cases the expected
number of cases.
One row per subject with the observed outcome, for performancePlot()
Description
One row per subject with the observed outcome, for performancePlot()
Usage
performance_outcome(data, survival_fit_all, id_variable)
Arguments
data |
The first |
survival_fit_all |
A |
id_variable |
The subject ID column. |
Value
A data.frame with columns id, time,
status (1 = event, 0 = censored) and cause (1 or 2 for the
event type matching risk_prob_1/risk_prob_2 of
predictRisk(); NA without competing risks or when censored).
Observed outcome per subject for an interval-censored fit
Description
The interval-censored counterpart of
performance_outcome(): each subject's event is known only to lie in
(L, R] (R = Inf if no event was detected), with its type
when detected.
Usage
performance_outcome_interval(data, survival_fit_all, id_variable)
Arguments
data |
The first |
survival_fit_all |
An interval-censored |
id_variable |
The subject ID column. |
Value
A data.frame with columns id, L, R,
followup (end of follow-up: R after an event, L
otherwise) and cause (1 or 2, matching
risk_prob_1/risk_prob_2; NA without competing risks
or without an event).
Cumulative hazard of a piecewise-constant hazard
Description
Cumulative hazard of a piecewise-constant hazard
Usage
piecewise_cumhaz(t, lambda, cuts)
Arguments
t |
Times. |
lambda |
Hazard on each piece. |
cuts |
Piece boundaries, starting at 0 and ending at |
Plot a fitted longitudinal sub-model
Description
Diagnostic plots for the per-biomarker mixed models and the shared
random-effects covariance of a longitudinalSub fit, one
panel per biomarker:
"residuals"Standardized (Pearson) residuals against fitted values, with a LOESS smoother; a trend or a funnel shape suggests a misspecified mean or non-constant variance.
"qq"Normal Q-Q plot of the standardized residuals.
"ranef"Normal Q-Q plot of each predicted random effect (the model assumes they are normal).
"corr"Heat map of the correlation matrix of the multivariate random-effects covariance
Sigma_fit, i.e. how the subject-level deviations of the different biomarkers move together.
Residual plots are drawn for continuous biomarkers only; an ordinal
biomarker (fit by ordinal::clmm()) has no residuals on the
response scale and is left out of them with a message.
Usage
## S3 method for class 'longitudinalSub.BJM'
plot(x, which = c("residuals", "qq", "ranef", "corr"), ...)
Arguments
x |
A |
which |
Which plot to draw: |
... |
Currently unused. |
Value
A ggplot object.
Examples
data(pbc3)
long_sub_fixed <- list("long1" = serBilir ~ year + age + sex + years,
"long2" = albumin ~ year + age + sex + years)
long_sub_random <- list("long1" = ~ year | id, "long2" = ~ year | id)
data_fit <- pbc3[pbc3$status3 == 1, ]
long_fit_all <- longitudinalSub(list(data_fit, data_fit), long_sub_fixed, long_sub_random)
plot(long_fit_all)
plot(long_fit_all, which = "qq")
plot(long_fit_all, which = "ranef")
plot(long_fit_all, which = "corr")
Plot predicted biomarker distributions
Description
Plots the predicted distribution of the biomarker at
prediction_time + horizon returned by predictLongitudinal,
one curve per patient, with a dashed line at the patient's point
prediction (the most likely value, Y_predict). For an ordinal
biomarker, plots each patient's predicted category probabilities as bars
instead.
Usage
## S3 method for class 'predictLongitudinal.BJM'
plot(x, subject = NULL, ...)
Arguments
x |
A |
subject |
Patients to plot, as ids (the names of |
... |
Currently unused. |
Value
A ggplot object.
Examples
data(pbc3)
survival_fit_all <- survivalSub(pbc3[!duplicated(pbc3$id), ],
Surv(years, status3) ~ age + sex, NULL)
long_sub_fixed <- list("long1" = serBilir ~ year + age + sex + years)
long_sub_random <- list("long1" = ~ year | id)
long_fit_all <- longitudinalSub(list(pbc3[pbc3$status3 == 1, ]),
long_sub_fixed, long_sub_random)
data_predict <- pbc3[pbc3$id %in% c(2, 4, 5) & pbc3$year <= 3, ]
data_predict$years <- NA
trans <- survivalTrans(c(1, 3, 5, 7))
pred <- predictLongitudinal(data_predict, long_fit_all, survival_fit_all,
prediction_time = 3, horizon = 1, time_variable = "year",
trans$survival_variable_all, trans$survival_trans_function, bio_i = 1)
plot(pred)
Plot simulated futures
Description
Plots the draws of simulateTrajectory, pooling every draw of
the patients chosen by id (all of them by default: one patient's
draws picture that patient's predictive distribution, one draw each of a
synthetic cohort pictures the cohort):
"trajectory"One panel per biomarker. For a continuous biomarker,
n_pathsof the simulated trajectories as thin lines, with the median and the centrallevelinterval of the draws at each time, and the measurements conditioned on as points. For an ordinal biomarker, the share of the draws in each category at each time. Withtruncate = TRUEinsimulateTrajectory()the summaries at a time describe the draws still event-free then."event"The cumulative incidence of the event (of each event type, with competing risks) after
prediction_time, computed from the drawn event times.
Usage
## S3 method for class 'simulateTrajectory.BJM'
plot(
x,
which = c("trajectory", "event"),
id = NULL,
n_paths = 30,
level = 0.9,
...
)
Arguments
x |
A |
which |
|
id |
Patient id(s) whose draws to plot; |
n_paths |
Number of simulated trajectories drawn as lines (the first ones; the draws are independent, so they are a random sample). |
level |
Coverage of the interval drawn around the median. |
... |
Currently unused. |
Value
A ggplot object.
Examples
data(pbc3)
survival_fit_all <- survivalSub(pbc3[!duplicated(pbc3$id), ],
Surv(years, status3) ~ age + sex, NULL)
long_fit_all <- longitudinalSub(list(pbc3[pbc3$status3 == 1, ], pbc3[pbc3$status3 == 1, ]),
list(serBilir ~ year + age + sex + years,
albumin ~ year + age + sex + years),
list(~ year | id, ~ year | id))
history <- pbc3[pbc3$id == 2 & pbc3$year <= 3, ]
sims <- simulateTrajectory(history, long_fit_all, survival_fit_all,
prediction_time = 3, times = seq(3, 8, by = 0.5),
time_variable = "year", survival_variable_all = list(),
survival_trans_function = list(), n_sim = 200, seed = 1)
plot(sims)
plot(sims, which = "event")
Plot a fitted survival sub-model
Description
"forest"Forest plot of the hazard ratios (with 95\ confidence intervals) of the Cox marginal survival model and, when the fit has competing risks, of the odds ratios (with 95\ intervals) of the logistic event-type model, on a log scale with a reference line at 1.
"basehaz"The Cox model's baseline cumulative hazard (covariates at 0, not centered), one step curve per stratum.
Usage
## S3 method for class 'survivalSub.BJM'
plot(x, which = c("forest", "basehaz"), ...)
Arguments
x |
A |
which |
Which plot to draw: |
... |
Currently unused. |
Value
A ggplot object.
Examples
data(pbc3)
data_survival_fitting <- pbc3[!duplicated(pbc3$id), ]
survival_fit_all <- survivalSub(data_survival_fitting,
Surv(years, status3) ~ age + sex,
status4 ~ years + age + sex)
plot(survival_fit_all)
plot(survival_fit_all, which = "basehaz")
Pool longitudinal sub-model fits across multiple imputations with Rubin's rules
Description
When imputeLongitudinal is run with
impute = "multiple", it returns n_imputations independently
completed versions of data_fit_all in data_fit_all_list,
instead of a single completed dataset filled with the across-draw mean
(impute = "single"). Fitting longitudinalSub once per
completion and combining the resulting fixed-effect estimates with
Rubin's rules (Rubin, 1987) – rather than averaging into one completed
dataset up front – is the standard way multiple imputation propagates
the extra uncertainty from not knowing the true missing values into the
final standard errors; impute = "single" does not do this (see
the "Missing data imputation" article on the package website).
poolLongitudinalSub() performs that combination step.
Usage
poolLongitudinalSub(long_fit_all_list)
Arguments
long_fit_all_list |
A list of two or more |
Details
For each fixed-effect coefficient, across the m fits in
long_fit_all_list:
the pooled estimate is the mean of the
mper-completion estimates (\bar{Q});the pooled variance (
T) adds the average within-imputation variance (\bar{U}, from each fit's own standard error) to the between-imputation variance (B, the sample variance of themestimates) inflated by a factor of1 + 1/m:T = \bar{U} + (1 + 1/m) B;the pooled degrees of freedom use the Barnard & Rubin (1999) adjustment, which interpolates between the classical Rubin (1987) degrees of freedom (based only on
mand the fraction of missing information) and the complete-data degrees of freedom reported by eachnlme::lme()fit. This adjustment matters most at the small sample sizes typical of this package's use case, where the classical, unadjusted degrees of freedom can otherwise come out larger than the complete-data degrees of freedom, which is not sensible.
A large fraction of missing information (fmi, close to 1) for a
coefficient means the missing data cost a lot of precision for that
coefficient specifically, even if the biomarker's overall missingness
rate is modest.
Value
An object of class "poolLongitudinalSub.BJM", a named list
with elements:
- pooled
A list, one
data.frameper longitudinal outcome, with one row per fixed-effect coefficient and columnsestimate,std_error,df,statistic,p_value,riv(relative increase in variance due to missingness), andfmi(fraction of missing information).- m
The number of completed-data fits pooled.
- long_sub_fixed
The
long_sub_fixedused by every fit (taken from the first element oflong_fit_all_list).- long_fit_all
A
longitudinalSub.BJMobject usable for prediction (e.g. aslong_fit_allinpredictRisk): the first fit, with each biomarker's fixed effects replaced by their pooled estimates, its residual variance by the average across completions, andSigma_fitby the average of themfits'Sigma_fit(Rubin's rules point estimates). Averaging themfits' predictions instead is a common alternative.
References
Rubin, D. B. (1987). Multiple Imputation for Nonresponse in Surveys. John Wiley & Sons.
Barnard, J. and Rubin, D. B. (1999). Small-Sample Degrees of Freedom with Multiple Imputation. Biometrika, 86(4):948-955.
Examples
if (requireNamespace("torch", quietly = TRUE) && torch::torch_is_installed()) {
data(pbc3)
data_fit_all <- pbc3[pbc3$status3 == 1, ]
set.seed(1)
n <- nrow(data_fit_all)
data_fit_all$serBilir[sample.int(n, floor(0.1 * n))] <- NA
data_fit_all$albumin[sample.int(n, floor(0.1 * n))] <- NA
long_sub_fixed <- list(
"long1" = serBilir ~ year + age + sex + years,
"long2" = albumin ~ year + age + sex + years)
long_sub_random <- list("long1" = ~ year | id, "long2" = ~ year | id)
imputed <- imputeLongitudinal(data_fit_all, long_sub_fixed, long_sub_random,
time_variable = "year", n_imputations = 5,
impute = "multiple", epochs = 50, seed = 1)
long_fit_all_list <- lapply(imputed$data_fit_all_list, function(d) {
longitudinalSub(d, long_sub_fixed, long_sub_random)
})
pooled <- poolLongitudinalSub(long_fit_all_list)
pooled
}
Average the event-type model over the imputations
Description
With competing risks fitIntervalBJM() refits the
event-type glm on every imputation, since it uses the event time.
For prediction the coefficients are averaged over the imputations (the
Rubin's-rules point estimate); the rest of the returned glm object
is the first imputation's fit.
Usage
pool_event_type_fit(survival_fit_all_list)
Arguments
survival_fit_all_list |
One |
Value
The first fit, with the averaged event-type coefficients.
A prediction-ready fit from Rubin's-rules point estimates
Description
Helper for poolLongitudinalSub(): see its
long_fit_all return element.
Usage
pooled_longitudinal_fit(long_fit_all_list, pooled)
Arguments
long_fit_all_list |
The completed-data fits. |
pooled |
|
Value
A longitudinalSub.BJM object.
Dynamic prediction function for future longitudinal outcomes
Description
Companion to predictRisk, using the same
fitted longitudinal (longitudinalSub) and survival
(survivalSub) sub-models, but instead of an event-risk
probability this returns a predictive density for a future value of one
or more chosen biomarkers (bio_i) at prediction_time +
horizon, conditional on the subject's observed longitudinal history up
to prediction_time and on survival (being event-free) through
prediction_time + horizon, the time the biomarker is predicted at.
This is the single, unified entry point for biomarker-value prediction:
If
bio_inames exactly one biomarker, this predicts just that biomarker and returns a single"predictLongitudinal.BJM"object.If
bio_inames more than one biomarker, or is left at its defaultNULL(meaning every biomarker inlong_fit_all), this instead predicts every requested biomarker and returns a named list of"predictLongitudinal.BJM"objects (classed"predictLongitudinalAll.BJM"), computing the bio_i-independent part of the pipeline (restricting to at-risk patients, the survival-side integration grid, and the denominator conditional density) only once and reusing it across every biomarker, instead of recomputing it once per biomarker. This matters most under the Gaussian-copula path (seelongitudinalSubCopula), where that denominator involvesmvtnorm::pmvnorm()Monte-Carlo evaluations that are otherwise the dominant cost of a multi-biomarker prediction run.
See Value for the exact return shape in each case.
bio_i may refer to either continuous or ordinal
biomarkers (see longitudinalSubCopula), and a single call
may mix both. For an ordinal biomarker, Y_all/Y_predict hold
integer category codes (1:K, in the fitted factor's
levels()/threshold order) rather than a numeric grid, with
Y_all additionally carrying a "category_labels" attribute
giving the matching level-label strings; bandcount3 is ignored for
that biomarker, since its candidate grid is fixed at its category count
(see Details).
The prediction is conditional on the longitudinal history observed up to
prediction_time: rows of data_predict_all whose
time_variable is later than prediction_time are dropped,
with a warning, before predicting.
bandcount2 (controlling the shared survival-integration grid) and
bandcount3 (controlling each biomarker's own candidate-value grid)
are auto-tuned separately when left at their default "auto", via
the doubling-until-stable check described in Details. When bio_i
names more than one biomarker, bandcount2 is resolved once
using a single representative biomarker (the first one in bio_i),
and bandcount3 is then resolved independently for every
requested biomarker (their candidate-value grids need not converge at the
same resolution) – except for any ordinal biomarker, for which
bandcount3 tuning is always skipped (see above) and
"bandcount3" is recorded as NA for that biomarker.
Usage
predictLongitudinal(
bio_i = NULL,
data_predict_all,
long_fit_all,
survival_fit_all,
prediction_time,
horizon,
time_variable,
survival_variable_all,
survival_trans_function,
bandcount2 = "auto",
bandcount3 = "auto",
n_cores = 1
)
Arguments
bio_i |
Integer vector of biomarkers to predict. May include
continuous and/or ordinal biomarkers (see Details). Defaults to
|
data_predict_all |
This involves a collection of |
long_fit_all |
Outputs from the model fitting process using the
|
survival_fit_all |
Results and parameters generated from the model
fitting procedure, utilizing the |
prediction_time |
Time used to make the prediction |
horizon |
Prediction horizon |
time_variable |
The name of time variable in linear mixed model. |
survival_variable_all |
The name of the transformed time-to-event outcomes variable. |
survival_trans_function |
The transformation function used for
time-to-event outcomes, in the order of |
bandcount2 |
The number of grid points spanning
|
bandcount3 |
The number of points in the candidate-biomarker-value
grid ( |
n_cores |
Number of CPU cores. With an ordinal biomarker the
prediction's cost is per patient, and the patients are spread over
|
Details
There is no universal correct value for bandcount2/bandcount3:
as a practical check, double both and confirm the results barely change;
if they do, keep doubling. By default (bandcount2 = "auto",
bandcount3 = "auto"), this doubling check is done for you:
starting from small built-in values, both are doubled together, and the
result is compared to the previous round, until the largest relative
change in Y_predict drops below 1%, or 2 doublings have been
tried (so at most 3 calls' worth of work). If it still has not converged
by then, a warning reports this and the result at the largest value
tried is returned anyway (not an error), so this never silently loops
for an unbounded amount of time. Pass an explicit number for either
argument to skip auto-tuning it and use a fixed value instead, or call
checkBandcountConvergence() directly for more control over the
tolerance and doubling count. See also
vignette("BJM-intro", package = "BJM") for a worked example.
The integration grid, and the candidate-value grid of a continuous biomarker (which spans the observed values of every patient predicted), are shared by all patients predicted in one call, so a patient's value can differ slightly, within the grids' discretization error, depending on which other patients are predicted alongside it.
The denominator integrates over every event time after
prediction_time, including times beyond the last follow-up time
in the data survivalSub() was fit on. There, the baseline
cumulative hazard is extrapolated linearly (a constant hazard), and the
longitudinal sub-model's mean is evaluated at event times it was never
fit on. When much of an at-risk patient's survival probability lies
beyond the last follow-up time (e.g. a prediction late in follow-up),
the prediction depends on this extrapolation; in pbc3 it moved
the risks checked by less than one percentage point.
For a fit with an ordinal biomarker, a patient's ordinal measurements
contribute the probability of their observed categories. With a single
ordinal random effect (e.g. one ordinal biomarker with a random
intercept) it is computed by Gauss-Hermite quadrature, and is exact; with
more, by Monte Carlo (mvtnorm::pmvnorm()), with an error around the
fourth significant digit. Each patient's Monte Carlo draws use a fixed
seed derived from the patient's id, so the results are the same on every
run and for any n_cores, without set.seed(), and the
caller's random-number state is left unchanged.
Value
If bio_i names exactly one biomarker: an object of class
"predictLongitudinal.BJM", a named list with elements:
- Y_predict
A vector, one entry per at-risk patient (named by patient id), giving the MAP (most likely) predicted value of the biomarker at
prediction_time + horizon. For an ordinal biomarker, this is an integer category code (see Details), not a raw value.- Y_density
A probability matrix whose rows correspond to the candidate biomarker values in
Y_alland whose columns correspond to individual patients; each entry is the dynamically predicted density of the biomarker taking that value. For an ordinal biomarker, each row is instead that category's predicted probability.- Y_all
The grid of candidate biomarker values used to build
Y_density. For an ordinal biomarker, this is the vector of integer category codes1:K, carrying a"category_labels"attribute with the matching level-label strings (see Details).
Otherwise (bio_i names more than one biomarker, or is NULL):
a named list of such objects, one per requested biomarker, named by that
biomarker's response-variable name; with attributes "bandcount2"
(the single resolved/used bandcount2) and "bandcount3" (a
named numeric vector of the resolved/used bandcount3 for each
biomarker, or NA for an ordinal biomarker). Classed
"predictLongitudinalAll.BJM".
Examples
data(pbc3)
data_survival_fitting = pbc3[!duplicated(pbc3$id), ]
form_marginal_surv = Surv(years, status3) ~ age + sex
form_conditional_cr = NULL
survival_fit_all = survivalSub(data_survival_fitting, form_marginal_surv,
form_conditional_cr)
long_sub_fixed = list(
"long1" = serBilir ~ year + age + sex + (years) + (years) * year,
"long2" = prothrombin ~ year + age + sex + (years) + (years) * year,
"long3" = albumin ~ year + age + age * year + sex + (years) + (years) * year,
"long4" = alkaline ~ year + age + sex + (years) + (years) * year,
"long5" = SGOT ~ year + age + sex + (years) + (years) * year,
"long6" = platelets ~ year + age + sex + (years) + (years) * year)
long_sub_random =list(
"long1" = ~ year| id,
"long2" = ~ year| id,
"long3" = ~ year| id,
"long4" = ~ year| id,
"long5" = ~ year| id,
"long6" = ~ year| id)
survival_variable_all = list(
"Tyears1", "Tyears2", "Tyears3", "Tyears4"
)
survival_trans_function = list(
fun1 = function(x){abs(x - 1)},
fun2 = function(x){abs(x - 3)},
fun3 = function(x){abs(x - 5)},
fun4 = function(x){abs(x - 7)}
)
# Complete case analysis
data_fit_all = list()
for(i in seq_len(length(long_sub_fixed))){
data_fit_all[[i]] = pbc3[pbc3$status3 == 1, ]
}
# fitting longitudinal submodel
long_fit_all = longitudinalSub(data_fit_all, long_sub_fixed, long_sub_random)
i_PID = 2
data.raw.predict.1 = pbc3[pbc3$id == i_PID, ]
data_predict_all = list()
for(i in seq_len(length(long_sub_fixed))){
data_predict_all[[i]] = data.raw.predict.1[data.raw.predict.1$year <= 3,]
}
# A single biomarker -> a single predictLongitudinal.BJM result
Y_predict = predictLongitudinal(bio_i = 1, data_predict_all, long_fit_all,
survival_fit_all, prediction_time = 3,
horizon = 3, time_variable = "year",
survival_variable_all, survival_trans_function,
bandcount2 = 40, bandcount3 = 400)
# Multiple biomarkers (or bio_i = NULL for all of them) -> a named list
Y_predict_all = predictLongitudinal(bio_i = NULL, data_predict_all, long_fit_all,
survival_fit_all, prediction_time = 3,
horizon = 3, time_variable = "year",
survival_variable_all, survival_trans_function,
bandcount2 = 40, bandcount3 = 400)
Plot risk and future biomarker predictions across a horizon sweep
Description
Fixes the landmark time at prediction_time and sweeps forward over
a range of prediction horizons, calling predictRisk
(and, when bio_pred is supplied, predictLongitudinal)
at each horizon to trace out how predicted event risk (and, optionally,
the predicted density/point-forecast of biomarker bio_pred) evolves
the further out the prediction window extends from a single fixed
landmark. This contrasts with riskPlot, which instead fixes
the horizon and sweeps over a set of landmark times.
Usage
predictPlot(
data_predict_all_one,
long_fit_all,
survival_fit_all,
prediction_time = 4,
horizon = seq(0, 3, 0.5),
time_variable,
survival_variable_all,
survival_trans_function,
bandcount1 = "auto",
bandcount2 = "auto",
bandcount3 = "auto",
bio_his = 1,
bio_pred = 1,
density = 1,
n_cores = 1
)
Arguments
data_predict_all_one |
This involves a collection of |
long_fit_all |
Outputs from the model fitting process using the |
survival_fit_all |
Results and parameters generated from the model fitting
procedure, utilizing the |
prediction_time |
Time used to make the prediction. |
horizon |
Prediction horizon. |
time_variable |
The name of time variable in linear mixed model. |
survival_variable_all |
The name of the transformed time-to-event outcomes variable. |
survival_trans_function |
The transformation function used for time-to-event outcomes,
in the order of |
bandcount1 |
The number of grid points spanning the prediction window,
from |
bandcount2 |
The number of grid points used to approximate
integrating out to infinity when normalizing the predicted risk/density.
A wider follow-up range needs a larger |
bandcount3 |
The number of points in the candidate-biomarker-value
grid used to build the predicted density curve; controls the resolution
of the density, not a time integral. Defaults to Pass explicit numbers instead of |
bio_his |
Which biomarker history will be plotted |
bio_pred |
Indicator, predict future biomarker or not, if NULL do not predict |
density |
Indicator, plot future biomarker density or not, if NULL do not plot. For an ordinal biomarker the prediction is instead drawn as each category's predicted probability (points sized by it), with the axis labelled by category. |
n_cores |
Number of CPU cores to use for computing the prediction at
each point in |
Value
Plot of risk and future biomarker with density using dynamic prediction.
Examples
data(pbc3)
data_survival_fitting = pbc3[!duplicated(pbc3$id), ]
form_marginal_surv = Surv(years, status3) ~ age + sex
form_conditional_cr = NULL
survival_fit_all = survivalSub(data_survival_fitting, form_marginal_surv,
form_conditional_cr)
long_sub_fixed = list(
"long1" = serBilir ~ year + age + sex + (years) + (years) * year,
"long2" = prothrombin ~ year + age + sex + (years) + (years) * year,
"long3" = albumin ~ year + age + age * year + sex + (years) + (years) * year,
"long4" = alkaline ~ year + age + sex + (years) + (years) * year,
"long5" = SGOT ~ year + age + sex + (years) + (years) * year,
"long6" = platelets ~ year + age + sex + (years) + (years) * year)
long_sub_random =list(
"long1" = ~ year| id,
"long2" = ~ year| id,
"long3" = ~ year| id,
"long4" = ~ year| id,
"long5" = ~ year| id,
"long6" = ~ year| id)
survival_variable_all = list(
"Tyears1", "Tyears2", "Tyears3", "Tyears4"
)
survival_trans_function = list(
fun1 = function(x){abs(x - 1)},
fun2 = function(x){abs(x - 3)},
fun3 = function(x){abs(x - 5)},
fun4 = function(x){abs(x - 7)}
)
# Complete case analysis
data_fit_all = list()
for(i in seq_len(length(long_sub_fixed))){
data_fit_all[[i]] = pbc3[pbc3$status3 == 1, ]
}
# fitting longitudinal submodel
long_fit_all = longitudinalSub(data_fit_all, long_sub_fixed, long_sub_random)
i_PID = 2
data.raw.predict.plot = pbc3[pbc3$id == i_PID, ]
data_predict_all_one = list(data.raw.predict.plot, data.raw.predict.plot, data.raw.predict.plot,
data.raw.predict.plot, data.raw.predict.plot, data.raw.predict.plot)
# plot biomarker 1 history, predict future biomarker
predictPlot(data_predict_all_one, long_fit_all, survival_fit_all,
prediction_time = 5, bio_his = 1, bio_pred = 1,
horizon = seq(0.5, 3.0, 0.5), time_variable = "year",
survival_variable_all, survival_trans_function,
bandcount1 = 10, bandcount2 = 10, bandcount3 = 200)
Predict the number of recurrent events in a window, by rolling episodes forward
Description
Experimental. For a patient at the start of (or partway through)
an episode of a gap-time recurrent-event model fit with
buildRecurrentEpisodes, predicts the distribution of the
number of recurrences in (prediction_time, prediction_time +
horizon], together with the probability of dying in it, by Monte Carlo:
every patient is simulated forward n_sim times, episode after
episode, until the window closes or the patient dies. predictRisk()
answers "is the next event a recurrence or a terminal event, and
when"; this answers "how many recurrences".
Usage
predictRecurrentCount(
data_predict,
long_fit_all,
survival_fit_all,
prediction_time,
horizon,
time_variable,
survival_variable_all = list(),
survival_trans_function = list(),
episode_variable = "episode",
next_covariates = NULL,
stay_lengths = 0,
n_sim = 200,
max_stages = 10,
bandcount2 = 100,
seed = NULL
)
Arguments
data_predict |
The rows of the patients' open episode, as
|
long_fit_all, survival_fit_all |
Fits from |
prediction_time |
The time (on the episode clock, as in
|
horizon |
Length of the window after |
time_variable |
Name of the time variable of the longitudinal
sub-model (e.g. |
survival_variable_all, survival_trans_function |
As in
|
episode_variable |
Name of the episode-counter covariate (default
|
next_covariates |
Optional |
stay_lengths |
A numeric vector of non-negative stay lengths (the
time between a recurrence and the discharge that restarts the process),
resampled with replacement. |
n_sim |
Paths simulated per patient. |
max_stages |
Most episodes drawn per path (including the first); paths still running at the cap are counted as they stand, with a warning. |
bandcount2 |
Event-time grid size passed to
|
seed |
Optional integer seed; the caller's random number stream is left unchanged. |
Details
Each simulated path goes as follows.
The current (open) episode's event time and type are drawn with
simulateTrajectory, from their posterior given the patient's biomarker history in that episode – the same distributionpredictRiskintegrates, so the first step uses the biomarkers exactly aspredictRisk()does.If the event is a terminal event inside the window, the path ends (the patient dies). If it is a recurrence inside the window, the count goes up by one; a stay length is then drawn (from
stay_lengths) and the next episode starts at discharge. If the event (or the discharge) falls after the window closes, the path ends.Each later episode is drawn the same way, but with no biomarker history – the patient has not been observed in it yet – so from the model's prior given the patient's covariates, with the episode counter advanced by
next_covariates. Repeat until no path is still running, ormax_stagesepisodes have been drawn.
Limitation. Only the first step uses the patient's biomarkers.
Later episodes depend on the patient only through their covariates (and
the episode counter), so any patient-level tendency to recur that is not
in those covariates – a frailty – is not carried forward, and the
predicted spread of the counts (e.g. the chance of two or more
recurrences) will be too small when such a tendency is present. Putting
the number of previous episodes, or the length of the previous gap, among
the survival sub-model's covariates (and updating them in
next_covariates) recovers some of it.
The stay length is not part of the backward joint model, so it has to be
supplied. The default, 0, means a recurrence takes no time (a
tumour recurrence, say); for hospitalizations pass observed stay lengths
(they are resampled with replacement).
Value
An object of class "predictRecurrentCount.BJM", a list with
- summary
One row per patient:
mean_count(expected number of recurrences in the window),p_count_0,p_count_1,p_count_2,p_count_3plus,p_ge1,p_ge2, andp_death(probability of dying in the window, outside hospital: a death during a stay is not part of the model, seebuildRecurrentEpisodes).- counts
The raw draws: a patients x
n_simmatrix of recurrence counts.- died
A patients x
n_simlogical matrix: died in the window.- horizon, prediction_time, n_sim
As supplied.
- stages
Most episodes drawn on any path.
- capped
TRUEif some path was still running whenmax_stagesstopped it (a warning, giving their share, is also given): its later recurrences are not counted, so the counts are slightly low.- capped_fraction
The share of all simulated paths stopped by
max_stages(0if none).
See Also
buildRecurrentEpisodes, predictRisk,
simulateTrajectory
Examples
## Simulated recurrent events and a biomarker. Subject 9999 is still at
## risk in their 2nd episode, and is who we predict for.
set.seed(1)
n <- 60
data_events <- do.call(rbind, lapply(seq_len(n), function(id) {
k <- sample(1:3, 1)
data.frame(
id = id, time = cumsum(rexp(k, rate = 1)),
status = c(rep(1, k - 1), sample(c(0, 2), 1, prob = c(0.6, 0.4))),
age = round(50 + rnorm(1, sd = 8)), sex = id %% 2
)
}))
data_events <- rbind(data_events, data.frame(
id = 9999, time = c(0.8, 1.6), status = c(1, 0), age = 55, sex = 1
))
data_long <- do.call(rbind, lapply(c(seq_len(n), 9999), function(id) {
tt <- seq(0, max(data_events$time[data_events$id == id]) + 0.2, by = 0.25)
data.frame(id = id, year = tt, biomarker = rnorm(length(tt)))
}))
built <- buildRecurrentEpisodes(data_events, data_long, time_variable_long = "year")
survival_fit_recur <- survivalSub(
built$episode_data,
Surv(gap, event_any) ~ age + sex + episode,
event_type ~ gap + age + sex + episode
)
long_fit_recur <- longitudinalSub(
built$long_data[built$long_data$event_any == 1, ],
biomarker ~ time_in_episode + age + sex + gap + gap * time_in_episode,
~ time_in_episode | episode_id
)
## The number of recurrences over the next 3 time units
open <- built$long_data[built$long_data$id == 9999 & built$long_data$event_any == 0, ]
counts <- predictRecurrentCount(
open, long_fit_recur, survival_fit_recur,
prediction_time = max(open$time_in_episode), horizon = 3,
time_variable = "time_in_episode", n_sim = 50, bandcount2 = 30, seed = 1
)
counts
Dynamic prediction function for future event risk
Description
Combines a fitted longitudinal sub-model (longitudinalSub)
and survival sub-model (survivalSub) into a backward joint
model prediction: for each at-risk subject at prediction_time, the
subject's observed longitudinal history up to that time is used to update
their individual random effects (empirical Bayes), which are then
integrated against the survival sub-model's hazard to give the predicted
probability of experiencing the event within
(prediction_time, prediction_time + horizon], conditional on being
event-free at prediction_time. The numerator (event within the
window) is evaluated on a grid over the window whose resolution is
controlled by bandcount1; the denominator (survival to
prediction_time, used for normalization) is that same window
integral plus the integral beyond the window, evaluated on a grid
controlled by bandcount2; see Details.
The prediction is conditional on the longitudinal history observed up to
prediction_time: rows of data_predict_all whose
time_variable is later than prediction_time are dropped,
with a warning, before predicting.
A patient with no biomarker measurement up to prediction_time
(rows whose biomarker values are all NA, or only rows after
prediction_time, e.g. a prediction_time before the first
visit) has no history to condition on. The longitudinal factor
f(Y \mid T) of the model is then 1, and the patient's risk is that
of the survival sub-model alone, given the covariates in their earliest
row; a warning names these patients. The other patients' predictions do
not change. A patient with measurements of some biomarkers but none of
another is still not predicted (NA, with a warning).
Usage
predictRisk(
data_predict_all,
long_fit_all,
survival_fit_all,
prediction_time,
horizon,
time_variable,
survival_variable_all,
survival_trans_function,
bandcount1 = "auto",
bandcount2 = "auto",
n_cores = 1
)
Arguments
data_predict_all |
This involves a collection of |
long_fit_all |
Outputs from the model fitting process using the |
survival_fit_all |
Results and parameters generated from the model fitting
procedure, utilizing the |
prediction_time |
Time used to make the prediction. |
horizon |
Prediction horizon: the length of the window after
|
time_variable |
The name of time variable in linear mixed model. |
survival_variable_all |
The name of the transformed time-to-event outcomes variable. The longitudinal sub-model must use the event time, directly or through one of these transformations. |
survival_trans_function |
The transformation function used for time-to-event outcomes,
in the order of |
bandcount1 |
The number of grid points spanning the prediction window,
from |
bandcount2 |
The number of grid points spanning
|
n_cores |
Number of CPU cores. With an ordinal biomarker the
prediction's cost is per patient, and the patients are spread over
|
Details
There is no universal correct value for bandcount1/bandcount2:
as a practical check, double both and confirm the resulting risk
probabilities barely change; if they do, keep doubling. By default
(bandcount1 = "auto", bandcount2 = "auto"), this doubling
check is done for you: starting from small built-in values, both are
doubled together, and the result is compared to the previous round,
until the largest relative change in the risk probabilities drops below
1%, or 2 doublings have been tried (so at most 3 calls' worth of work).
If it still has not converged by then, a warning reports this and the
result at the largest value tried is returned anyway (not an error), so
this never silently loops for an unbounded amount of time. Pass an
explicit number for either argument to skip auto-tuning it and use a
fixed value instead (as in previous package versions), or call
checkBandcountConvergence() directly for more control over the
tolerance and doubling count. See also vignette("BJM-intro",
package = "BJM") for a worked example.
The denominator integrates over every event time after
prediction_time, including times beyond the last follow-up time
in the data survivalSub() was fit on. There, the baseline
cumulative hazard is extrapolated linearly (a constant hazard), and the
longitudinal sub-model's mean is evaluated at event times it was never
fit on. When much of an at-risk patient's survival probability lies
beyond the last follow-up time (e.g. a prediction late in follow-up),
the prediction depends on this extrapolation; in pbc3 it moved
the risks checked by less than one percentage point.
For a fit with an ordinal biomarker, a patient's ordinal measurements
contribute the probability of their observed categories. With a single
ordinal random effect (e.g. one ordinal biomarker with a random
intercept) it is computed by Gauss-Hermite quadrature, and is exact; with
more, by Monte Carlo (mvtnorm::pmvnorm()), with an error around the
fourth significant digit. Each patient's Monte Carlo draws use a fixed
seed derived from the patient's id, so the results are the same on every
run and for any n_cores, without set.seed(), and the
caller's random-number state is left unchanged.
Value
An object of class "predictRisk.BJM", a named list with elements:
- risk_prob_1
A vector of dynamically predicted probabilities, one per at-risk patient, of experiencing the (first) event within the prediction horizon, named by patient id. All
0whenhorizon = 0.- risk_prob_2
When
survival_fit_allwas fit with competing risks, a vector of dynamically predicted probabilities, one per at-risk patient, of experiencing the competing event within the prediction horizon, named by patient id (all0whenhorizon = 0).NULLwhen there is no competing risk.- prob_undetected_1, prob_undetected_2
Only for an interval-censored
survival_fit_all(see below): the probability that the (first / competing) event already happened between the patient's last visit andprediction_time, undetected.prob_undetected_2isNULLwithout competing risks.
Interval-censored fits. An event is then only detected at a visit,
so at prediction_time s a patient is known to be event-free
only up to their last visit V (the last time in their history up to
s). risk_prob_1 is then
P(s < T \le s + horizon \mid T > V, history) and
prob_undetected_1 is P(V < T \le s \mid T > V, history) (with
competing risks, each _1/_2 value is that probability jointly
with the event type); with
V = s they reduce to the usual conditioning on T > s.
Only patients still at risk at prediction_time are predicted: a patient whose
recorded survival time is before prediction_time is left out (one with a missing
survival time is kept). The names show which patients each value belongs to.
The numerical integration grids are shared by all patients predicted in one
call, so a patient's value can differ slightly (within the grid's
discretization error, which shrinks as bandcount1/bandcount2
grow) depending on which other patients are predicted alongside it.
Examples
data(pbc3)
data_survival_fitting = pbc3[!duplicated(pbc3$id), ]
form_marginal_surv = Surv(years, status3) ~ age + sex
form_conditional_cr = NULL
survival_fit_all = survivalSub(data_survival_fitting, form_marginal_surv,
form_conditional_cr)
long_sub_fixed = list(
"long1" = serBilir ~ year + age + sex + (years) + (years) * year,
"long2" = prothrombin ~ year + age + sex + (years) + (years) * year,
"long3" = albumin ~ year + age + age * year + sex + (years) + (years) * year,
"long4" = alkaline ~ year + age + sex + (years) + (years) * year,
"long5" = SGOT ~ year + age + sex + (years) + (years) * year,
"long6" = platelets ~ year + age + sex + (years) + (years) * year)
long_sub_random =list(
"long1" = ~ year| id,
"long2" = ~ year| id,
"long3" = ~ year| id,
"long4" = ~ year| id,
"long5" = ~ year| id,
"long6" = ~ year| id)
survival_variable_all = list(
"Tyears1", "Tyears2", "Tyears3", "Tyears4"
)
survival_trans_function = list(
fun1 = function(x){abs(x - 1)},
fun2 = function(x){abs(x - 3)},
fun3 = function(x){abs(x - 5)},
fun4 = function(x){abs(x - 7)}
)
# Complete case analysis
data_fit_all = list()
for(i in seq_len(length(long_sub_fixed))){
data_fit_all[[i]] = pbc3[pbc3$status3 == 1, ]
}
# fitting longitudinal submodel
long_fit_all = longitudinalSub(data_fit_all, long_sub_fixed, long_sub_random)
i_PID = 2
data.raw.predict.1 = pbc3[pbc3$id == i_PID, ]
data_predict_all = list()
for(i in seq_len(length(long_sub_fixed))){
data_predict_all[[i]] = data.raw.predict.1[data.raw.predict.1$year <= 3,]
}
# predict risk probability
risk.prob = predictRisk(data_predict_all, long_fit_all, survival_fit_all,
prediction_time = 3,
horizon = 3, time_variable = "year",
survival_variable_all, survival_trans_function,
bandcount1 = 10, bandcount2 = 10)
# poly() in its default orthogonal mode, splines::ns()/bs(), and factor()
# in a continuous biomarker's long_sub_fixed produce correct dynamic
# predictions (see ?longitudinalSub) -- including
# when a patient has only a single longitudinal observation to condition
# on -- because the basis/contrasts fit on the full training data are
# cached (via each biomarker's terms object and long_fit_all$xlevels) and
# reused here at prediction time, instead of being recomputed from that
# patient's small per-prediction-time slice:
long_sub_fixed_nonlinear = list(
"long1" = serBilir ~ poly(year, 2) + age + sex + years,
"long2" = albumin ~ year + age + sex + years)
long_sub_random_nonlinear = list("long1" = ~ year | id, "long2" = ~ year | id)
long_fit_nonlinear = longitudinalSub(pbc3[pbc3$status3 == 1, ],
long_sub_fixed_nonlinear,
long_sub_random_nonlinear)
data_predict_normal = data.raw.predict.1[data.raw.predict.1$year <= 3, ]
data_predict_sparse = data.raw.predict.1[1, ]
risk.prob.normal = predictRisk(data_predict_normal, long_fit_nonlinear,
survival_fit_all, prediction_time = 3,
horizon = 3, time_variable = "year",
survival_variable_all, survival_trans_function,
bandcount1 = 10, bandcount2 = 10)
risk.prob.sparse = predictRisk(data_predict_sparse, long_fit_nonlinear,
survival_fit_all, prediction_time = 3,
horizon = 3, time_variable = "year",
survival_variable_all, survival_trans_function,
bandcount1 = 10, bandcount2 = 10)
# both give a sane, non-degenerate risk_prob_1 (not 0, no error)
risk.prob.normal$risk_prob_1
risk.prob.sparse$risk_prob_1
Patient ids, in the order predictions are returned
Description
The density helpers (marginalT(),
conditionalYT(), ...) all produce one column per patient, in order
of first appearance in data_predict_all[[1]]; this returns those
ids, as character, to name the returned predictions with.
Usage
prediction_patient_ids(data_predict_all, long_fit_all)
Value
A character vector.
Standard deviation of each patient's predictive distribution
Description
Helper for max_relative_diff(): from a
dynamicPredictionBio()-style result (Y_density, one column
per patient, tabulated on Y_all), the standard deviation of each
patient's predicted biomarker distribution. 0 if the result has no
density.
Usage
predictive_sd(result)
Arguments
result |
A prediction result. |
Value
A numeric vector, one per patient, or 0.
Build the prediction-to-infinity integration grid and marginal survival
Description
Shared helper for predictRisk and
dynamicPredictionBio: builds the numerical-integration grid from
prediction_time out to upper_bound, and evaluates the
marginal survival function S(T) over it.
The bandcount2 + 1 intervals are spaced by probability, not by
time: each holds an equal share of the at-risk patients' (averaged)
model-based conditional survival mass beyond prediction_time. The
upper bound sits far out in the tail (see
integration_upper_bound()), so equally spaced intervals would spend
most of the grid where there is almost no mass and converge more slowly
as bandcount2 grows. The first interval starts exactly at
prediction_time; the previous equally spaced grid started half an
interval earlier, counting mass from before the prediction time. Each
grid point is its interval's midpoint.
Usage
prepare_infinity_grid(
data_predict_all,
long_fit_all,
survival_fit_all,
prediction_time,
upper_bound,
bandcount2
)
Value
A list with predict.time.infinity (the grid points),
predict.time.infinity.1 (the interval edges), and
S_T_all_infinity.
Prediction data for predictRisk(), keeping patients without measurements
Description
Without any biomarker measurement up to
prediction_time, the longitudinal factor f(Y \mid T) of the
backward joint model is 1, and the predicted risk is that of the survival
sub-model alone given the patient's covariates. predictRisk()
predicts such patients that way instead of refusing them. This runs the
usual drop_after_prediction_time(), align_ordinal_levels()
and drop_missing_longitudinal(), then puts back one row per
patient left without any measurement: their earliest row, with the
biomarker set to NA and time_variable set to their last
visit up to prediction_time (min(0, prediction_time) if
there is none), which an interval-censored fit takes as the last time
they were known to be event-free. The covariates come from that row: the
survival sub-model's covariates are baseline values, one row per patient.
A warning names these patients. split_no_measurement() separates
them again; running this on its own output returns the same data, as
"auto" bandcount tuning does.
A patient with measurements of some biomarkers but not of others is still reported as unpredictable, as before.
Usage
prepare_risk_data(
data_predict_all,
long_fit_all,
survival_fit_all,
prediction_time,
time_variable,
survival_variable_all
)
Value
data_predict_all, filtered, with the added rows in the
position of each patient's earliest row.
Print a fitIntervalBJM fit
Description
A short summary of a fitIntervalBJM fit: the
imputation settings, the survival sub-model's hazard ratios and the
longitudinal sub-model's fixed effects (pooled over the imputations, with
the fraction of missing information). The components themselves are in
the returned list (e.g. x$pooled, x$trace).
Usage
## S3 method for class 'fitIntervalBJM'
print(x, digits = 4, ...)
Arguments
x |
A |
digits |
Number of significant digits. |
... |
Not used. |
Value
x, invisibly.
Print method for longitudinalSub.BJM objects
Description
Automatically called when you type long_fit_all at the console.
Usage
## S3 method for class 'longitudinalSub.BJM'
print(x, digits = 4, ...)
Arguments
x |
A |
digits |
Number of significant digits. Default is 4. |
... |
Additional arguments (currently unused). |
Value
Invisibly returns x.
Print method for poolLongitudinalSub.BJM objects
Description
Automatically called when you type pooled or
print(pooled) at the console. Displays a JMbayes2-style
formatted summary of the Rubin's-rules-pooled longitudinal sub-model(s).
Usage
## S3 method for class 'poolLongitudinalSub.BJM'
print(x, digits = 4, ...)
Arguments
x |
A |
digits |
Number of significant digits. Default is 4. |
... |
Additional arguments (currently unused). |
Value
Invisibly returns x.
Examples
if (requireNamespace("torch", quietly = TRUE) && torch::torch_is_installed()) {
data(pbc3)
data_fit_all <- pbc3[pbc3$status3 == 1, ]
set.seed(1)
n <- nrow(data_fit_all)
data_fit_all$serBilir[sample.int(n, floor(0.1 * n))] <- NA
data_fit_all$albumin[sample.int(n, floor(0.1 * n))] <- NA
long_sub_fixed <- list(
"long1" = serBilir ~ year + age + sex + years,
"long2" = albumin ~ year + age + sex + years)
long_sub_random <- list("long1" = ~ year | id, "long2" = ~ year | id)
imputed <- imputeLongitudinal(data_fit_all, long_sub_fixed, long_sub_random,
time_variable = "year", n_imputations = 5,
impute = "multiple", epochs = 50, seed = 1)
long_fit_all_list <- lapply(imputed$data_fit_all_list, function(d) {
longitudinalSub(d, long_sub_fixed, long_sub_random)
})
pooled <- poolLongitudinalSub(long_fit_all_list)
pooled # triggers print.poolLongitudinalSub.BJM automatically
}
Print method for predictLongitudinal.BJM objects
Description
Automatically called when you type the result of
predictLongitudinal() (with a single bio_i) at the
console.
Usage
## S3 method for class 'predictLongitudinal.BJM'
print(
x,
bio_i = NULL,
long_fit_all = NULL,
prediction_time = NULL,
horizon = NULL,
subject_ids = NULL,
digits = 4,
...
)
Arguments
x |
A |
bio_i |
Biomarker index (for label lookup). Default |
long_fit_all |
|
prediction_time |
Landmark time (for display). Default |
horizon |
Prediction horizon (for display). Default |
subject_ids |
Optional subject ID labels. |
digits |
Decimal places. Default 4. |
... |
Additional arguments (currently unused). |
Value
Invisibly returns x.
Print method for predictLongitudinalAll.BJM objects
Description
Called when you print the result of predictLongitudinal()
with several biomarkers (bio_i naming more than one, or
NULL): prints each biomarker's prediction in turn, under its name.
Usage
## S3 method for class 'predictLongitudinalAll.BJM'
print(x, ...)
Arguments
x |
A |
... |
Passed to |
Value
Invisibly returns x.
Print method for predictRisk.BJM objects
Description
Automatically called when you type the result of predictRisk()
at the console.
Usage
## S3 method for class 'predictRisk.BJM'
print(
x,
prediction_time = NULL,
horizon = NULL,
subject_ids = NULL,
digits = 4,
...
)
Arguments
x |
A |
prediction_time |
Landmark time (for display). Default |
horizon |
Prediction horizon (for display). Default |
subject_ids |
Optional subject ID labels. |
digits |
Decimal places. Default 4. |
... |
Additional arguments (currently unused). |
Value
Invisibly returns x.
Print method for survivalSub.BJM objects
Description
Automatically called when you type survival_fit_all or
print(survival_fit_all) at the console. Displays a JMbayes2-style
formatted summary of the survival sub-model.
Usage
## S3 method for class 'survivalSub.BJM'
print(x, digits = 4, ...)
Arguments
x |
A |
digits |
Number of significant digits. Default is 4. |
... |
Additional arguments (currently unused). |
Value
Invisibly returns x.
Examples
data(pbc3)
data_survival_fitting <- pbc3[!duplicated(pbc3$id), ]
form_marginal_surv <- Surv(years, status3) ~ age + sex
form_conditional_cr <- status4 ~ years + age + sex
survival_fit_all <- survivalSub(data_survival_fitting,
form_marginal_surv, form_conditional_cr)
survival_fit_all # triggers print.survivalSub.BJM automatically
Log probability of an ordinal category and its derivatives
Description
\log P(lo < \eta + \epsilon \le hi) for
\epsilon \sim N(0, 1), with its first and second derivatives in
\eta; evaluated on the side of the mean where the interval lies, so
a category far in a tail keeps its precision.
Usage
probit_box_derivs(eta, lo, hi)
Arguments
eta |
Linear predictors; |
lo, hi |
Lower and upper thresholds ( |
Value
A list with logP, g (first derivative) and h
(second derivative, \le 0).
Factor one patient's joint longitudinal covariance for conditionalYTBio()/conditionalYDTBio()
Description
Factor one patient's joint longitudinal covariance for conditionalYTBio()/conditionalYDTBio()
Usage
process_variance(
num_i,
time_new,
bio_i,
data_predict_all,
long_fit_all,
time_variable
)
Value
The result of cov_factor(), or NA if the patient has
no rows for some biomarker.
Random-effects design matrix for one patient, across all biomarkers
Description
Shared helper for build_conditional_design(),
build_conditional_design_copula(), and process_variance():
builds each biomarker's random-effects design from the left-hand side of
its own long_sub_random formula (e.g. ~ 1, ~ year,
~ year + I(year^2)) and stacks them block-diagonally, matching the
block-diagonal Sigma_fit built by longitudinalSub().
Previously this assumed every biomarker had either a random intercept
only, or a random intercept plus a slope on time_variable, so a
mix of the two (or any other random-effects formula) failed with
"non-conformable arguments".
Usage
random_effects_design(data_num_i_list, long_sub_random, Sigma)
Arguments
data_num_i_list |
One patient's data, one data frame per biomarker. |
long_sub_random |
The list of random-effects formulas. |
Sigma |
The random-effects covariance matrix ( |
Value
A matrix with one row per observation (stacked across biomarkers) and one column per random effect.
Resolve the per-biomarker type used by longitudinalSub()
Description
If biomarker_type is supplied explicitly, it always
wins (after validation). Otherwise each biomarker's type is auto-detected
from its own response column in data_fit_all: a factor (or
ordered factor) response is treated as "ordinal"; anything
else is treated as "continuous". This mirrors exactly the priority
rule requested for longitudinalSub: explicit
biomarker_type overrides auto-detection, and auto-detection is only
consulted when biomarker_type is NULL.
Usage
resolve_biomarker_type(biomarker_type, data_fit_all, long_sub_fixed, M)
Plot predicted risk across a sweep of landmark times
Description
Fixes the prediction horizon and sweeps backward/forward over a set of
landmark times, calling predictRisk at each landmark
in prediction_time to trace out how predicted event risk over that
fixed-length window changes as the landmark moves – i.e. as more (or
less) of the subject's longitudinal history is used to update their
risk. prediction_time may be a vector of landmark times to use
directly; a single value, which is expanded to three landmarks
(prediction_time, 1.5 * prediction_time,
2 * prediction_time); or NULL, which uses every observed
longitudinal measurement time (across subjects, from the first
biomarker's data) as a landmark. This contrasts with
predictPlot, which instead fixes the landmark and sweeps
over a range of horizons. The optional bio_i argument only selects
which biomarker's observed trajectory is overlaid on the plot for visual
reference – it does not affect the risk computation itself (unlike
predictPlot's bio_pred, which drives a biomarker
density prediction).
When the data give the subject's survival time and status, a vertical line marks the end of follow-up: solid for an observed event (coloured by event type with competing risks), dashed grey for a censoring.
Usage
riskPlot(
data_predict_all_pre,
long_fit_all,
survival_fit_all,
prediction_time = NULL,
bio_i = NULL,
horizon,
time_variable,
survival_variable_all,
survival_trans_function,
bandcount1 = "auto",
bandcount2 = "auto",
n_cores = 1
)
Arguments
data_predict_all_pre |
This involves a collection of |
long_fit_all |
Outputs from the model fitting process using the |
survival_fit_all |
Results and parameters generated from the model fitting
procedure, utilizing the |
prediction_time |
The landmark time(s) at which risks are predicted
and plotted: |
bio_i |
Biomarker whose observed history is drawn alongside the
predicted risks (by position in |
horizon |
Prediction horizon. |
time_variable |
The name of time variable in linear mixed model. |
survival_variable_all |
The name of the transformed time-to-event outcomes variable. |
survival_trans_function |
The transformation function used for time-to-event outcomes,
in the order of |
bandcount1 |
The number of grid points spanning the prediction window,
from |
bandcount2 |
The number of grid points used to approximate
integrating out to infinity when normalizing the predicted risk. A wider
follow-up range needs a larger Pass explicit numbers instead of |
n_cores |
Number of CPU cores to use for computing the prediction at
each landmark time in |
Value
Plot of risk using dynamic prediction.
Examples
data(pbc3)
survival_fit_all <- survivalSub(pbc3[!duplicated(pbc3$id), ],
Surv(years, status3) ~ age + sex, NULL)
long_fit_all <- longitudinalSub(pbc3[pbc3$status3 == 1, ],
list(serBilir ~ year + age + sex + years,
albumin ~ year + age + sex + years),
list(~ year | id, ~ year | id))
# one patient's whole history; risks within 2 years of landmarks 2, 3, 4
patient <- pbc3[pbc3$id == 2, ]
riskPlot(patient, long_fit_all, survival_fit_all, prediction_time = c(2, 3, 4),
bio_i = 1, horizon = 2, time_variable = "year",
survival_variable_all = list(), survival_trans_function = list(),
bandcount1 = 10, bandcount2 = 20)
Biomarker axis with the risk probabilities on a secondary axis
Description
Helper for riskPlot() and predictPlot(), which
draw risks on the biomarker's axis multiplied by scale_prob. The
secondary (risk) axis is labelled only within [0, 1]: where the
biomarker goes below 0 it used to show negative "probabilities". For an
ordinal biomarker the primary axis is labelled with its categories.
Usage
risk_axis_scale(scale_prob, category_labels = NULL)
Arguments
scale_prob |
Factor the risks were multiplied by. |
category_labels |
Category labels of an ordinal biomarker (plotted as
their codes |
Value
A ggplot2 scale.
Restricted cubic spline basis (Royston–Parmar)
Description
Columns x and, for each internal knot k_j,
(x - k_j)_+^3 - \lambda_j (x - k_{min})_+^3 - (1 - \lambda_j)
(x - k_{max})_+^3, \lambda_j = (k_{max} - k_j) / (k_{max} -
k_{min}): a cubic spline that is linear beyond the boundary knots. With
deriv = TRUE, the derivative of each column in x.
Usage
rp_basis(x, knots, deriv = FALSE)
Arguments
x |
Values (log times). |
knots |
Boundary and internal knots, sorted, boundary first and last. |
deriv |
Return the derivative instead. |
Draw from a truncated multivariate normal distribution
Description
n draws of N(\mu, V) restricted to the box
(lower, upper]: by rejection from batches of untruncated
proposals, and, for any draws still missing once max_proposals
proposals have been spent (a box of small probability), by Gibbs
sampling, one coordinate at a time from its univariate truncated normal
conditional distribution, after burn_in sweeps and keeping every
thin-th one.
Usage
rtmvnorm_box(
n,
mean,
sigma,
lower,
upper,
max_proposals = 20000,
burn_in = 50,
thin = 5
)
Value
A matrix with one column per draw.
Draw from a univariate truncated normal distribution
Description
By inversion, on the side of the mean where the truncation
interval lies (so a box far in the upper tail does not lose precision to
pnorm() rounding to 1).
Usage
rtnorm_one(mean, sd, lower, upper)
Vectorized draws from truncated standard-variance normals
Description
N(mean, 1) truncated to (lower, upper], by
inversion on the side of the mean where the interval lies.
Usage
rtnorm_vec(mean, lower, upper)
Arguments
mean, lower, upper |
Vectors of equal length. |
Value
A numeric vector of draws.
Combine one coefficient's per-completion estimates with Rubin's rules
Description
Internal helper for poolLongitudinalSub:
implements the scalar-parameter pooling rules of Rubin (1987, Ch. 3) and
the Barnard & Rubin (1999) small-sample degrees-of-freedom adjustment,
for a single fixed-effect coefficient's m per-completion
estimates/standard errors.
Usage
rubin_pool_scalar(estimates, std_errors, dfcom)
Arguments
estimates |
Length- |
std_errors |
Length- |
dfcom |
The complete-data degrees of freedom (assumed common across
completions; see |
Filter longitudinal data down to a single patient
Description
Shared helper for conditionalYT and conditionalYDT:
extracts each biomarker's rows for one patient ID, replicating the shared
data frame across biomarkers when only one was supplied.
Usage
select_patient_longitudinal_data(
data.long,
num,
num_i,
n_longitudinal,
time_variable
)
Value
A list with rep_num_i_list and data_num_i_list, or
NULL if any biomarker has zero rows for this patient (caller should
skip the patient in that case).
Filter longitudinal data down to a single patient, substituting the candidate biomarker prediction value
Description
Shared helper for conditionalYTBio and
conditionalYDTBio: extracts each biomarker's rows for one patient
ID, and for the biomarker being predicted (bio_i), appends a row at
time_new with each candidate value in Y_all substituted in
turn.
Usage
select_patient_longitudinal_data_bio(
data.long,
num,
num_i,
n_longitudinal,
time_variable,
bio_i,
time_new,
Y_all,
long_fit_all
)
Value
A list with rep_num_i_list, data_num_i_list, and
Y_select_all (a matrix of the substituted biomarker values, one
column per element of Y_all), or NULL if any biomarker has
zero rows for this patient (caller should skip the patient in that case).
Set a patient's survival-time columns to one integration grid point
Description
Sets the survival-time column and each
survival_variable_all column (through its
survival_trans_function) to their values at event time
l.
Usage
set_survival_columns(
df,
survival_variable,
l,
survival_variable_all,
survival_trans_function
)
Value
df, updated.
Simulate future trajectories from the backward joint model
Description
Draws n_sim complete futures for each patient from the
fitted backward joint model, conditional on the patient's longitudinal
history up to prediction_time and on being event-free at
prediction_time: an event time (and, with competing risks, an
event type), and the values of every biomarker at the requested
times. Where predictRisk and
predictLongitudinal summarize the predictive distribution,
this returns samples from it, so it can be used to generate synthetic
patients or to propagate the prediction into any other quantity (e.g. a
patient's "digital twin" under the fitted model).
Each draw follows the model's factorization
f(T, D)\, f(Y \mid T, D):
-
(T, D)is drawn from its posterior given the history, proportional tof(Y_{hist} \mid T, D)\, f(D \mid T)\, f(T)restricted toT >prediction_time, evaluated on the same integration grid aspredictRisk's denominator; the random effects
bare drawn from their Gaussian posterior given the history and the drawn(T, D);the biomarkers at
timesare drawn from the longitudinal sub-model given(T, D, b), adding residual error.
A patient with no non-missing biomarker measurement up to
prediction_time (e.g. a single baseline row with the biomarkers set
to NA) is simulated from the model's prior given their baseline
covariates, i.e. as a new synthetic patient.
Usage
simulateTrajectory(
data_predict_all,
long_fit_all,
survival_fit_all,
prediction_time,
times,
time_variable,
survival_variable_all,
survival_trans_function,
n_sim = 100,
bandcount2 = 100,
max_event_time = NULL,
truncate = TRUE,
future_covariates = NULL,
seed = NULL
)
Arguments
data_predict_all |
This involves a collection of |
long_fit_all |
Outputs from the model fitting process using the |
survival_fit_all |
Results and parameters generated from the model fitting
procedure, utilizing the |
prediction_time |
Time used to make the prediction. |
times |
Numeric vector of times (on the scale of
|
time_variable |
The name of time variable in linear mixed model. |
survival_variable_all |
The name of the transformed time-to-event outcomes variable. The longitudinal sub-model must use the event time, directly or through one of these transformations. |
survival_trans_function |
The transformation function used for time-to-event outcomes,
in the order of |
n_sim |
Number of futures to draw per patient. |
bandcount2 |
The number of intervals the event-time distribution
after |
max_event_time |
Event times beyond this are reported as
event-free through it, and the model is evaluated at it in their place
(see Details). Defaults to |
truncate |
If |
future_covariates |
Optional data frame of covariate values at future
times: a |
seed |
Optional integer seed, for reproducible draws. The caller's random number stream is left unchanged. |
Details
The event time is drawn on the grid of bandcount2 intervals that
predictRisk integrates over (each holding about the same
share of the conditional survival probability, with an extra edge at
max_event_time): an interval is drawn with its posterior
probability, the model is evaluated at its midpoint, and the event time
reported is uniform within it, so that summaries of the drawn event times
(e.g. the share before a given time) are not biased by rounding to the
midpoints.
Past the last follow-up time of the data survivalSub() was fit
on, the event-time distribution rests on an extrapolated baseline hazard
and the longitudinal sub-model's mean on event times it was never fit
on; drawn from a patient's prior (no history), a large share of event
times can land there, with implausible biomarker values. So an event
time beyond max_event_time (by default that last follow-up time)
is reported as event-free through max_event_time
(event = 0, event_time = max_event_time), and both the
history's likelihood and the simulated biomarkers use T =
max_event_time in its place, i.e. a patient whose event comes after
follow-up is assumed to follow the trajectory of one whose event comes
at its end. max_event_time = Inf turns this off and reproduces
the event-time distribution predictRisk integrates over.
The biomarkers at times are evaluated with the covariates of the
patient's last row in data_predict_all (with time_variable
set to each element of times), so a time-varying covariate is
carried forward at its last value, unless future_covariates says
otherwise. Each row of future_covariates sets the covariates in its
columns from its time_variable on, until a later row changes them:
a time in times takes the covariates of the last row at or before
it, and those of the patient's last row in data_predict_all for
anything that row does not set. The history, and so the posterior of the
event time and random effects, is unaffected. A covariate that is a known
function of time (such as current age) is better written into the model
formula as one (e.g. I(age + year)), so that it is updated without
future_covariates. Comparing draws under different
future_covariates compares the outcomes the model associates with
those covariate paths; it is not a causal effect of changing them.
The backward model describes the biomarkers before the event. With
truncate = TRUE (the default) a value at a time at or after the
drawn (or censored) event time is NA; with truncate = FALSE it is still
drawn from the sub-model, which is the quantity whose density
predictLongitudinal returns.
An ordinal biomarker (see longitudinalSubCopula) is the
discretization of a latent Gaussian score by the fitted thresholds, and is
simulated that way: its latent score is drawn like a continuous biomarker
(with unit residual variance) and cut into categories. In the history it
is known only to have fallen between the thresholds bracketing its
observed category: its contribution to the likelihood of (T, D) is
a multivariate normal box probability, computed as in
predictRisk, and the random effects are drawn by first
drawing the history's latent scores from their truncated multivariate
normal distribution given the continuous measurements (by rejection, or
by Gibbs sampling when few proposals fall in the box), and then the
random effects given those scores. Supply seed for reproducible
draws.
Value
A data frame of class "simulateTrajectory.BJM", with one
row per patient, draw and element of times, and columns: the
patient id (named as in long_sub_random); sim, the draw
number; time_variable; event_time, the drawn event time,
or max_event_time if the event comes after it; event, 1 if
the event occurs at event_time and 0 if the patient is event-free
through it; with competing risks, the drawn event type (named as the
response of form_conditional_cr, with the same 0/1 coding, and
NA when event is 0); and one column per
biomarker, named by its response variable (an ordered factor, with the
fitted categories as levels, for an ordinal biomarker). Attributes
"prediction_time", "times" and "max_event_time"
record the values used, and "history" the continuous biomarker
measurements conditioned on (for plot.simulateTrajectory.BJM).
See Also
plot.simulateTrajectory.BJM to plot the draws.
Examples
data(pbc3)
survival_fit_all = survivalSub(pbc3[!duplicated(pbc3$id), ],
Surv(years, status3) ~ age + sex, NULL)
long_sub_fixed = list(
"long1" = serBilir ~ year + age + sex + (years) + (years) * year,
"long2" = albumin ~ year + age + sex + (years) + (years) * year)
long_sub_random = list("long1" = ~ year | id, "long2" = ~ year | id)
data_fit_all = list(pbc3[pbc3$status3 == 1, ], pbc3[pbc3$status3 == 1, ])
long_fit_all = longitudinalSub(data_fit_all, long_sub_fixed, long_sub_random)
trans = survivalTrans(c(1, 3, 5, 7))
# Patient 2's history up to year 3: 200 possible futures over years 3-8
history = pbc3[pbc3$id == 2 & pbc3$year <= 3, ]
sims = simulateTrajectory(list(history, history), long_fit_all, survival_fit_all,
prediction_time = 3, times = seq(3, 8, by = 0.5),
time_variable = "year", trans$survival_variable_all,
trans$survival_trans_function, n_sim = 200, seed = 1)
head(sims)
# Covariates after the last row are carried forward unless
# future_covariates sets them from a given time on. Age at entry does not
# change, so this is only a sensitivity check of the forecast's dependence
# on age; a time-varying covariate such as ascites would be set the same
# way, e.g. data.frame(year = 6, ascites = "Yes").
sims_older = simulateTrajectory(list(history, history), long_fit_all, survival_fit_all,
prediction_time = 3, times = seq(3, 8, by = 0.5),
time_variable = "year", trans$survival_variable_all,
trans$survival_trans_function, n_sim = 200, seed = 1,
future_covariates = data.frame(year = 5, age = history$age[1] + 10))
tapply(sims_older$serBilir - sims$serBilir, sims$year, mean, na.rm = TRUE)
# Probability of an event within 2 years, from the draws
first = sims[!duplicated(sims$sim), ]
mean(first$event == 1 & first$event_time <= 5)
# A new synthetic patient: baseline covariates only, biomarkers NA
new_patient = data.frame(id = "new", year = 0, age = 50, sex = 1,
serBilir = NA, albumin = NA)
synthetic = simulateTrajectory(new_patient, long_fit_all, survival_fit_all,
prediction_time = 0, times = 0:6, time_variable = "year",
trans$survival_variable_all,
trans$survival_trans_function, n_sim = 100, seed = 1)
plot(sims)
plot(sims, which = "event")
Sinusoidal diffusion-timestep embedding
Description
Internal helper for imputeLongitudinal's
method = "diffusion" backend: the standard Transformer/DDPM
sinusoidal position embedding, applied to a (possibly per-row-varying)
integer timestep instead of a sequence position. The frequency basis is
computed once in plain R (not with torch::torch_arange(), whose
inclusive/exclusive endpoint convention is a frequent source of
off-by-one bugs) and then multiplied against the timestep tensor, so the
output width is always exactly dim regardless of parity.
Usage
sinusoidal_time_embedding(t, dim)
Arguments
t |
A 1-D torch float tensor of timesteps, one per row. |
dim |
Output embedding width. |
Plot individual longitudinal trajectories (spaghetti plot)
Description
Draws one line per subject for a longitudinal biomarker, optionally
colored by the subject's eventual outcome, with a smoothed mean
trajectory per outcome group overlaid. With align = "event" the
horizontal axis becomes the time remaining until the subject's
event/censoring time (survival_variable - time_variable, so 0 is
the event time, at the left edge). This mirrors the backward joint
model's view of the biomarker as a function of time-to-event. Unlike
cmtPlot, which averages over subjects sharing one event
time, this shows every subject's raw history.
Usage
spaghettiPlot(
data_plot_all,
bio_variable,
time_variable,
id_variable = "id",
survival_variable = NULL,
event_type_variable = NULL,
align = c("baseline", "event"),
n_subjects = NULL,
smooth = TRUE,
alpha = 0.3
)
Arguments
data_plot_all |
A long-format |
bio_variable |
Name of the biomarker variable to plot. |
time_variable |
Name of the measurement-time variable. |
id_variable |
Name of the subject ID column. Default is |
survival_variable |
Name of the time-to-event variable. Required when
|
event_type_variable |
Name of the variable used to color subjects
(e.g. the event indicator or event type). Must be constant within each
subject. |
align |
Either |
n_subjects |
Optional number of subjects to draw, sampled at random
(call |
smooth |
Logical; overlay a LOESS mean trajectory per group. Default
|
alpha |
Transparency of the individual lines. Default |
Value
A ggplot object.
Examples
data(pbc3)
# trajectories colored by death (1) vs alive/transplanted (0)
spaghettiPlot(pbc3, bio_variable = "serBilir", time_variable = "year",
event_type_variable = "status2")
# aligned at each subject's event/censoring time
spaghettiPlot(pbc3, bio_variable = "serBilir", time_variable = "year",
survival_variable = "years", event_type_variable = "status2",
align = "event")
Separate the patients prepare_risk_data() found without measurements
Description
A patient none of whose rows, in any biomarker's data, has
a complete measurement (prepare_risk_data() has already dropped
the incomplete rows of every other patient) is predicted from the
survival sub-model alone.
Usage
split_no_measurement(
data_predict_all,
long_fit_all,
survival_variable,
survival_variable_all
)
Arguments
survival_variable |
The outcome variables of the survival sub-model
( |
Value
A list: measured, data_predict_all without these
patients, and no_measurement, a list with one row per such
patient in each element (as marginalT() and friends read
covariates from), or NULL if there is none.
Fixed-effects means at every candidate event time
Description
For each biomarker's rows, X(l)\beta with the
survival-time columns (and the event type, with competing risks) set to
each element of l_grid, stacked across biomarkers.
Usage
stacked_mean_on_grid(
rows,
long_fit_all,
l_grid,
d,
survival_variable,
event_type_variable,
survival_variable_all,
survival_trans_function
)
Value
A matrix with one row per stacked observation and one column per
element of l_grid.
Random-effects design stacked across biomarkers
Description
Like random_effects_design(), but a biomarker may
contribute no rows (its block is then empty and its random effects
simply do not enter).
Usage
stacked_random_design(rows, long_sub_random)
Interval endpoints per subject
Description
Interval endpoints per subject
Usage
subject_intervals(data_survival, form_marginal_surv, id, event_type = NULL)
Restrict prediction data to patients still at risk
Description
Shared helper for predictRisk and
dynamicPredictionBio: drops rows whose survival-time variable is
below prediction_time from every biomarker's data frame. Rows
whose survival time is missing (e.g. a new patient whose event time is
not yet known) are kept: they are treated as at risk. It is an error if
that leaves no patient (it used to return an empty result from
predictRisk() without comment, and fail with an unrelated error in
predictLongitudinal()).
With allow_empty = TRUE an empty result is returned instead (the
caller checks that some other patient is left).
Usage
subset_at_risk(
data_predict_all,
survival_variable,
prediction_time,
allow_empty = FALSE
)
Value
data_predict_all, filtered in place per element.
Summary method for longitudinalSub.BJM objects
Description
Like print but adds per-outcome random-effects variance components
and the full correlation matrix of D.
Usage
## S3 method for class 'longitudinalSub.BJM'
summary(object, digits = 4, ...)
Arguments
object |
A |
digits |
Number of significant digits. Default is 4. |
... |
Additional arguments (currently unused). |
Value
Invisibly returns a list of per-outcome summary.lme objects.
Summary method for predictLongitudinal.BJM objects
Description
Like print but also shows distribution-level summaries across subjects.
Usage
## S3 method for class 'predictLongitudinal.BJM'
summary(
object,
bio_i = NULL,
long_fit_all = NULL,
prediction_time = NULL,
horizon = NULL,
subject_ids = NULL,
digits = 4,
...
)
Arguments
object |
A |
bio_i |
Biomarker index (for label lookup). Default |
long_fit_all |
|
prediction_time |
Landmark time (for display). Default |
horizon |
Prediction horizon (for display). Default |
subject_ids |
Optional subject ID labels. |
digits |
Decimal places. Default 4. |
... |
Additional arguments (currently unused). |
Value
Invisibly returns object.
Summary method for predictRisk.BJM objects
Description
Like print but also shows mean, SD, and range of predicted risks.
Usage
## S3 method for class 'predictRisk.BJM'
summary(
object,
prediction_time = NULL,
horizon = NULL,
subject_ids = NULL,
digits = 4,
...
)
Arguments
object |
A |
prediction_time |
Landmark time (for display). Default |
horizon |
Prediction horizon (for display). Default |
subject_ids |
Optional subject ID labels. |
digits |
Decimal places. Default 4. |
... |
Additional arguments (currently unused). |
Value
Invisibly returns object.
Summary method for survivalSub.BJM objects
Description
Called via summary(survival_fit_all). Returns (and prints) an
extended summary including baseline hazard range, BIC, and McFadden R2
for the competing-risks GLM.
Usage
## S3 method for class 'survivalSub.BJM'
summary(object, digits = 4, ...)
Arguments
object |
A |
digits |
Number of significant digits. Default is 4. |
... |
Additional arguments (currently unused). |
Value
Invisibly returns a list with components cox_summary and
(if competing risks) glm_summary.
Examples
data(pbc3)
data_survival_fitting <- pbc3[!duplicated(pbc3$id), ]
form_marginal_surv <- Surv(years, status3) ~ age + sex
form_conditional_cr <- status4 ~ years + age + sex
survival_fit_all <- survivalSub(data_survival_fitting,
form_marginal_surv, form_conditional_cr)
summary(survival_fit_all)
Fit the survival sub-model
Description
Fits the survival building block used by predictRisk
and dynamicPredictionBio: a marginal Cox proportional-hazards
model (via coxph, with x = TRUE, y = TRUE so
the fit is self-contained for later prediction) for the time-to-event
outcome given form_marginal_surv, plus, optionally, a logistic
event-type/competing-risks model (via glm with
family = binomial) given form_conditional_cr. The
competing-risks model is fit only among subjects who experienced an event
(i.e. whose censoring indicator is non-zero), predicting which type of
event occurred conditional on an event having occurred. Together with
longitudinalSub, the object returned here forms the pair of
sub-models that dynamic prediction is built on.
Usage
survivalSub(
data_survival_fitting,
form_marginal_surv,
form_conditional_cr,
event_time = NULL,
baseline = c("spline", "piecewise"),
df = NULL
)
Arguments
data_survival_fitting |
One row per subject, containing the
time-to-event outcome, censoring/event-type indicator, and any baseline
covariates referenced in |
form_marginal_surv |
A survival formula, e.g.
|
form_conditional_cr |
An optional formula for the competing-risks
(event-type) model, e.g. |
event_time |
Only for an interval-censored outcome: the name of the
event-time column that the longitudinal sub-model's formulas use (the
exact time is unknown, so it is not part of |
baseline |
Only for an interval-censored outcome: the baseline
hazard, |
df |
Only for an interval-censored outcome: spline degrees of freedom (default 3) or number of pieces (default 6). |
Details
Interval censoring (experimental). When
form_marginal_surv is Surv(L, R, type = "interval2") ~
covariates – the event is only known to lie in (L, R], with
R = NA or Inf for right-censored subjects – the marginal
model is a proportional hazards model with a smooth spline (or
piecewise-constant) baseline hazard fit by maximum likelihood (see
icphFit()), stored as ic_fit in place of
coxph_fit. A strata() term gives each stratum its own
baseline hazard. With competing risks, the event type (0/1) is taken as
known once the event is detected, and form_conditional_cr is fit
among subjects with an observed event (finite R); if it uses the
event time, that column must be filled for them, as
fitIntervalBJM does with imputed times. Ordinary right-censored Surv(time,
status) outcomes are fit with coxph exactly as before.
Events that happened before a subject's first visit (L = 0) only
say that the event fell between time 0 and that visit. Before the
earliest visit in the data there are no interval endpoints, so the
baseline hazard there is extrapolated, not estimated, and
fitIntervalBJM places these subjects' event times
according to that extrapolation. print() reports how many events
are of this kind: when they are a large share of all events (in
simulations, around a third or more), results can depend noticeably on
the assumed shape of the early hazard.
Value
An object of class "survivalSub.BJM", a named list with elements:
- coxph_fit
The fitted
coxphmarginal survival model (ic_fit, an interval-censored PH fit, in its place for an interval-censored outcome).- form_marginal_surv
The
form_marginal_survformula, as supplied.- glm_fit
The fitted
glmcompeting-risks (event type) model, orNULLifform_conditional_crwas not supplied.- form_conditional_cr
The
form_conditional_crformula, as supplied (orNULL).
Examples
data(pbc3)
data_survival_fitting = pbc3[!duplicated(pbc3$id), ]
form_marginal_surv = Surv(years, status3) ~ age + sex
form_conditional_cr = NULL
survival_fit_all = survivalSub(data_survival_fitting, form_marginal_surv,
form_conditional_cr)
Interval-censored branch of survivalSub()
Description
Interval-censored branch of survivalSub()
Usage
survivalSubInterval(
data_survival_fitting,
form_marginal_surv,
form_conditional_cr,
event_time,
baseline,
df
)
Build a survival-time transform basis from cut points
Description
predictRisk(), dynamicPredictionBio(),
predictPlot(), and riskPlot() all take a pair of arguments,
survival_variable_all/survival_trans_function, that describe
transformed basis variables of the (remaining) survival time; these can
optionally be referenced in long_sub_fixed formulas to let the
longitudinal sub-model depend flexibly on time-to-event. In every example
in this package, that pair follows the same convention: variables named
"Tyears1", "Tyears2", ... , each defined as the absolute
distance from a fixed cut point (function(x) abs(x - k)).
survivalTrans() builds exactly that pair from a plain vector of cut
points, so you do not have to hand-write two parallel lists of matching
names and closures. You remain free to construct
survival_variable_all/survival_trans_function by hand for any
other transform.
Usage
survivalTrans(cut_points, prefix = "Tyears")
Arguments
cut_points |
A non-empty numeric vector of cut points, one per transformed basis variable. |
prefix |
Prefix used for the generated variable names in
|
Value
A named list with elements survival_variable_all and
survival_trans_function, in the format expected by
predictRisk(), dynamicPredictionBio(),
predictPlot(), and riskPlot().
Examples
trans <- survivalTrans(c(1, 3, 5, 7))
trans$survival_variable_all
trans$survival_trans_function[[1]](2)
# equivalent to hand-writing:
survival_variable_all <- list("Tyears1", "Tyears2", "Tyears3", "Tyears4")
survival_trans_function <- list(
fun1 = function(x) abs(x - 1),
fun2 = function(x) abs(x - 3),
fun3 = function(x) abs(x - 5),
fun4 = function(x) abs(x - 7)
)
# survivalTrans() only ever builds the abs(x - k) family above. Any other
# transform -- as long as it is a function of a single numeric time value
# that returns a single finite numeric value -- is written by hand the
# same way, one entry of survival_trans_function per entry of
# survival_variable_all. A few examples that all work equally well with
# predictRisk()/dynamicPredictionBio()/predictPlot()/riskPlot():
survival_variable_all <- list("Tlog", "Tsqrt", "Tsq", "Texp", "Tinv")
survival_trans_function <- list(
fun1 = function(x) log(x + 1), # log(x + 1)
fun2 = function(x) sqrt(abs(x)), # sqrt(abs(x))
fun3 = function(x) x^2, # x^2
fun4 = function(x) exp(-x / 10), # exp(-x / 10)
fun5 = function(x) 1 / (x + 1) # 1 / (x + 1)
)
sapply(survival_trans_function, function(f) f(2))
Accessors for the marginal survival sub-model
Description
Everything the prediction code needs from the marginal
survival model of survivalSub() – the name of the event-time
variable, each patient's linear predictor, the baseline cumulative hazard
(per stratum), and the last time it is tabulated at – goes through these
helpers, so the same prediction code serves both a right-censored fit (a
coxph model, survival_fit_all$coxph_fit)
and an interval-censored fit (a piecewise-constant-hazard proportional
hazards model, survival_fit_all$ic_fit; see icphFit()).
For a right-censored fit each accessor returns exactly what the code used
to compute from coxph_fit inline.
Usage
is_interval_censored(survival_fit_all)
survival_time_variable(survival_fit_all)
survival_lp(survival_fit_all, newdata)
survival_cum_basehaz(survival_fit_all)
survival_patient_strata(survival_fit_all, newdata)
survival_max_followup(survival_fit_all)
Arguments
survival_fit_all |
Output of |
newdata |
One row per patient. |
Functions
-
is_interval_censored():TRUEfor an interval-censored fit. -
survival_time_variable(): Name of the event-time variable that the longitudinal sub-model conditions on (for an interval-censored fit, theevent_timecolumn given tosurvivalSub()). -
survival_lp(): Uncentered linear predictor,NAfor a patient with a missing covariate. -
survival_cum_basehaz(): Baseline cumulative hazard: a data frame with columnshazardandtime(andstratafor a stratified Cox model). -
survival_patient_strata(): Each patient's stratum (as character), orNULLwhen the model is not stratified. -
survival_max_followup(): Largest follow-up time in the data the survival model was fit on (for interval-censored data, the largest finite interval endpoint).
Square root of a covariance matrix
Description
S with S S^T = V, from the eigendecomposition so
a positive semi-definite V (e.g. a posterior covariance that has
lost definiteness to rounding) works too.
Usage
symmetric_sqrt(V)
Time by which conditional survival falls below a threshold
Description
Helper for integration_upper_bound(): for each linear
predictor in lp, the earliest time t > s with
\exp(-(H_0(t) - H_0(s)) e^{lp}) < tail_prob, where
H_0 is the tabulated baseline cumulative hazard and, past its last
time, the same linear extrapolation cumulative_baseline_at() uses.
Inf if the extrapolated hazard is 0.
Usage
tail_time(cum_basehaz, lp, s, tail_prob)
Arguments
cum_basehaz |
A data frame with columns |
lp |
Linear predictors ( |
s |
The prediction time. |
tail_prob |
The survival threshold. |
Value
A numeric vector, one time per element of lp.
Train the conditional diffusion network
Description
Internal helper for imputeLongitudinal's
method = "diffusion" backend, following CSDI's self-supervised
training (Tashiro et al., 2021): in every mini-batch, each row's observed
entries are split at random into targets (each with a probability drawn
uniformly per row) and conditions; the targets are noised to a random
diffusion step and the network learns to predict that noise from the
conditions' true values. Only observed entries ever serve as targets, so
the loss never touches a missing cell. An earlier version noised whole
rows without giving the network the other observed values, so imputations
made little use of the row's other biomarkers.
Usage
train_diffusion(
net,
X_cov,
X_resp,
X_mask,
epochs,
diffusion_steps,
time_emb_dim,
batch_size = 128,
learning_rate = 0.001
)
Arguments
net |
Network from |
X_cov, X_resp, X_mask |
Covariates, standardized responses (0 where
missing) and the observation mask, as |
epochs |
Passes over the data. |
diffusion_steps |
Number of diffusion steps. |
time_emb_dim |
Width of the timestep embedding. |
batch_size |
Rows per mini-batch. |
learning_rate |
Adam learning rate. |
Value
A list with the trained net and its noise schedule.
Train the MIWAE network with a full-batch importance-weighted ELBO
Description
Internal helper for imputeLongitudinal.
Usage
train_miwae(net, X_cov, X_resp, X_mask, epochs, K, learning_rate = 0.001)
Largest value of the episode counter the survival sub-model was fit on
Description
Largest value of the episode counter the survival sub-model was fit on
Usage
training_episode_max(survival_fit_all, episode_variable)
Factor levels of the data a biomarker's sub-model was fit on
Description
Used by longitudinalSub() to cache, per biomarker,
the factor levels seen by lme() – computed from the full fitting
data before it is restricted to the subjects retained in the joint fit,
which may lack some levels.
Usage
training_xlevels(terms_model, data, fixed_formula)
Arguments
terms_model |
The fitted model's |
data |
The data the model was fit on. |
fixed_formula |
The fixed-effects formula (for its variables). |
Value
A named list of factor levels, as from .getXlevels().
Formula terms whose values depend on the other rows of the data
Description
The calls in expr to funs – by default
poly() (unless raw = TRUE), splines::ns(),
splines::bs() and factor() – as deparsed strings. See
warn_unsafe_formula_terms().
Usage
unsafe_formula_calls(expr, funs = c("poly", "ns", "bs", "factor"))
Value
A character vector, without duplicates.
Warn about formula terms whose basis is recomputed from whatever data they are given
Description
poly() (in its default orthogonal mode),
splines::ns()/splines::bs(), and factor() compute
their basis/contrasts from whatever data is passed to model.matrix().
For a continuous biomarker's long_sub_fixed formula this is
handled: the basis lme() was fit with (its terms'
predvars) and the training factor levels are reused both when
estimating Sigma_fit and at prediction time, so such terms are not
flagged there. They are still rebuilt from the data at hand – a single
patient's rows at prediction time, the retained subjects when estimating
Sigma_fit – for long_sub_random formulas and for an
ordinal biomarker's long_sub_fixed formula, which is where this
warns. poly(..., raw = TRUE), I(x^2), log(),
sqrt(), and similar terms that do not depend on the surrounding
data are never flagged.
Usage
warn_unsafe_formula_terms(formula_list, arg_name)
Probability of a case and of a control from the posterior of T
Description
Model-based case/control status of one subject in the
window (s, e], given what was observed: the event in (L, R]
with type cause, or no event by L (R = Inf). With
F_k the posterior cumulative mass of event type k
(event_time_posterior(), linear within cells), the probability
of being a case of type k is the posterior mass of
(\max(L, s), \min(R, e)] for the observed type over that of
(L, R], and of being a control (event-free at e) the mass
beyond e over that of (L, R]. Yang et al. (2026), Biometrical
Journal 68:e70108.
Usage
window_status_probs(post, L, R, cause, s, e, k)
Arguments
post |
One element of |
L, R, cause |
The subject's observed outcome. |
s, e |
The window. |
k |
The event type evaluated. |
Value
c(case = , control = ).