1 |
#' Fitting an MMRM with Single Optimizer |
|
2 |
#' |
|
3 |
#' @description `r lifecycle::badge("stable")` |
|
4 |
#' |
|
5 |
#' This function helps to fit an MMRM using `TMB` with a single optimizer, |
|
6 |
#' while capturing messages and warnings. |
|
7 |
#' |
|
8 |
#' @inheritParams mmrm |
|
9 |
#' @param control (`mmrm_control`)\cr object. |
|
10 |
#' @param tmb_data (`mmrm_tmb_data`)\cr object. |
|
11 |
#' @param formula_parts (`mmrm_tmb_formula_parts`)\cr object. |
|
12 |
#' @param ... Additional arguments to pass to [mmrm_control()]. |
|
13 |
#' |
|
14 |
#' @details |
|
15 |
#' `fit_single_optimizer` will fit the `mmrm` model using the `control` provided. |
|
16 |
#' If there are multiple optimizers provided in `control`, only the first optimizer |
|
17 |
#' will be used. |
|
18 |
#' If `tmb_data` and `formula_parts` are both provided, `formula`, `data`, `weights`, |
|
19 |
#' `reml`, and `covariance` are ignored. |
|
20 |
#' |
|
21 |
#' @return The `mmrm_fit` object, with additional attributes containing warnings, |
|
22 |
#' messages, optimizer used and convergence status in addition to the |
|
23 |
#' `mmrm_tmb` contents. |
|
24 |
#' @export |
|
25 |
#' |
|
26 |
#' @examples |
|
27 |
#' mod_fit <- fit_single_optimizer( |
|
28 |
#' formula = FEV1 ~ RACE + SEX + ARMCD * AVISIT + us(AVISIT | USUBJID), |
|
29 |
#' data = fev_data, |
|
30 |
#' weights = rep(1, nrow(fev_data)), |
|
31 |
#' optimizer = "nlminb" |
|
32 |
#' ) |
|
33 |
#' attr(mod_fit, "converged") |
|
34 |
fit_single_optimizer <- function(formula, |
|
35 |
data, |
|
36 |
weights, |
|
37 |
reml = TRUE, |
|
38 |
covariance = NULL, |
|
39 |
tmb_data, |
|
40 |
formula_parts, |
|
41 |
..., |
|
42 |
control = mmrm_control(...)) { |
|
43 | 199x |
to_remove <- list( |
44 |
# Transient visit to invalid parameters. |
|
45 | 199x |
warnings = c("NA/NaN function evaluation") |
46 |
) |
|
47 | 199x |
as_diverged <- list( |
48 | 199x |
errors = c( |
49 | 199x |
"NA/NaN Hessian evaluation", |
50 | 199x |
"L-BFGS-B needs finite values of 'fn'" |
51 |
) |
|
52 |
) |
|
53 | 199x |
if (missing(tmb_data) || missing(formula_parts)) { |
54 | 14x |
h_valid_formula(formula) |
55 | 13x |
assert_data_frame(data) |
56 | 13x |
assert_numeric(weights, any.missing = FALSE, lower = .Machine$double.xmin) |
57 | 13x |
assert_flag(reml) |
58 | 13x |
assert_class(control, "mmrm_control") |
59 | 13x |
assert_list(control$optimizers, names = "unique", types = c("function", "partial")) |
60 | 13x |
quiet_fit <- h_record_all_output( |
61 | 13x |
fit_mmrm( |
62 | 13x |
formula = formula, |
63 | 13x |
data = data, |
64 | 13x |
weights = weights, |
65 | 13x |
reml = reml, |
66 | 13x |
covariance = covariance, |
67 | 13x |
control = control |
68 |
), |
|
69 | 13x |
remove = to_remove, |
70 | 13x |
divergence = as_diverged |
71 |
) |
|
72 |
} else { |
|
73 | 185x |
assert_class(tmb_data, "mmrm_tmb_data") |
74 | 185x |
assert_class(formula_parts, "mmrm_tmb_formula_parts") |
75 | 185x |
quiet_fit <- h_record_all_output( |
76 | 185x |
fit_mmrm( |
77 | 185x |
formula_parts = formula_parts, |
78 | 185x |
tmb_data = tmb_data, |
79 | 185x |
control = control |
80 |
), |
|
81 | 185x |
remove = to_remove, |
82 | 185x |
divergence = as_diverged |
83 |
) |
|
84 |
} |
|
85 | 198x |
if (length(quiet_fit$errors)) { |
86 | 4x |
stop(quiet_fit$errors) |
87 |
} |
|
88 | 194x |
converged <- (length(quiet_fit$warnings) == 0L) && |
89 | 194x |
(length(quiet_fit$divergence) == 0L) && |
90 | 194x |
isTRUE(quiet_fit$result$opt_details$convergence == 0) |
91 | 194x |
structure( |
92 | 194x |
quiet_fit$result, |
93 | 194x |
warnings = quiet_fit$warnings, |
94 | 194x |
messages = quiet_fit$messages, |
95 | 194x |
divergence = quiet_fit$divergence, |
96 | 194x |
converged = converged, |
97 | 194x |
class = c("mmrm_fit", class(quiet_fit$result)) |
98 |
) |
|
99 |
} |
|
100 | ||
101 |
#' Summarizing List of Fits |
|
102 |
#' |
|
103 |
#' @param all_fits (`list` of `mmrm_fit` or `try-error`)\cr list of fits. |
|
104 |
#' |
|
105 |
#' @return List with `warnings`, `messages`, `log_liks` and `converged` results. |
|
106 |
#' @keywords internal |
|
107 |
h_summarize_all_fits <- function(all_fits) { |
|
108 | 8x |
assert_list(all_fits, types = c("mmrm_fit", "try-error")) |
109 | 8x |
is_error <- vapply(all_fits, is, logical(1), class2 = "try-error") |
110 | ||
111 | 8x |
warnings <- messages <- vector(mode = "list", length = length(all_fits)) |
112 | 8x |
warnings[is_error] <- lapply(all_fits[is_error], as.character) |
113 | 8x |
warnings[!is_error] <- lapply(all_fits[!is_error], attr, which = "warnings") |
114 | 8x |
messages[!is_error] <- lapply(all_fits[!is_error], attr, which = "messages") |
115 | 8x |
log_liks <- as.numeric(rep(NA, length.out = length(all_fits))) |
116 | 8x |
log_liks[!is_error] <- vapply(all_fits[!is_error], stats::logLik, numeric(1L)) |
117 | 8x |
converged <- rep(FALSE, length.out = length(all_fits)) |
118 | 8x |
converged[!is_error] <- vapply(all_fits[!is_error], attr, logical(1), which = "converged") |
119 | ||
120 | 8x |
list( |
121 | 8x |
warnings = warnings, |
122 | 8x |
messages = messages, |
123 | 8x |
log_liks = log_liks, |
124 | 8x |
converged = converged |
125 |
) |
|
126 |
} |
|
127 | ||
128 |
#' Refitting MMRM with Multiple Optimizers |
|
129 |
#' |
|
130 |
#' @description `r lifecycle::badge("stable")` |
|
131 |
#' |
|
132 |
#' @param fit (`mmrm_fit`)\cr original model fit from [fit_single_optimizer()]. |
|
133 |
#' @param ... Additional arguments passed to [mmrm_control()]. |
|
134 |
#' @param control (`mmrm_control`)\cr object. |
|
135 |
#' |
|
136 |
#' @return The best (in terms of log likelihood) fit which converged. |
|
137 |
#' |
|
138 |
#' @note For Windows, no parallel computations are currently implemented. |
|
139 |
#' @export |
|
140 |
#' |
|
141 |
#' @examples |
|
142 |
#' fit <- fit_single_optimizer( |
|
143 |
#' formula = FEV1 ~ RACE + SEX + ARMCD * AVISIT + us(AVISIT | USUBJID), |
|
144 |
#' data = fev_data, |
|
145 |
#' weights = rep(1, nrow(fev_data)), |
|
146 |
#' optimizer = "nlminb" |
|
147 |
#' ) |
|
148 |
#' best_fit <- refit_multiple_optimizers(fit) |
|
149 |
refit_multiple_optimizers <- function(fit, |
|
150 |
..., |
|
151 |
control = mmrm_control(...)) { |
|
152 | 6x |
assert_class(fit, "mmrm_fit") |
153 | 6x |
assert_class(control, "mmrm_control") |
154 | ||
155 | 6x |
n_cores_used <- ifelse( |
156 | 6x |
.Platform$OS.type == "windows", |
157 | 6x |
1L, |
158 | 6x |
min( |
159 | 6x |
length(control$optimizers), |
160 | 6x |
control$n_cores |
161 |
) |
|
162 |
) |
|
163 | 6x |
controls <- h_split_control( |
164 | 6x |
control, |
165 | 6x |
start = fit$theta_est |
166 |
) |
|
167 | ||
168 |
# Take the results from old fit as starting values for new fits. |
|
169 | 6x |
all_fits <- suppressWarnings(parallel::mcmapply( |
170 | 6x |
FUN = fit_single_optimizer, |
171 | 6x |
control = controls, |
172 | 6x |
MoreArgs = list( |
173 | 6x |
tmb_data = fit$tmb_data, |
174 | 6x |
formula_parts = fit$formula_parts |
175 |
), |
|
176 | 6x |
mc.cores = n_cores_used, |
177 | 6x |
mc.silent = TRUE, |
178 | 6x |
SIMPLIFY = FALSE |
179 |
)) |
|
180 | 6x |
all_fits <- c(all_fits, list(old_result = fit)) |
181 | ||
182 |
# Find the results that are ok and return best in terms of log-likelihood. |
|
183 | 6x |
all_fits_summary <- h_summarize_all_fits(all_fits) |
184 | 6x |
is_ok <- all_fits_summary$converged |
185 | 6x |
if (!any(is_ok)) { |
186 | 1x |
stop( |
187 | 1x |
"No optimizer led to a successful model fit. ", |
188 | 1x |
"Please try to use a different covariance structure or other covariates." |
189 |
) |
|
190 |
} |
|
191 | 5x |
best_optimizer <- which.max(all_fits_summary$log_liks[is_ok]) |
192 | 5x |
all_fits[[which(is_ok)[best_optimizer]]] |
193 |
} |
|
194 | ||
195 |
#' Control Parameters for Fitting an MMRM |
|
196 |
#' |
|
197 |
#' @description `r lifecycle::badge("stable")` |
|
198 |
#' Fine-grained specification of the MMRM fit details is possible using this |
|
199 |
#' control function. |
|
200 |
#' |
|
201 |
#' @param n_cores (`count`)\cr number of cores to be used. |
|
202 |
#' @param method (`string`)\cr adjustment method for degrees of freedom. |
|
203 |
#' @param vcov (`string`)\cr coefficients covariance matrix adjustment method. |
|
204 |
#' @param start (`NULL`, `numeric` or `function`)\cr optional start values for variance |
|
205 |
#' parameters. See details for more information. |
|
206 |
#' @param accept_singular (`flag`)\cr whether singular design matrices are reduced |
|
207 |
#' to full rank automatically and additional coefficient estimates will be missing. |
|
208 |
#' @param optimizers (`list`)\cr optimizer specification, created with [h_get_optimizers()]. |
|
209 |
#' @param drop_visit_levels (`flag`)\cr whether to drop levels for visit variable, |
|
210 |
#' if visit variable is a factor, see details. |
|
211 |
#' @param ... additional arguments passed to [h_get_optimizers()]. |
|
212 |
#' |
|
213 |
#' @details |
|
214 |
# - The `drop_visit_levels` flag will decide whether unobserved visits will be kept for analysis. |
|
215 |
#' For example, if the data only has observations at visits `VIS1`, `VIS3` and `VIS4`, by default |
|
216 |
#' they are treated to be equally spaced, the distance from `VIS1` to `VIS3`, and from `VIS3` to `VIS4`, |
|
217 |
#' are identical. However, you can manually convert this visit into a factor, with |
|
218 |
#' `levels = c("VIS1", "VIS2", "VIS3", "VIS4")`, and also use `drop_visits_levels = FALSE`, |
|
219 |
#' then the distance from `VIS1` to `VIS3` will be double, as `VIS2` is a valid visit. |
|
220 |
#' However, please be cautious because this can lead to convergence failure |
|
221 |
#' when using an unstructured covariance matrix and there are no observations |
|
222 |
#' at the missing visits. |
|
223 |
#' - The `method` and `vcov` arguments specify the degrees of freedom and coefficients |
|
224 |
#' covariance matrix adjustment methods, respectively. |
|
225 |
#' - Allowed `vcov` includes: "Asymptotic", "Kenward-Roger", "Kenward-Roger-Linear", "Empirical" (CR0), |
|
226 |
#' "Empirical-Jackknife" (CR3), and "Empirical-Bias-Reduced" (CR2). |
|
227 |
#' - Allowed `method` includes: "Satterthwaite", "Kenward-Roger", "Between-Within" and "Residual". |
|
228 |
#' - If `method` is "Kenward-Roger" then only "Kenward-Roger" or "Kenward-Roger-Linear" are allowed for `vcov`. |
|
229 |
#' - The `vcov` argument can be `NULL` to use the default covariance method depending on the `method` |
|
230 |
#' used for degrees of freedom, see the following table: |
|
231 |
#' |
|
232 |
#' | `method` | Default `vcov`| |
|
233 |
#' |-----------|----------| |
|
234 |
#' |Satterthwaite| Asymptotic| |
|
235 |
#' |Kenward-Roger| Kenward-Roger| |
|
236 |
#' |Residual| Empirical| |
|
237 |
#' |Between-Within| Asymptotic| |
|
238 |
#' |
|
239 |
#' - Please note that "Kenward-Roger" for "Unstructured" covariance gives different results |
|
240 |
#' compared to SAS; Use "Kenward-Roger-Linear" for `vcov` instead for better matching |
|
241 |
#' of the SAS results. |
|
242 |
#' |
|
243 |
#' - The argument `start` is used to facilitate the choice of initial values for fitting the model. |
|
244 |
#' If `function` is provided, make sure its parameter is a valid element of `mmrm_tmb_data` |
|
245 |
#' or `mmrm_tmb_formula_parts` and it returns a numeric vector. |
|
246 |
#' By default or if `NULL` is provided, `std_start` will be used. |
|
247 |
#' Other implemented methods include `emp_start`. |
|
248 |
#' |
|
249 |
#' @return List of class `mmrm_control` with the control parameters. |
|
250 |
#' @export |
|
251 |
#' |
|
252 |
#' @examples |
|
253 |
#' mmrm_control( |
|
254 |
#' optimizer_fun = stats::optim, |
|
255 |
#' optimizer_args = list(method = "L-BFGS-B") |
|
256 |
#' ) |
|
257 |
mmrm_control <- function(n_cores = 1L, |
|
258 |
method = c("Satterthwaite", "Kenward-Roger", "Residual", "Between-Within"), |
|
259 |
vcov = NULL, |
|
260 |
start = std_start, |
|
261 |
accept_singular = TRUE, |
|
262 |
drop_visit_levels = TRUE, |
|
263 |
..., |
|
264 |
optimizers = h_get_optimizers(...)) { |
|
265 | 243x |
assert_count(n_cores, positive = TRUE) |
266 | 243x |
assert_character(method) |
267 | 243x |
if (is.null(start)) { |
268 | 1x |
start <- std_start |
269 |
} |
|
270 | 243x |
assert( |
271 | 243x |
check_function(start, args = "..."), |
272 | 243x |
check_numeric(start, null.ok = FALSE), |
273 | 243x |
combine = "or" |
274 |
) |
|
275 | 243x |
assert_flag(accept_singular) |
276 | 243x |
assert_flag(drop_visit_levels) |
277 | 243x |
assert_list(optimizers, names = "unique", types = c("function", "partial")) |
278 | 243x |
assert_string(vcov, null.ok = TRUE) |
279 | 243x |
method <- match.arg(method) |
280 | 243x |
if (is.null(vcov)) { |
281 | 192x |
vcov <- h_get_cov_default(method) |
282 |
} |
|
283 | 243x |
assert_subset( |
284 | 243x |
vcov, |
285 | 243x |
c( |
286 | 243x |
"Asymptotic", |
287 | 243x |
"Empirical", |
288 | 243x |
"Empirical-Bias-Reduced", |
289 | 243x |
"Empirical-Jackknife", |
290 | 243x |
"Kenward-Roger", |
291 | 243x |
"Kenward-Roger-Linear" |
292 |
) |
|
293 |
) |
|
294 | 243x |
if (xor(identical(method, "Kenward-Roger"), vcov %in% c("Kenward-Roger", "Kenward-Roger-Linear"))) { |
295 | 5x |
stop(paste( |
296 | 5x |
"Kenward-Roger degrees of freedom must work together with Kenward-Roger", |
297 | 5x |
"or Kenward-Roger-Linear covariance!" |
298 |
)) |
|
299 |
} |
|
300 | 238x |
structure( |
301 | 238x |
list( |
302 | 238x |
optimizers = optimizers, |
303 | 238x |
start = start, |
304 | 238x |
accept_singular = accept_singular, |
305 | 238x |
method = method, |
306 | 238x |
vcov = vcov, |
307 | 238x |
n_cores = as.integer(n_cores), |
308 | 238x |
drop_visit_levels = drop_visit_levels |
309 |
), |
|
310 | 238x |
class = "mmrm_control" |
311 |
) |
|
312 |
} |
|
313 | ||
314 |
#' Fit an MMRM |
|
315 |
#' |
|
316 |
#' @description `r lifecycle::badge("stable")` |
|
317 |
#' |
|
318 |
#' This is the main function fitting the MMRM. |
|
319 |
#' |
|
320 |
#' @param formula (`formula`)\cr the model formula, see details. |
|
321 |
#' @param data (`data`)\cr the data to be used for the model. |
|
322 |
#' @param weights (`vector`)\cr an optional vector of weights to be used in |
|
323 |
#' the fitting process. Should be `NULL` or a numeric vector. |
|
324 |
#' @param reml (`flag`)\cr whether restricted maximum likelihood (REML) |
|
325 |
#' estimation is used, otherwise maximum likelihood (ML) is used. |
|
326 |
#' @param covariance (`cov_struct`)\cr a covariance structure type definition |
|
327 |
#' as produced with [cov_struct()], or value that can be coerced to a |
|
328 |
#' covariance structure using [as.cov_struct()]. If no value is provided, |
|
329 |
#' a structure is derived from the provided formula. |
|
330 |
#' @param control (`mmrm_control`)\cr fine-grained fitting specifications list |
|
331 |
#' created with [mmrm_control()]. |
|
332 |
#' @param ... arguments passed to [mmrm_control()]. |
|
333 |
#' |
|
334 |
#' @details |
|
335 |
#' The `formula` typically looks like: |
|
336 |
#' `FEV1 ~ RACE + SEX + ARMCD * AVISIT + us(AVISIT | USUBJID)` |
|
337 |
#' so specifies response and covariates as usual, and exactly one special term |
|
338 |
#' defines which covariance structure is used and what are the time point and |
|
339 |
#' subject variables. The covariance structures in the formula can be |
|
340 |
#' found in [`covariance_types`]. |
|
341 |
#' |
|
342 |
#' The time points have to be unique for each subject. That is, |
|
343 |
#' there cannot be time points with multiple observations for any subject. |
|
344 |
#' The rationale is that these observations would need to be correlated, but it |
|
345 |
#' is not possible within the currently implemented covariance structure framework |
|
346 |
#' to do that correctly. Moreover, for non-spatial covariance structures, the time |
|
347 |
#' variable must be a factor variable. |
|
348 |
#' |
|
349 |
#' When optimizer is not set, first the default optimizer |
|
350 |
#' (`L-BFGS-B`) is used to fit the model. If that converges, this is returned. |
|
351 |
#' If not, the other available optimizers from [h_get_optimizers()], |
|
352 |
#' including `BFGS`, `CG` and `nlminb` are |
|
353 |
#' tried (in parallel if `n_cores` is set and not on Windows). |
|
354 |
#' If none of the optimizers converge, then the function fails. Otherwise |
|
355 |
#' the best fit is returned. |
|
356 |
#' |
|
357 |
#' Note that fine-grained control specifications can either be passed directly |
|
358 |
#' to the `mmrm` function, or via the `control` argument for bundling together |
|
359 |
#' with the [mmrm_control()] function. Both cannot be used together, since |
|
360 |
#' this would delete the arguments passed via `mmrm`. |
|
361 |
#' |
|
362 |
#' @return An `mmrm` object. |
|
363 |
#' |
|
364 |
#' @note The `mmrm` object is also an `mmrm_fit` and an `mmrm_tmb` object, |
|
365 |
#' therefore corresponding methods also work (see [`mmrm_tmb_methods`]). |
|
366 |
#' |
|
367 |
#' Additional contents depend on the choice of the adjustment `method`: |
|
368 |
#' - If Satterthwaite adjustment is used, the Jacobian information `jac_list` |
|
369 |
#' is included. |
|
370 |
#' - If Kenward-Roger adjustment is used, `kr_comp` contains necessary |
|
371 |
#' components and `beta_vcov_adj` includes the adjusted coefficients covariance |
|
372 |
#' matrix. |
|
373 |
#' |
|
374 |
#' Use of the package `emmeans` is supported, see [`emmeans_support`]. |
|
375 |
#' |
|
376 |
#' NA values are always omitted regardless of `na.action` setting. |
|
377 |
#' |
|
378 |
#' When the number of visit levels is large, it usually requires large memory to create the |
|
379 |
#' covariance matrix. By default, the maximum allowed visit levels is 100, and if there are more |
|
380 |
#' visit levels, a confirmation is needed if run interactively. |
|
381 |
#' You can use `options(mmrm.max_visits = <target>)` to increase the maximum allowed number of visit |
|
382 |
#' levels. In non-interactive sessions the confirmation is not raised and will directly give you an error if |
|
383 |
#' the number of visit levels exceeds the maximum. |
|
384 |
#' |
|
385 |
#' @export |
|
386 |
#' |
|
387 |
#' @examples |
|
388 |
#' fit <- mmrm( |
|
389 |
#' formula = FEV1 ~ RACE + SEX + ARMCD * AVISIT + us(AVISIT | USUBJID), |
|
390 |
#' data = fev_data |
|
391 |
#' ) |
|
392 |
#' |
|
393 |
#' # Direct specification of control details: |
|
394 |
#' fit <- mmrm( |
|
395 |
#' formula = FEV1 ~ RACE + SEX + ARMCD * AVISIT + us(AVISIT | USUBJID), |
|
396 |
#' data = fev_data, |
|
397 |
#' weights = fev_data$WEIGHTS, |
|
398 |
#' method = "Kenward-Roger" |
|
399 |
#' ) |
|
400 |
#' |
|
401 |
#' # Alternative specification via control argument (but you cannot mix the |
|
402 |
#' # two approaches): |
|
403 |
#' fit <- mmrm( |
|
404 |
#' formula = FEV1 ~ RACE + SEX + ARMCD * AVISIT + us(AVISIT | USUBJID), |
|
405 |
#' data = fev_data, |
|
406 |
#' control = mmrm_control(method = "Kenward-Roger") |
|
407 |
#' ) |
|
408 |
mmrm <- function(formula, |
|
409 |
data, |
|
410 |
weights = NULL, |
|
411 |
covariance = NULL, |
|
412 |
reml = TRUE, |
|
413 |
control = mmrm_control(...), |
|
414 |
...) { |
|
415 | 175x |
assert_false(!missing(control) && !missing(...)) |
416 | 174x |
assert_class(control, "mmrm_control") |
417 | 169x |
assert_list(control$optimizers, min.len = 1) |
418 | ||
419 | 169x |
if (control$method %in% c("Kenward-Roger", "Kenward-Roger-Linear") && !reml) { |
420 | ! |
stop("Kenward-Roger only works for REML") |
421 |
} |
|
422 | 169x |
h_valid_formula(formula) |
423 | 168x |
covariance <- h_reconcile_cov_struct(formula, covariance) |
424 | 167x |
formula_parts <- h_mmrm_tmb_formula_parts(formula, covariance) |
425 | 167x |
h_tmb_warn_non_deterministic() |
426 | ||
427 | 167x |
if (!missing(data)) { |
428 | 166x |
attr(data, which = "dataname") <- toString(match.call()$data) |
429 |
} else { |
|
430 |
# na.action set to na.pass to allow data to be full; will be futher trimmed later |
|
431 | 1x |
data <- model.frame(formula_parts$full_formula, na.action = "na.pass") |
432 |
} |
|
433 | ||
434 | 167x |
if (is.null(weights)) { |
435 | 151x |
weights <- rep(1, nrow(data)) |
436 |
} else { |
|
437 | 16x |
attr(weights, which = "dataname") <- deparse(match.call()$weights) |
438 |
} |
|
439 | 167x |
tmb_data <- h_mmrm_tmb_data( |
440 | 167x |
formula_parts, data, weights, reml, |
441 | 167x |
singular = if (control$accept_singular) "drop" else "error", |
442 | 167x |
drop_visit_levels = control$drop_visit_levels, |
443 | 167x |
allow_na_response = FALSE |
444 |
) |
|
445 | 167x |
fit <- structure("", class = "try-error") |
446 | 167x |
names_all_optimizers <- names(control$optimizers) |
447 | 167x |
while (is(fit, "try-error") && length(control$optimizers) > 0) { |
448 | 171x |
fit <- fit_single_optimizer( |
449 | 171x |
tmb_data = tmb_data, |
450 | 171x |
formula_parts = formula_parts, |
451 | 171x |
control = control |
452 |
) |
|
453 | 168x |
if (is(fit, "try-error")) { |
454 | 6x |
warning(paste0( |
455 | 6x |
"Divergence with optimizer ", names(control$optimizers[1L]), " due to problems: ", |
456 | 6x |
toString(attr(fit, "divergence")) |
457 |
)) |
|
458 |
} |
|
459 | 168x |
control$optimizers <- control$optimizers[-1] |
460 |
} |
|
461 | 164x |
if (!attr(fit, "converged")) { |
462 | 7x |
more_optimizers <- length(control$optimizers) >= 1L |
463 | 7x |
if (more_optimizers) { |
464 | 5x |
fit <- refit_multiple_optimizers( |
465 | 5x |
fit = fit, |
466 | 5x |
control = control |
467 |
) |
|
468 |
} else { |
|
469 | 2x |
all_problems <- unlist( |
470 | 2x |
attributes(fit)[c("errors", "warnings")], |
471 | 2x |
use.names = FALSE |
472 |
) |
|
473 | 2x |
stop(paste0( |
474 | 2x |
"Chosen optimizers '", toString(names_all_optimizers), "' led to problems during model fit:\n", |
475 | 2x |
paste(paste0(seq_along(all_problems), ") ", all_problems), collapse = ";\n"), "\n", |
476 | 2x |
"Consider trying multiple or different optimizers." |
477 |
)) |
|
478 |
} |
|
479 |
} |
|
480 | 161x |
fit_msg <- attr(fit, "messages") |
481 | 161x |
if (!is.null(fit_msg)) { |
482 | ! |
message(paste(fit_msg, collapse = "\n")) |
483 |
} |
|
484 | 161x |
fit$call <- match.call() |
485 | 161x |
fit$call$formula <- formula |
486 | 161x |
fit$method <- control$method |
487 | 161x |
fit$vcov <- control$vcov |
488 | 161x |
if (control$vcov %in% c("Kenward-Roger", "Kenward-Roger-Linear")) { |
489 | 47x |
fit$kr_comp <- h_get_kr_comp(fit$tmb_data, fit$theta_est) |
490 | 47x |
fit$beta_vcov_adj <- h_var_adj( |
491 | 47x |
v = fit$beta_vcov, |
492 | 47x |
w = component(fit, "theta_vcov"), |
493 | 47x |
p = fit$kr_comp$P, |
494 | 47x |
q = fit$kr_comp$Q, |
495 | 47x |
r = fit$kr_comp$R, |
496 | 47x |
linear = (control$vcov == "Kenward-Roger-Linear") |
497 |
) |
|
498 | 114x |
} else if (control$vcov %in% c("Empirical", "Empirical-Bias-Reduced", "Empirical-Jackknife")) { |
499 | 31x |
empirical_comp <- h_get_empirical( |
500 | 31x |
fit$tmb_data, fit$theta_est, fit$beta_est, fit$beta_vcov, control$vcov |
501 |
) |
|
502 | 31x |
fit$beta_vcov_adj <- empirical_comp$cov |
503 | 31x |
fit$empirical_df_mat <- empirical_comp$df_mat |
504 | 31x |
dimnames(fit$beta_vcov_adj) <- dimnames(fit$beta_vcov) |
505 | 83x |
} else if (identical(control$vcov, "Asymptotic")) { |
506 |
# Note that we only need the Jacobian list under Asymptotic covariance method, |
|
507 |
# cf. the Satterthwaite vignette. |
|
508 | 83x |
if (identical(fit$method, "Satterthwaite")) { |
509 | 81x |
fit$jac_list <- h_jac_list(fit$tmb_data, fit$theta_est, fit$beta_vcov) |
510 |
} |
|
511 |
} else { |
|
512 | ! |
stop("Unrecognized coefficent variance-covariance method!") |
513 |
} |
|
514 | ||
515 | 161x |
class(fit) <- c("mmrm", class(fit)) |
516 | 161x |
fit |
517 |
} |
1 |
#' Capture all Output |
|
2 |
#' |
|
3 |
#' This function silences all warnings, errors & messages and instead returns a list |
|
4 |
#' containing the results (if it didn't error), as well as the warnings, errors |
|
5 |
#' and messages and divergence signals as character vectors. |
|
6 |
#' |
|
7 |
#' @param expr (`expression`)\cr to be executed. |
|
8 |
#' @param remove (`list`)\cr optional list with elements `warnings`, `errors`, |
|
9 |
#' `messages` which can be character vectors, which will be removed from the |
|
10 |
#' results if specified. |
|
11 |
#' @param divergence (`list`)\cr optional list similar as `remove`, but these |
|
12 |
#' character vectors will be moved to the `divergence` result and signal |
|
13 |
#' that the fit did not converge. |
|
14 |
#' |
|
15 |
#' @return |
|
16 |
#' A list containing |
|
17 |
#' |
|
18 |
#' - `result`: The object returned by `expr` or `list()` if an error was thrown. |
|
19 |
#' - `warnings`: `NULL` or a character vector if warnings were thrown. |
|
20 |
#' - `errors`: `NULL` or a string if an error was thrown. |
|
21 |
#' - `messages`: `NULL` or a character vector if messages were produced. |
|
22 |
#' - `divergence`: `NULL` or a character vector if divergence messages were caught. |
|
23 |
#' |
|
24 |
#' @keywords internal |
|
25 |
h_record_all_output <- function(expr, |
|
26 |
remove = list(), |
|
27 |
divergence = list()) { |
|
28 |
# Note: We don't need to and cannot assert `expr` here. |
|
29 | 201x |
assert_list(remove, types = "character") |
30 | 201x |
assert_list(divergence, types = "character") |
31 | 201x |
env <- new.env() |
32 | 201x |
result <- withCallingHandlers( |
33 | 201x |
withRestarts( |
34 | 201x |
expr, |
35 | 201x |
muffleStop = function(e) structure(e$message, class = "try-error") |
36 |
), |
|
37 | 201x |
message = function(m) { |
38 | 6x |
msg_without_newline <- gsub(m$message, pattern = "\n$", replacement = "") |
39 | 6x |
env$message <- c(env$message, msg_without_newline) |
40 | 6x |
invokeRestart("muffleMessage") |
41 |
}, |
|
42 | 201x |
warning = function(w) { |
43 | 14x |
env$warning <- c(env$warning, w$message) |
44 | 14x |
invokeRestart("muffleWarning") |
45 |
}, |
|
46 | 201x |
error = function(e) { |
47 | 14x |
env$error <- c(env$error, e$message) |
48 | 14x |
invokeRestart("muffleStop", e) |
49 |
} |
|
50 |
) |
|
51 | 201x |
list( |
52 | 201x |
result = result, |
53 | 201x |
warnings = setdiff(env$warning, c(remove$warnings, divergence$warnings)), |
54 | 201x |
errors = setdiff(env$error, c(remove$errors, divergence$errors)), |
55 | 201x |
messages = setdiff(env$message, c(remove$messages, divergence$messages)), |
56 | 201x |
divergence = c( |
57 | 201x |
intersect(env$warning, divergence$warnings), |
58 | 201x |
intersect(env$error, divergence$errors), |
59 | 201x |
intersect(env$message, divergence$messages) |
60 |
) |
|
61 |
) |
|
62 |
} |
|
63 | ||
64 |
#' Trace of a Matrix |
|
65 |
#' |
|
66 |
#' @description Obtain the trace of a matrix if the matrix is diagonal, otherwise raise an error. |
|
67 |
#' |
|
68 |
#' @param x (`matrix`)\cr square matrix input. |
|
69 |
#' |
|
70 |
#' @return The trace of the square matrix. |
|
71 |
#' |
|
72 |
#' @keywords internal |
|
73 |
h_tr <- function(x) { |
|
74 | 1790x |
if (nrow(x) != ncol(x)) { |
75 | 1x |
stop("x must be square matrix") |
76 |
} |
|
77 | 1789x |
sum(Matrix::diag(x)) |
78 |
} |
|
79 | ||
80 |
#' Split Control List |
|
81 |
#' |
|
82 |
#' @description Split the [mmrm_control()] object according to its optimizers and use additional arguments |
|
83 |
#' to replace the elements in the original object. |
|
84 |
#' |
|
85 |
#' @param control (`mmrm_control`)\cr object. |
|
86 |
#' @param ... additional parameters to update the `control` object. |
|
87 |
#' |
|
88 |
#' @return A `list` of `mmrm_control` entries. |
|
89 |
#' @keywords internal |
|
90 |
h_split_control <- function(control, ...) { |
|
91 | 8x |
assert_class(control, "mmrm_control") |
92 | 8x |
l <- length(control$optimizers) |
93 | 8x |
lapply(seq_len(l), function(i) { |
94 | 22x |
ret <- utils::modifyList(control, list(...)) |
95 | 22x |
ret$optimizers <- control$optimizers[i] |
96 | 22x |
ret |
97 |
}) |
|
98 |
} |
|
99 | ||
100 |
#' Obtain Optimizer according to Optimizer String Value |
|
101 |
#' |
|
102 |
#' @description This function creates optimizer functions with arguments. |
|
103 |
#' |
|
104 |
#' @param optimizer (`character`)\cr names of built-in optimizers to try, subset |
|
105 |
#' of "L-BFGS-B", "BFGS", "CG" and "nlminb". |
|
106 |
#' @param optimizer_fun (`function` or `list` of `function`)\cr alternatively to `optimizer`, |
|
107 |
#' an optimizer function or a list of optimizer functions can be passed directly here. |
|
108 |
#' @param optimizer_args (`list`)\cr additional arguments for `optimizer_fun`. |
|
109 |
#' @param optimizer_control (`list`)\cr passed to argument `control` in `optimizer_fun`. |
|
110 |
#' |
|
111 |
#' @details |
|
112 |
#' If you want to use only the built-in optimizers: |
|
113 |
#' - `optimizer` is a shortcut to create a list of built-in optimizer functions |
|
114 |
#' passed to `optimizer_fun`. |
|
115 |
#' - Allowed are "L-BFGS-B", "BFGS", "CG" (using [stats::optim()] with corresponding method) |
|
116 |
#' and "nlminb" (using [stats::nlminb()]). |
|
117 |
#' - Other arguments should go into `optimizer_args`. |
|
118 |
#' |
|
119 |
#' If you want to use your own optimizer function: |
|
120 |
#' - Make sure that there are three arguments: parameter (start value), objective function |
|
121 |
#' and gradient function are sequentially in the function arguments. |
|
122 |
#' - If there are other named arguments in front of these, make sure they are correctly |
|
123 |
#' specified through `optimizer_args`. |
|
124 |
#' - If the hessian can be used, please make sure its argument name is `hessian` and |
|
125 |
#' please add attribute `use_hessian = TRUE` to the function, |
|
126 |
#' using `attr(fun, "use_hessian) <- TRUE`. |
|
127 |
#' |
|
128 |
#' @return Named `list` of optimizers created by [h_partial_fun_args()]. |
|
129 |
#' |
|
130 |
#' @keywords internal |
|
131 |
h_get_optimizers <- function(optimizer = c("L-BFGS-B", "BFGS", "CG", "nlminb"), |
|
132 |
optimizer_fun = h_optimizer_fun(optimizer), |
|
133 |
optimizer_args = list(), |
|
134 |
optimizer_control = list()) { |
|
135 | 246x |
if ("automatic" %in% optimizer) { |
136 | 1x |
lifecycle::deprecate_warn( |
137 | 1x |
when = "0.2.0", |
138 | 1x |
what = I("\"automatic\" optimizer"), |
139 | 1x |
details = "please just omit optimizer argument" |
140 |
) |
|
141 | 1x |
optimizer_fun <- h_optimizer_fun() |
142 |
} |
|
143 | 246x |
assert( |
144 | 246x |
test_function(optimizer_fun), |
145 | 246x |
test_list(optimizer_fun, types = "function", names = "unique") |
146 |
) |
|
147 | 246x |
if (is.function(optimizer_fun)) { |
148 | 7x |
optimizer_fun <- list(custom_optimizer = optimizer_fun) |
149 |
} |
|
150 | 246x |
lapply(optimizer_fun, function(x) { |
151 | 924x |
do.call(h_partial_fun_args, c(list(fun = x, control = optimizer_control), optimizer_args)) |
152 |
}) |
|
153 |
} |
|
154 | ||
155 |
#' Obtain Optimizer Function with Character |
|
156 |
#' @description Obtain the optimizer function through the character provided. |
|
157 |
#' @param optimizer (`character`)\cr vector of optimizers. |
|
158 |
#' |
|
159 |
#' @return A (`list`)\cr of optimizer functions generated from [h_partial_fun_args()]. |
|
160 |
#' @keywords internal |
|
161 |
h_optimizer_fun <- function(optimizer = c("L-BFGS-B", "BFGS", "CG", "nlminb")) { |
|
162 | 240x |
optimizer <- match.arg(optimizer, several.ok = TRUE) |
163 | 240x |
lapply(stats::setNames(optimizer, optimizer), function(x) { |
164 | 920x |
switch(x, |
165 | 229x |
"L-BFGS-B" = h_partial_fun_args(fun = stats::optim, method = x), |
166 | 230x |
"BFGS" = h_partial_fun_args(fun = stats::optim, method = x), |
167 | 228x |
"CG" = h_partial_fun_args(fun = stats::optim, method = x), |
168 | 233x |
"nlminb" = h_partial_fun_args(fun = stats::nlminb, additional_attr = list(use_hessian = TRUE)) |
169 |
) |
|
170 |
}) |
|
171 |
} |
|
172 | ||
173 |
#' Create Partial Functions |
|
174 |
#' @description Creates partial functions with arguments. |
|
175 |
#' |
|
176 |
#' @param fun (`function`)\cr to be wrapped. |
|
177 |
#' @param ... Additional arguments for `fun`. |
|
178 |
#' @param additional_attr (`list`)\cr of additional attributes to apply to the result. |
|
179 |
#' |
|
180 |
#' @details This function add `args` attribute to the original function, |
|
181 |
#' and add an extra class `partial` to the function. |
|
182 |
#' `args` is the argument for the function, and elements in `...` will override the existing |
|
183 |
#' arguments in attribute `args`. `additional_attr` will override the existing attributes. |
|
184 |
#' |
|
185 |
#' @return Object with S3 class `"partial"`, a `function` with `args` attribute (and possibly more |
|
186 |
#' attributes from `additional_attr`). |
|
187 |
#' @keywords internal |
|
188 |
h_partial_fun_args <- function(fun, ..., additional_attr = list()) { |
|
189 | 1848x |
assert_function(fun) |
190 | 1848x |
assert_list(additional_attr, names = "unique") |
191 | 1848x |
a_args <- list(...) |
192 | 1848x |
assert_list(a_args, names = "unique") |
193 | 1848x |
args <- attr(fun, "args") |
194 | 1848x |
if (is.null(args)) { |
195 | 932x |
args <- list() |
196 |
} |
|
197 | 1848x |
do.call( |
198 | 1848x |
structure, |
199 | 1848x |
args = utils::modifyList( |
200 | 1848x |
list( |
201 | 1848x |
.Data = fun, |
202 | 1848x |
args = utils::modifyList(args, a_args), |
203 | 1848x |
class = c("partial", "function") |
204 |
), |
|
205 | 1848x |
additional_attr |
206 |
) |
|
207 |
) |
|
208 |
} |
|
209 | ||
210 |
#' Obtain Default Covariance Method |
|
211 |
#' |
|
212 |
#' @description Obtain the default covariance method depending on |
|
213 |
#' the degrees of freedom method used. |
|
214 |
#' |
|
215 |
#' @param method (`string`)\cr degrees of freedom method. |
|
216 |
#' |
|
217 |
#' @details The default covariance method is different for different degrees of freedom method. |
|
218 |
#' For "Satterthwaite" or "Between-Within", "Asymptotic" is returned. |
|
219 |
#' For "Kenward-Roger" only, "Kenward-Roger" is returned. |
|
220 |
#' For "Residual" only, "Empirical" is returned. |
|
221 |
#' |
|
222 |
#' @return String of the default covariance method. |
|
223 |
#' @keywords internal |
|
224 |
h_get_cov_default <- function(method = c("Satterthwaite", "Kenward-Roger", "Residual", "Between-Within")) { |
|
225 | 197x |
assert_string(method) |
226 | 197x |
method <- match.arg(method) |
227 | 196x |
switch(method, |
228 | 1x |
"Residual" = "Empirical", |
229 | 158x |
"Satterthwaite" = "Asymptotic", |
230 | 35x |
"Kenward-Roger" = "Kenward-Roger", |
231 | 2x |
"Between-Within" = "Asymptotic" |
232 |
) |
|
233 |
} |
|
234 | ||
235 |
#' Complete `character` Vector Names From Values |
|
236 |
#' |
|
237 |
#' @param x (`character` or `list`)\cr value whose names should be completed |
|
238 |
#' from element values. |
|
239 |
#' |
|
240 |
#' @return A named vector or list. |
|
241 |
#' |
|
242 |
#' @keywords internal |
|
243 |
fill_names <- function(x) { |
|
244 | 4x |
n <- names(x) |
245 | 4x |
is_unnamed <- if (is.null(n)) rep_len(TRUE, length(x)) else n == "" |
246 | 4x |
names(x)[is_unnamed] <- x[is_unnamed] |
247 | 4x |
x |
248 |
} |
|
249 | ||
250 |
#' Drop Items from an Indexible |
|
251 |
#' |
|
252 |
#' Drop elements from an indexible object (`vector`, `list`, etc.). |
|
253 |
#' |
|
254 |
#' @param x Any object that can be consumed by [seq_along()] and indexed by a |
|
255 |
#' logical vector of the same length. |
|
256 |
#' @param n (`integer`)\cr the number of terms to drop. |
|
257 |
#' |
|
258 |
#' @return A subset of `x`. |
|
259 |
#' |
|
260 |
#' @keywords internal |
|
261 |
drop_elements <- function(x, n) { |
|
262 | 819x |
x[seq_along(x) > n] |
263 |
} |
|
264 | ||
265 |
#' Ask for Confirmation on Large Visit Levels |
|
266 |
#' |
|
267 |
#' @description Ask the user for confirmation if there are too many visit levels |
|
268 |
#' for non-spatial covariance structure in interactive sessions. |
|
269 |
#' |
|
270 |
#' @param x (`numeric`)\cr number of visit levels. |
|
271 |
#' |
|
272 |
#' @return Logical value `TRUE`. |
|
273 |
#' @keywords internal |
|
274 |
h_confirm_large_levels <- function(x) { |
|
275 | 297x |
assert_count(x) |
276 | 297x |
allowed_lvls <- x <= getOption("mmrm.max_visits", 100) |
277 | 297x |
if (allowed_lvls) { |
278 | 295x |
return(TRUE) |
279 |
} |
|
280 | 2x |
if (!interactive()) { |
281 | 2x |
stop("Visit levels too large!", call. = FALSE) |
282 |
} |
|
283 | ! |
proceed <- utils::askYesNo( |
284 | ! |
paste( |
285 | ! |
"Visit levels is possibly too large.", |
286 | ! |
"This requires large memory. Are you sure to continue?", |
287 | ! |
collapse = " " |
288 |
) |
|
289 |
) |
|
290 | ! |
if (!identical(proceed, TRUE)) { |
291 | ! |
stop("Visit levels too large!", call. = FALSE) |
292 |
} |
|
293 | ! |
return(TRUE) |
294 |
} |
|
295 | ||
296 |
#' Default Value on NULL |
|
297 |
#' Return default value when first argument is NULL. |
|
298 |
#' |
|
299 |
#' @param x Object. |
|
300 |
#' @param y Object. |
|
301 |
#' |
|
302 |
#' @details If `x` is NULL, returns `y`. Otherwise return `x`. |
|
303 |
#' |
|
304 |
#' @keywords internal |
|
305 |
h_default_value <- function(x, y) { |
|
306 | 312x |
if (is.null(x)) { |
307 | 277x |
y |
308 |
} else { |
|
309 | 35x |
x |
310 |
} |
|
311 |
} |
|
312 | ||
313 |
#' Warn on na.action |
|
314 |
#' @keywords internal |
|
315 |
h_warn_na_action <- function() { |
|
316 | 260x |
if (!identical(getOption("na.action"), "na.omit")) { |
317 | 6x |
warning("na.action is always set to `na.omit` for `mmrm` fit!") |
318 |
} |
|
319 |
} |
|
320 | ||
321 |
#' Obtain `na.action` as Function |
|
322 |
#' @keywords internal |
|
323 |
h_get_na_action <- function(na_action) { |
|
324 | 56x |
if (is.function(na_action) && identical(methods::formalArgs(na_action), c("object", "..."))) { |
325 | 5x |
return(na_action) |
326 |
} |
|
327 | 51x |
if (is.character(na_action) && length(na_action) == 1L) { |
328 | 51x |
assert_subset(na_action, c("na.omit", "na.exclude", "na.fail", "na.pass", "na.contiguous")) |
329 | 51x |
return(get(na_action, mode = "function", pos = "package:stats")) |
330 |
} |
|
331 |
} |
|
332 | ||
333 |
#' Validate mmrm Formula |
|
334 |
#' @param formula (`formula`)\cr to check. |
|
335 |
#' |
|
336 |
#' @details In mmrm models, `.` is not allowed as it introduces ambiguity of covariates |
|
337 |
#' to be used, so it is not allowed to be in formula. |
|
338 |
#' |
|
339 |
#' @keywords internal |
|
340 |
h_valid_formula <- function(formula) { |
|
341 | 183x |
assert_formula(formula) |
342 | 183x |
if ("." %in% all.vars(formula)) { |
343 | 2x |
stop("`.` is not allowed in mmrm models!") |
344 |
} |
|
345 |
} |
|
346 | ||
347 |
#' Standard Starting Value |
|
348 |
#' |
|
349 |
#' @description Obtain standard start values. |
|
350 |
#' |
|
351 |
#' @param cov_type (`string`)\cr name of the covariance structure. |
|
352 |
#' @param n_visits (`int`)\cr number of visits. |
|
353 |
#' @param n_groups (`int`)\cr number of groups. |
|
354 |
#' @param ... not used. |
|
355 |
#' |
|
356 |
#' @details |
|
357 |
#' `std_start` will try to provide variance parameter from identity matrix. |
|
358 |
#' However, for `ar1` and `ar1h` the corresponding values are not ideal because the |
|
359 |
#' \eqn{\rho} is usually a positive number thus using 0 as starting value can lead to |
|
360 |
#' incorrect optimization result, and we use 0.5 as the initial value of \eqn{\rho}. |
|
361 |
#' |
|
362 |
#' @return A numeric vector of starting values. |
|
363 |
#' |
|
364 |
#' @export |
|
365 |
std_start <- function(cov_type, n_visits, n_groups, ...) { |
|
366 | 502x |
assert_string(cov_type) |
367 | 502x |
assert_subset(cov_type, cov_types(c("abbr", "habbr"))) |
368 | 502x |
assert_int(n_visits, lower = 1L) |
369 | 502x |
assert_int(n_groups, lower = 1L) |
370 | 502x |
start_value <- switch(cov_type, |
371 | 502x |
us = rep(0, n_visits * (n_visits + 1) / 2), |
372 | 502x |
toep = rep(0, n_visits), |
373 | 502x |
toeph = rep(0, 2 * n_visits - 1), |
374 | 502x |
ar1 = c(0, 0.5), |
375 | 502x |
ar1h = c(rep(0, n_visits), 0.5), |
376 | 502x |
ad = rep(0, n_visits), |
377 | 502x |
adh = rep(0, 2 * n_visits - 1), |
378 | 502x |
cs = rep(0, 2), |
379 | 502x |
csh = rep(0, n_visits + 1), |
380 | 502x |
sp_exp = rep(0, 2) |
381 |
) |
|
382 | 502x |
rep(start_value, n_groups) |
383 |
} |
|
384 | ||
385 |
#' Empirical Starting Value |
|
386 |
#' |
|
387 |
#' @description Obtain empirical start value for unstructured covariance |
|
388 |
#' |
|
389 |
#' @param data (`data.frame`)\cr data used for model fitting. |
|
390 |
#' @param model_formula (`formula`)\cr the formula in mmrm model without covariance structure part. |
|
391 |
#' @param visit_var (`string`)\cr visit variable. |
|
392 |
#' @param subject_var (`string`)\cr subject id variable. |
|
393 |
#' @param subject_groups (`factor`)\cr subject group assignment. |
|
394 |
#' @param ... not used. |
|
395 |
#' |
|
396 |
#' @details |
|
397 |
#' This `emp_start` only works for unstructured covariance structure. |
|
398 |
#' It uses linear regression to first obtain the coefficients and use the residuals |
|
399 |
#' to obtain the empirical variance-covariance, and it is then used to obtain the |
|
400 |
#' starting values. |
|
401 |
#' |
|
402 |
#' @note `data` is used instead of `full_frame` because `full_frame` is already |
|
403 |
#' transformed if model contains transformations, e.g. `log(FEV1) ~ exp(FEV1_BL)` will |
|
404 |
#' drop `FEV1` and `FEV1_BL` but add `log(FEV1)` and `exp(FEV1_BL)` in `full_frame`. |
|
405 |
#' |
|
406 |
#' @return A numeric vector of starting values. |
|
407 |
#' |
|
408 |
#' @export |
|
409 |
emp_start <- function(data, model_formula, visit_var, subject_var, subject_groups, ...) { |
|
410 | 4x |
assert_formula(model_formula) |
411 | 4x |
assert_data_frame(data) |
412 | 4x |
assert_subset(all.vars(model_formula), colnames(data)) |
413 | 4x |
assert_string(visit_var) |
414 | 4x |
assert_string(subject_var) |
415 | 4x |
assert_factor(data[[visit_var]]) |
416 | 4x |
n_visits <- length(levels(data[[visit_var]])) |
417 | 4x |
assert_factor(data[[subject_var]]) |
418 | 4x |
subjects <- droplevels(data[[subject_var]]) |
419 | 4x |
n_subjects <- length(levels(subjects)) |
420 | 4x |
fit <- stats::lm(formula = model_formula, data = data) |
421 | 4x |
res <- rep(NA, n_subjects * n_visits) |
422 | 4x |
res[ |
423 | 4x |
n_visits * as.integer(subjects) - n_visits + as.integer(data[[visit_var]]) |
424 | 4x |
] <- residuals(fit) |
425 | 4x |
res_mat <- matrix(res, ncol = n_visits, nrow = n_subjects, byrow = TRUE) |
426 | 4x |
emp_covs <- lapply( |
427 | 4x |
unname(split(seq_len(n_subjects), subject_groups)), |
428 | 4x |
function(x) { |
429 | 4x |
stats::cov(res_mat[x, , drop = FALSE], use = "pairwise.complete.obs") |
430 |
} |
|
431 |
) |
|
432 | 4x |
unlist(lapply(emp_covs, h_get_theta_from_cov)) |
433 |
} |
|
434 |
#' Obtain Theta from Covariance Matrix |
|
435 |
#' |
|
436 |
#' @description Obtain unstructured theta from covariance matrix. |
|
437 |
#' |
|
438 |
#' @param covariance (`matrix`) of covariance matrix values. |
|
439 |
#' |
|
440 |
#' @details |
|
441 |
#' If the covariance matrix has `NA` in some of the elements, they will be replaced by |
|
442 |
#' 0 (non-diagonal) and 1 (diagonal). This ensures that the matrix is positive definite. |
|
443 |
#' |
|
444 |
#' @return Numeric vector of the theta values. |
|
445 |
#' @keywords internal |
|
446 |
h_get_theta_from_cov <- function(covariance) { |
|
447 | 7x |
assert_matrix(covariance, mode = "numeric", ncols = nrow(covariance)) |
448 | 7x |
covariance[is.na(covariance)] <- 0 |
449 | 7x |
diag(covariance)[diag(covariance) == 0] <- 1 |
450 |
# empirical is not always positive definite in some special cases of numeric singularity. |
|
451 | 7x |
qr_res <- qr(covariance) |
452 | 7x |
if (qr_res$rank < ncol(covariance)) { |
453 | ! |
covariance <- Matrix::nearPD(covariance)$mat |
454 |
} |
|
455 | 7x |
emp_chol <- t(chol(covariance)) |
456 | 7x |
mat <- t(solve(diag(diag(emp_chol)), emp_chol)) |
457 | 7x |
ret <- c(log(diag(emp_chol)), mat[upper.tri(mat)]) |
458 | 7x |
unname(ret) |
459 |
} |
|
460 | ||
461 |
#' Register S3 Method |
|
462 |
#' Register S3 method to a generic. |
|
463 |
#' |
|
464 |
#' @param pkg (`string`) name of the package name. |
|
465 |
#' @param generic (`string`) name of the generic. |
|
466 |
#' @param class (`string`) class name the function want to dispatch. |
|
467 |
#' @param envir (`environment`) the location the method is defined. |
|
468 |
#' |
|
469 |
#' @details This function is adapted from `emmeans:::register_s3_method()`. |
|
470 |
#' |
|
471 |
#' @keywords internal |
|
472 |
h_register_s3 <- function(pkg, generic, class, envir = parent.frame()) { |
|
473 | 1x |
assert_string(pkg) |
474 | 1x |
assert_string(generic) |
475 | 1x |
assert_string(class) |
476 | 1x |
assert_environment(envir) |
477 | 1x |
fun <- get(paste0(generic, ".", class), envir = envir) |
478 | 1x |
if (isNamespaceLoaded(pkg)) { |
479 | 1x |
registerS3method(generic, class, fun, envir = asNamespace(pkg)) |
480 |
} |
|
481 | 1x |
setHook(packageEvent(pkg, "onLoad"), function(...) { |
482 | ! |
registerS3method(generic, class, fun, envir = asNamespace(pkg)) |
483 |
}) |
|
484 |
} |
|
485 | ||
486 |
#' Check if a Factor Should Drop Levels |
|
487 |
#' |
|
488 |
#' @param x (`vector`) vector to check. |
|
489 |
#' |
|
490 |
#' @keywords internal |
|
491 |
h_extra_levels <- function(x) { |
|
492 | 1629x |
is.factor(x) && length(levels(x)) > length(unique(x)) |
493 |
} |
|
494 | ||
495 |
#' Drop Levels from Dataset |
|
496 |
#' @param data (`data.frame`) data to drop levels. |
|
497 |
#' @param subject_var (`character`) subject variable. |
|
498 |
#' @param visit_var (`character`) visit variable. |
|
499 |
#' @param except (`character`) variables to exclude from dropping. |
|
500 |
#' @keywords internal |
|
501 |
h_drop_levels <- function(data, subject_var, visit_var, except) { |
|
502 | 263x |
assert_data_frame(data) |
503 | 263x |
assert_character(subject_var) |
504 | 263x |
assert_character(visit_var) |
505 | 263x |
assert_character(except, null.ok = TRUE) |
506 | 263x |
all_cols <- colnames(data) |
507 | 263x |
to_drop <- vapply( |
508 | 263x |
data, |
509 | 263x |
h_extra_levels, |
510 | 263x |
logical(1L) |
511 |
) |
|
512 | 263x |
to_drop <- all_cols[to_drop] |
513 |
# only drop levels for those not defined in excep and not in visit_var. |
|
514 | 263x |
to_drop <- setdiff(to_drop, c(visit_var, except)) |
515 | 263x |
data[to_drop] <- lapply(data[to_drop], droplevels) |
516 |
# subject var are always dropped and no message given. |
|
517 | 263x |
dropped <- setdiff(to_drop, subject_var) |
518 | 263x |
if (length(dropped) > 0) { |
519 | 3x |
message( |
520 | 3x |
"Some factor levels are dropped due to singular design matrix: ", |
521 | 3x |
toString(dropped) |
522 |
) |
|
523 |
} |
|
524 | 263x |
data |
525 |
} |
|
526 | ||
527 |
#' Warn if TMB is Configured to Use Non-Deterministic Hash for Tape Optimizer |
|
528 |
#' |
|
529 |
#' This function checks the TMB configuration for the `tmbad_deterministic_hash` setting |
|
530 |
#' If it is set to `FALSE`, a warning is issued indicating that this may lead to |
|
531 |
#' unreproducible results. |
|
532 |
#' |
|
533 |
#' @return No return value, called for side effects. |
|
534 |
#' @keywords internal |
|
535 |
h_tmb_warn_non_deterministic <- function() { |
|
536 | 169x |
if (utils::packageVersion("TMB") < "1.9.15") { |
537 | ! |
return() |
538 |
} |
|
539 | 169x |
tmb_config <- TMB::config(DLL = "mmrm") |
540 | 169x |
tape_deterministic <- tmb_config$tmbad_deterministic_hash |
541 | 169x |
if (!tape_deterministic) { |
542 | 2x |
msg <- paste( |
543 | 2x |
"TMB is configured to use a non-deterministic hash for its tape optimizer,", |
544 | 2x |
"and this may lead to unreproducible results.", |
545 | 2x |
"To disable this behavior, use `TMB::config(tmbad_deterministic_hash = 1)`.", |
546 | 2x |
sep = "\n" |
547 |
) |
|
548 | 2x |
warning(msg) |
549 |
} |
|
550 |
} |
1 |
#' Extract Formula Terms used for Covariance Structure Definition |
|
2 |
#' |
|
3 |
#' @param f (`formula`)\cr a formula from which covariance terms should be |
|
4 |
#' extracted. |
|
5 |
#' |
|
6 |
#' @return A list of covariance structure expressions found in `f`. |
|
7 |
#' |
|
8 |
#' @importFrom stats terms |
|
9 |
#' @keywords internal |
|
10 |
h_extract_covariance_terms <- function(f) { |
|
11 | 291x |
specials <- cov_types(c("abbr", "habbr")) |
12 | 291x |
terms <- stats::terms(formula_rhs(f), specials = specials) |
13 | 291x |
covariance_terms <- Filter(length, attr(terms, "specials")) |
14 | 291x |
variables <- attr(terms, "variables") |
15 | 291x |
lapply(covariance_terms, function(i) variables[[i + 1]]) |
16 |
} |
|
17 | ||
18 |
#' Drop Formula Terms used for Covariance Structure Definition |
|
19 |
#' |
|
20 |
#' @param f (`formula`)\cr a formula from which covariance terms should be |
|
21 |
#' dropped. |
|
22 |
#' |
|
23 |
#' @return The formula without accepted covariance terms. |
|
24 |
#' |
|
25 |
#' @details `terms` is used and it will preserve the environment attribute. |
|
26 |
#' This ensures the returned formula and the input formula have the same environment. |
|
27 |
#' @importFrom stats terms drop.terms |
|
28 |
#' @keywords internal |
|
29 |
h_drop_covariance_terms <- function(f) { |
|
30 | 274x |
specials <- cov_types(c("abbr", "habbr")) |
31 | ||
32 | 274x |
terms <- stats::terms(f, specials = specials) |
33 | 274x |
covariance_terms <- Filter(Negate(is.null), attr(terms, "specials")) |
34 | ||
35 |
# if no covariance terms were found, return original formula |
|
36 | 274x |
if (length(covariance_terms) == 0) { |
37 | 6x |
return(f) |
38 |
} |
|
39 | 268x |
if (length(f) != 3) { |
40 | 1x |
update_str <- "~ . -" |
41 |
} else { |
|
42 | 267x |
update_str <- ". ~ . -" |
43 |
} |
|
44 | 268x |
stats::update( |
45 | 268x |
f, |
46 | 268x |
stats::as.formula(paste(update_str, deparse(attr(terms, "variables")[[covariance_terms[[1]] + 1]]))) |
47 |
) |
|
48 |
} |
|
49 | ||
50 |
#' Add Individual Covariance Variables As Terms to Formula |
|
51 |
#' |
|
52 |
#' @param f (`formula`)\cr a formula to which covariance structure terms should |
|
53 |
#' be added. |
|
54 |
#' @param covariance (`cov_struct`)\cr a covariance structure object from which |
|
55 |
#' additional variables should be sourced. |
|
56 |
#' |
|
57 |
#' @return A new formula with included covariance terms. |
|
58 |
#' |
|
59 |
#' @details [stats::update()] is used to append the covariance structure and the environment |
|
60 |
#' attribute will not be changed. This ensures the returned formula and the input formula |
|
61 |
#' have the same environment. |
|
62 |
#' |
|
63 |
#' @keywords internal |
|
64 |
h_add_covariance_terms <- function(f, covariance) { |
|
65 | 272x |
cov_terms <- with(covariance, c(subject, visits, group)) |
66 | 266x |
cov_terms <- paste(cov_terms, collapse = " + ") |
67 | 266x |
stats::update(f, stats::as.formula(paste(". ~ . + ", cov_terms))) |
68 |
} |
|
69 | ||
70 |
#' Add Formula Terms with Character |
|
71 |
#' |
|
72 |
#' Add formula terms from the original formula with character representation. |
|
73 |
#' |
|
74 |
#' @param f (`formula`)\cr a formula to be updated. |
|
75 |
#' @param adds (`character`)\cr representation of elements to be added. |
|
76 |
#' @param drop_response (`flag`)\cr whether response should be dropped. |
|
77 |
#' |
|
78 |
#' @details Elements in `adds` will be added from the formula, while the environment |
|
79 |
#' of the formula is unchanged. If `adds` is `NULL` or `character(0)`, the formula is |
|
80 |
#' unchanged. |
|
81 |
#' @return A new formula with elements in `drops` removed. |
|
82 |
#' |
|
83 |
#' @keywords internal |
|
84 |
h_add_terms <- function(f, adds, drop_response = FALSE) { |
|
85 | 599x |
assert_character(adds, null.ok = TRUE) |
86 | 599x |
if (length(adds) > 0L) { |
87 | 321x |
add_terms <- stats::as.formula(sprintf(". ~ . + %s", paste(adds, collapse = "+"))) |
88 | 321x |
f <- stats::update(f, add_terms) |
89 |
} |
|
90 | 599x |
if (drop_response && length(f) == 3L) { |
91 | 35x |
f[[2]] <- NULL |
92 |
} |
|
93 | 599x |
f |
94 |
} |
1 |
#' Methods for `mmrm_tmb` Objects |
|
2 |
#' |
|
3 |
#' @description `r lifecycle::badge("stable")` |
|
4 |
#' |
|
5 |
#' @param object (`mmrm_tmb`)\cr the fitted MMRM object. |
|
6 |
#' @param x (`mmrm_tmb`)\cr same as `object`. |
|
7 |
#' @param formula (`mmrm_tmb`)\cr same as `object`. |
|
8 |
#' @param complete (`flag`)\cr whether to include potential non-estimable |
|
9 |
#' coefficients. |
|
10 |
#' @param ... mostly not used; |
|
11 |
#' Exception is `model.matrix()` passing `...` to the default method. |
|
12 |
#' @return Depends on the method, see Functions. |
|
13 |
#' |
|
14 |
#' @name mmrm_tmb_methods |
|
15 |
#' |
|
16 |
#' @seealso [`mmrm_methods`], [`mmrm_tidiers`] for additional methods. |
|
17 |
#' |
|
18 |
#' @examples |
|
19 |
#' formula <- FEV1 ~ RACE + SEX + ARMCD * AVISIT + us(AVISIT | USUBJID) |
|
20 |
#' object <- fit_mmrm(formula, fev_data, weights = rep(1, nrow(fev_data))) |
|
21 |
NULL |
|
22 | ||
23 |
#' @describeIn mmrm_tmb_methods obtains the estimated coefficients. |
|
24 |
#' @importFrom stats coef |
|
25 |
#' @exportS3Method |
|
26 |
#' @examples |
|
27 |
#' # Estimated coefficients: |
|
28 |
#' coef(object) |
|
29 |
coef.mmrm_tmb <- function(object, complete = TRUE, ...) { |
|
30 | 58x |
assert_flag(complete) |
31 | 58x |
nm <- if (complete) "beta_est_complete" else "beta_est" |
32 | 58x |
component(object, name = nm) |
33 |
} |
|
34 | ||
35 |
#' @describeIn mmrm_tmb_methods obtains the fitted values. |
|
36 |
#' @importFrom stats fitted |
|
37 |
#' @exportS3Method |
|
38 |
#' @examples |
|
39 |
#' # Fitted values: |
|
40 |
#' fitted(object) |
|
41 |
fitted.mmrm_tmb <- function(object, ...) { |
|
42 | 19x |
fitted_col <- component(object, "x_matrix") %*% component(object, "beta_est") |
43 | 19x |
fitted_col[, 1L, drop = TRUE] |
44 |
} |
|
45 | ||
46 |
#' @describeIn mmrm_tmb_methods predict conditional means for new data; |
|
47 |
#' optionally with standard errors and confidence or prediction intervals. |
|
48 |
#' Returns a vector of predictions if `se.fit == FALSE` and |
|
49 |
#' `interval == "none"`; otherwise it returns a data.frame with multiple |
|
50 |
#' columns and one row per input data row. |
|
51 |
#' |
|
52 |
#' @param newdata (`data.frame`)\cr optional new data, otherwise data from `object` is used. |
|
53 |
#' @param se.fit (`flag`)\cr indicator if standard errors are required. |
|
54 |
#' @param interval (`string`)\cr type of interval calculation. Can be abbreviated. |
|
55 |
#' @param level (`number`)\cr tolerance/confidence level. |
|
56 |
#' @param nsim (`count`)\cr number of simulations to use. |
|
57 |
#' @param conditional (`flag`)\cr indicator if the prediction is conditional on the observation or not. |
|
58 |
#' |
|
59 |
#' @importFrom stats predict |
|
60 |
#' @exportS3Method |
|
61 |
#' |
|
62 |
#' @examples |
|
63 |
#' predict(object, newdata = fev_data) |
|
64 |
predict.mmrm_tmb <- function(object, |
|
65 |
newdata, |
|
66 |
se.fit = FALSE, # nolint |
|
67 |
interval = c("none", "confidence", "prediction"), |
|
68 |
level = 0.95, |
|
69 |
nsim = 1000L, |
|
70 |
conditional = FALSE, |
|
71 |
...) { |
|
72 | 45x |
if (missing(newdata)) { |
73 | 8x |
newdata <- object$data |
74 |
} |
|
75 | 45x |
assert_data_frame(newdata) |
76 | 45x |
orig_row_names <- row.names(newdata) |
77 | 45x |
assert_flag(se.fit) |
78 | 45x |
assert_number(level, lower = 0, upper = 1) |
79 | 45x |
assert_count(nsim, positive = TRUE) |
80 | 45x |
assert_flag(conditional) |
81 | 45x |
interval <- match.arg(interval) |
82 | 45x |
formula_parts <- object$formula_parts |
83 | 45x |
if (any(object$tmb_data$x_cols_aliased)) { |
84 | 1x |
warning( |
85 | 1x |
"In fitted object there are co-linear variables and therefore dropped terms, ", |
86 | 1x |
"and this could lead to incorrect prediction on new data." |
87 |
) |
|
88 |
} |
|
89 | 45x |
colnames <- names(Filter(isFALSE, object$tmb_data$x_cols_aliased)) |
90 | 45x |
if (!conditional && interval %in% c("none", "confidence")) { |
91 |
# model.matrix always return a complete matrix (no NA allowed) |
|
92 | 27x |
x_mat <- stats::model.matrix(object, data = newdata, use_response = FALSE)[, colnames, drop = FALSE] |
93 | 27x |
x_mat_full <- matrix( |
94 | 27x |
NA, |
95 | 27x |
nrow = nrow(newdata), ncol = ncol(x_mat), |
96 | 27x |
dimnames = list(row.names(newdata), colnames(x_mat)) |
97 |
) |
|
98 | 27x |
x_mat_full[row.names(x_mat), ] <- x_mat |
99 | 27x |
predictions <- (x_mat_full %*% component(object, "beta_est"))[, 1] |
100 | 27x |
predictions_raw <- stats::setNames(rep(NA_real_, nrow(newdata)), row.names(newdata)) |
101 | 27x |
predictions_raw[names(predictions)] <- predictions |
102 | 27x |
if (identical(interval, "none")) { |
103 | 20x |
return(predictions_raw) |
104 |
} |
|
105 | 7x |
se <- switch(interval, |
106 |
# can be NA if there are aliased cols |
|
107 | 7x |
"confidence" = diag(x_mat_full %*% component(object, "beta_vcov") %*% t(x_mat_full)), |
108 | 7x |
"none" = NA_real_ |
109 |
) |
|
110 | 7x |
res <- cbind( |
111 | 7x |
fit = predictions, se = se, |
112 | 7x |
lwr = predictions - stats::qnorm(1 - level / 2) * se, upr = predictions + stats::qnorm(1 - level / 2) * se |
113 |
) |
|
114 | 7x |
if (!se.fit) { |
115 | 1x |
res <- res[, setdiff(colnames(res), "se")] |
116 |
} |
|
117 | 7x |
res_raw <- matrix( |
118 | 7x |
NA_real_, |
119 | 7x |
ncol = ncol(res), nrow = nrow(newdata), |
120 | 7x |
dimnames = list(row.names(newdata), colnames(res)) |
121 |
) |
|
122 | 7x |
res_raw[row.names(res), ] <- res |
123 | 7x |
return(res_raw) |
124 |
} |
|
125 | 18x |
tmb_data <- h_mmrm_tmb_data( |
126 | 18x |
formula_parts, newdata, |
127 | 18x |
weights = rep(1, nrow(newdata)), |
128 | 18x |
reml = TRUE, |
129 | 18x |
singular = "keep", |
130 | 18x |
drop_visit_levels = FALSE, |
131 | 18x |
allow_na_response = TRUE, |
132 | 18x |
drop_levels = FALSE, |
133 | 18x |
xlev = component(object, "xlev"), |
134 | 18x |
contrasts = component(object, "contrasts") |
135 |
) |
|
136 | 18x |
tmb_data$x_matrix <- tmb_data$x_matrix[, colnames, drop = FALSE] |
137 | 18x |
predictions <- h_get_prediction( |
138 | 18x |
tmb_data, object$theta_est, object$beta_est, component(object, "beta_vcov") |
139 | 18x |
)$prediction |
140 | 18x |
res <- cbind(fit = rep(NA_real_, nrow(newdata))) |
141 | 18x |
new_order <- match(row.names(tmb_data$full_frame), orig_row_names) |
142 | 18x |
res[new_order, "fit"] <- predictions[, "fit"] |
143 | 18x |
se <- switch(interval, |
144 | 18x |
"confidence" = sqrt(predictions[, "conf_var"]), |
145 | 18x |
"prediction" = sqrt(h_get_prediction_variance(object, nsim, tmb_data)), |
146 | 18x |
"none" = NULL |
147 |
) |
|
148 | 18x |
if (interval != "none") { |
149 | 7x |
res <- cbind( |
150 | 7x |
res, |
151 | 7x |
se = NA_real_ |
152 |
) |
|
153 | 7x |
res[new_order, "se"] <- se |
154 | 7x |
alpha <- 1 - level |
155 | 7x |
z <- stats::qnorm(1 - alpha / 2) * res[, "se"] |
156 | 7x |
res <- cbind( |
157 | 7x |
res, |
158 | 7x |
lwr = res[, "fit"] - z, |
159 | 7x |
upr = res[, "fit"] + z |
160 |
) |
|
161 | 7x |
if (!se.fit) { |
162 | ! |
res <- res[, setdiff(colnames(res), "se")] |
163 |
} |
|
164 |
} |
|
165 |
# Use original names. |
|
166 | 18x |
row.names(res) <- orig_row_names |
167 | 18x |
if (ncol(res) == 1) { |
168 | 11x |
res <- res[, "fit"] |
169 |
} |
|
170 | 18x |
return(res) |
171 |
} |
|
172 | ||
173 |
#' Get Prediction |
|
174 |
#' |
|
175 |
#' @description Get predictions with given `data`, `theta`, `beta`, `beta_vcov`. |
|
176 |
#' |
|
177 |
#' @details See `predict` function in `predict.cpp` which is called internally. |
|
178 |
#' |
|
179 |
#' @param tmb_data (`mmrm_tmb_data`)\cr object. |
|
180 |
#' @param theta (`numeric`)\cr theta value. |
|
181 |
#' @param beta (`numeric`)\cr beta value. |
|
182 |
#' @param beta_vcov (`matrix`)\cr beta_vcov matrix. |
|
183 |
#' |
|
184 |
#' @return List with: |
|
185 |
#' - `prediction`: Matrix with columns `fit`, `conf_var`, and `var`. |
|
186 |
#' - `covariance`: List with subject specific covariance matrices. |
|
187 |
#' - `index`: List of zero-based subject indices. |
|
188 |
#' |
|
189 |
#' @keywords internal |
|
190 |
h_get_prediction <- function(tmb_data, theta, beta, beta_vcov) { |
|
191 | 1696x |
assert_class(tmb_data, "mmrm_tmb_data") |
192 | 1696x |
assert_numeric(theta) |
193 | 1696x |
n_beta <- ncol(tmb_data$x_matrix) |
194 | 1696x |
assert_numeric(beta, finite = TRUE, any.missing = FALSE, len = n_beta) |
195 | 1696x |
assert_matrix(beta_vcov, mode = "numeric", any.missing = FALSE, nrows = n_beta, ncols = n_beta) |
196 | 1696x |
.Call(`_mmrm_predict`, PACKAGE = "mmrm", tmb_data, theta, beta, beta_vcov) |
197 |
} |
|
198 | ||
199 |
#' Get Prediction Variance |
|
200 |
#' |
|
201 |
#' @description Get prediction variance with given fit, `tmb_data` with the Monte Carlo sampling method. |
|
202 |
#' |
|
203 |
#' @param object (`mmrm_tmb`)\cr the fitted MMRM. |
|
204 |
#' @param nsim (`count`)\cr number of samples. |
|
205 |
#' @param tmb_data (`mmrm_tmb_data`)\cr object. |
|
206 |
#' |
|
207 |
#' @keywords internal |
|
208 |
h_get_prediction_variance <- function(object, nsim, tmb_data) { |
|
209 | 7x |
assert_class(object, "mmrm_tmb") |
210 | 7x |
assert_class(tmb_data, "mmrm_tmb_data") |
211 | 7x |
assert_count(nsim, positive = TRUE) |
212 | 7x |
theta_chol <- chol(object$theta_vcov) |
213 | 7x |
n_theta <- length(object$theta_est) |
214 | 7x |
res <- replicate(nsim, { |
215 | 1150x |
z <- stats::rnorm(n = n_theta) |
216 | 1150x |
theta_sample <- object$theta_est + z %*% theta_chol |
217 | 1150x |
cond_beta_results <- object$tmb_object$report(theta_sample) |
218 | 1150x |
beta_mean <- cond_beta_results$beta |
219 | 1150x |
beta_cov <- cond_beta_results$beta_vcov |
220 | 1150x |
h_get_prediction(tmb_data, theta_sample, beta_mean, beta_cov)$prediction |
221 |
}) |
|
222 | 7x |
mean_of_var <- rowMeans(res[, "var", ]) |
223 | 7x |
var_of_mean <- apply(res[, "fit", ], 1, stats::var) |
224 | 7x |
mean_of_var + var_of_mean |
225 |
} |
|
226 | ||
227 |
#' @describeIn mmrm_tmb_methods obtains the model frame. |
|
228 |
#' @param data (`data.frame`)\cr object in which to construct the frame. |
|
229 |
#' @param include (`character`)\cr names of variable types to include. |
|
230 |
#' Must be `NULL` or one or more of `c("subject_var", "visit_var", "group_var", "response_var")`. |
|
231 |
#' @param full (`flag`)\cr indicator whether to return full model frame (deprecated). |
|
232 |
#' @param na.action (`string`)\cr na action. |
|
233 |
#' @importFrom stats model.frame |
|
234 |
#' @exportS3Method |
|
235 |
#' |
|
236 |
#' @details |
|
237 |
#' `include` argument controls the variables the returned model frame will include. |
|
238 |
#' Possible options are "response_var", "subject_var", "visit_var" and "group_var", representing the |
|
239 |
#' response variable, subject variable, visit variable or group variable. |
|
240 |
#' `character` values in new data will always be factorized according to the data in the fit |
|
241 |
#' to avoid mismatched in levels or issues in `model.matrix`. |
|
242 |
#' |
|
243 |
#' @examples |
|
244 |
#' # Model frame: |
|
245 |
#' model.frame(object) |
|
246 |
#' model.frame(object, include = "subject_var") |
|
247 |
model.frame.mmrm_tmb <- function(formula, data, include = c("subject_var", "visit_var", "group_var", "response_var"), |
|
248 |
full, na.action = "na.omit", ...) { # nolint |
|
249 |
# Construct updated formula and data arguments. |
|
250 | 46x |
lst_formula_and_data <- |
251 | 46x |
h_construct_model_frame_inputs( |
252 | 46x |
formula = formula, |
253 | 46x |
data = data, |
254 | 46x |
include = include, |
255 | 46x |
full = full |
256 |
) |
|
257 |
# Only if include is default (full) and also data is missing, and also na.action is na.omit we will |
|
258 |
# use the model frame from the tmb_data. |
|
259 | 46x |
include_choice <- c("subject_var", "visit_var", "group_var", "response_var") |
260 | 46x |
if (missing(data) && setequal(include, include_choice) && identical(h_get_na_action(na.action), stats::na.omit)) { |
261 | 2x |
ret <- formula$tmb_data$full_frame |
262 |
# Remove weights column. |
|
263 | 2x |
ret[, "(weights)"] <- NULL |
264 | 2x |
ret |
265 |
} else { |
|
266 |
# Construct data frame to return to users. |
|
267 | 44x |
ret <- |
268 | 44x |
stats::model.frame( |
269 | 44x |
formula = lst_formula_and_data$formula, |
270 | 44x |
data = h_get_na_action(na.action)(lst_formula_and_data$data), |
271 | 44x |
na.action = na.action, |
272 | 44x |
xlev = stats::.getXlevels(terms(formula), formula$tmb_data$full_frame) |
273 |
) |
|
274 |
} |
|
275 | 45x |
ret |
276 |
} |
|
277 | ||
278 | ||
279 |
#' Construction of Model Frame Formula and Data Inputs |
|
280 |
#' |
|
281 |
#' @description |
|
282 |
#' Input formulas are converted from mmrm-style to a style compatible |
|
283 |
#' with default [stats::model.frame()] and [stats::model.matrix()] methods. |
|
284 |
#' |
|
285 |
#' The full formula is returned so we can construct, for example, the |
|
286 |
#' `model.frame()` including all columns as well as the requested subset. |
|
287 |
#' The full set is used to identify rows to include in the reduced model frame. |
|
288 |
#' |
|
289 |
#' @param formula (`mmrm`)\cr mmrm fit object. |
|
290 |
#' @param data optional data frame that will be |
|
291 |
#' passed to `model.frame()` or `model.matrix()` |
|
292 |
#' @param include (`character`)\cr names of variable to include |
|
293 |
#' @param full (`flag`)\cr indicator whether to return full model frame (deprecated). |
|
294 |
#' |
|
295 |
#' @return named list with four elements: |
|
296 |
#' - `"formula"`: the formula including the columns requested in the `include=` argument. |
|
297 |
#' - `"data"`: a data frame including all columns needed in the formula. |
|
298 |
#' full formula are identical |
|
299 |
#' @keywords internal |
|
300 |
h_construct_model_frame_inputs <- function(formula, |
|
301 |
data, |
|
302 |
include, |
|
303 |
include_choice = c("subject_var", "visit_var", "group_var", "response_var"), |
|
304 |
full) { |
|
305 | 280x |
if (!missing(full) && identical(full, TRUE)) { |
306 | ! |
lifecycle::deprecate_warn("0.3", "model.frame.mmrm_tmb(full)") |
307 | ! |
include <- include_choice |
308 |
} |
|
309 | ||
310 | 280x |
assert_class(formula, classes = "mmrm_tmb") |
311 | 280x |
assert_subset(include, include_choice) |
312 | 280x |
if (missing(data)) { |
313 | 256x |
data <- formula$data |
314 |
} |
|
315 | 280x |
assert_data_frame(data) |
316 | ||
317 | 280x |
drop_response <- !"response_var" %in% include |
318 | 280x |
add_vars <- unlist(formula$formula_parts[include]) |
319 | 280x |
new_formula <- h_add_terms(formula$formula_parts$model_formula, add_vars, drop_response) |
320 | ||
321 | 280x |
drop_response_full <- !"response_var" %in% include_choice |
322 | 280x |
add_vars_full <- unlist(formula$formula_parts[include_choice]) |
323 | 280x |
new_formula_full <- |
324 | 280x |
h_add_terms(formula$formula_parts$model_formula, add_vars_full, drop_response_full) |
325 | ||
326 |
# Update data based on the columns in the full formula return. |
|
327 | 280x |
all_vars <- all.vars(new_formula_full) |
328 | 280x |
assert_names(colnames(data), must.include = all_vars) |
329 | 280x |
data <- data[, all_vars, drop = FALSE] |
330 | ||
331 |
# Return list with updated formula, data. |
|
332 | 280x |
list( |
333 | 280x |
formula = new_formula, |
334 | 280x |
data = data |
335 |
) |
|
336 |
} |
|
337 | ||
338 |
#' @describeIn mmrm_tmb_methods obtains the model matrix. |
|
339 |
#' @exportS3Method |
|
340 |
#' @param use_response (`flag`)\cr whether to use the response for complete rows. |
|
341 |
#' |
|
342 |
#' @examples |
|
343 |
#' # Model matrix: |
|
344 |
#' model.matrix(object) |
|
345 |
model.matrix.mmrm_tmb <- function(object, data, use_response = TRUE, ...) { # nolint |
|
346 |
# Always return the utilized model matrix if data not provided. |
|
347 | 37x |
if (missing(data)) { |
348 | 3x |
return(object$tmb_data$x_matrix) |
349 |
} |
|
350 | 34x |
stats::model.matrix( |
351 | 34x |
h_add_terms(object$formula_parts$model_formula, NULL, drop_response = !use_response), |
352 | 34x |
data = data, |
353 | 34x |
contrasts.arg = attr(object$tmb_data$x_matrix, "contrasts"), |
354 | 34x |
xlev = component(object, "xlev"), |
355 |
... |
|
356 |
) |
|
357 |
} |
|
358 | ||
359 |
#' @describeIn mmrm_tmb_methods obtains the terms object. |
|
360 |
#' @importFrom stats model.frame |
|
361 |
#' @exportS3Method |
|
362 |
#' |
|
363 |
#' @examples |
|
364 |
#' # terms: |
|
365 |
#' terms(object) |
|
366 |
#' terms(object, include = "subject_var") |
|
367 |
terms.mmrm_tmb <- function(x, include = "response_var", ...) { # nolint |
|
368 |
# Construct updated formula and data arguments. |
|
369 | 231x |
lst_formula_and_data <- |
370 | 231x |
h_construct_model_frame_inputs( |
371 | 231x |
formula = x, |
372 | 231x |
include = include |
373 |
) |
|
374 | ||
375 |
# Use formula method for `terms()` to construct the mmrm terms object. |
|
376 | 231x |
stats::terms( |
377 | 231x |
x = lst_formula_and_data$formula, |
378 | 231x |
data = lst_formula_and_data$data |
379 |
) |
|
380 |
} |
|
381 | ||
382 | ||
383 |
#' @describeIn mmrm_tmb_methods obtains the attained log likelihood value. |
|
384 |
#' @importFrom stats logLik |
|
385 |
#' @exportS3Method |
|
386 |
#' @examples |
|
387 |
#' # Log likelihood given the estimated parameters: |
|
388 |
#' logLik(object) |
|
389 |
logLik.mmrm_tmb <- function(object, ...) { |
|
390 | 50x |
-component(object, "neg_log_lik") |
391 |
} |
|
392 | ||
393 |
#' @describeIn mmrm_tmb_methods obtains the used formula. |
|
394 |
#' @importFrom stats formula |
|
395 |
#' @exportS3Method |
|
396 |
#' @examples |
|
397 |
#' # Formula which was used: |
|
398 |
#' formula(object) |
|
399 |
formula.mmrm_tmb <- function(x, ...) { |
|
400 | 5x |
x$formula_parts$formula |
401 |
} |
|
402 | ||
403 |
#' @describeIn mmrm_tmb_methods obtains the variance-covariance matrix estimate |
|
404 |
#' for the coefficients. |
|
405 |
#' @importFrom stats vcov |
|
406 |
#' @exportS3Method |
|
407 |
#' @examples |
|
408 |
#' # Variance-covariance matrix estimate for coefficients: |
|
409 |
#' vcov(object) |
|
410 |
vcov.mmrm_tmb <- function(object, complete = TRUE, ...) { |
|
411 | 3x |
assert_flag(complete) |
412 | 3x |
nm <- if (complete) "beta_vcov_complete" else "beta_vcov" |
413 | 3x |
component(object, name = nm) |
414 |
} |
|
415 | ||
416 |
#' @describeIn mmrm_tmb_methods obtains the variance-covariance matrix estimate |
|
417 |
#' for the residuals. |
|
418 |
#' @param sigma cannot be used (this parameter does not exist in MMRM). |
|
419 |
#' @importFrom nlme VarCorr |
|
420 |
#' @export VarCorr |
|
421 |
#' @aliases VarCorr |
|
422 |
#' @exportS3Method |
|
423 |
#' @examples |
|
424 |
#' # Variance-covariance matrix estimate for residuals: |
|
425 |
#' VarCorr(object) |
|
426 |
VarCorr.mmrm_tmb <- function(x, sigma = NA, ...) { # nolint |
|
427 | 10x |
assert_scalar_na(sigma) |
428 | ||
429 | 10x |
component(x, name = "varcor") |
430 |
} |
|
431 | ||
432 |
#' @describeIn mmrm_tmb_methods obtains the deviance, which is defined here |
|
433 |
#' as twice the negative log likelihood, which can either be integrated |
|
434 |
#' over the coefficients for REML fits or the usual one for ML fits. |
|
435 |
#' @importFrom stats deviance |
|
436 |
#' @exportS3Method |
|
437 |
#' @examples |
|
438 |
#' # REML criterion (twice the negative log likelihood): |
|
439 |
#' deviance(object) |
|
440 |
deviance.mmrm_tmb <- function(object, ...) { |
|
441 | 74x |
2 * component(object, "neg_log_lik") |
442 |
} |
|
443 | ||
444 |
#' @describeIn mmrm_tmb_methods obtains the Akaike Information Criterion, |
|
445 |
#' where the degrees of freedom are the number of variance parameters (`n_theta`). |
|
446 |
#' If `corrected`, then this is multiplied with `m / (m - n_theta - 1)` where |
|
447 |
#' `m` is the number of observations minus the number of coefficients, or |
|
448 |
#' `n_theta + 2` if it is smaller than that \insertCite{hurvich1989regression,burnham1998practical}{mmrm}. |
|
449 |
#' @param corrected (`flag`)\cr whether corrected AIC should be calculated. |
|
450 |
#' @param k (`number`)\cr the penalty per parameter to be used; default `k = 2` |
|
451 |
#' is the classical AIC. |
|
452 |
#' @importFrom stats AIC |
|
453 |
#' @exportS3Method |
|
454 |
#' @examples |
|
455 |
#' # AIC: |
|
456 |
#' AIC(object) |
|
457 |
#' AIC(object, corrected = TRUE) |
|
458 |
#' @references |
|
459 |
#' - \insertRef{hurvich1989regression}{mmrm} |
|
460 |
#' - \insertRef{burnham1998practical}{mmrm} |
|
461 |
AIC.mmrm_tmb <- function(object, corrected = FALSE, ..., k = 2) { |
|
462 |
# nolint |
|
463 | 44x |
assert_flag(corrected) |
464 | 44x |
assert_number(k, lower = 1) |
465 | ||
466 | 44x |
n_theta <- length(component(object, "theta_est")) |
467 | 44x |
df <- if (!corrected) { |
468 | 43x |
n_theta |
469 |
} else { |
|
470 | 1x |
n_obs <- length(component(object, "y_vector")) |
471 | 1x |
n_beta <- length(component(object, "beta_est")) |
472 | 1x |
m <- max(n_theta + 2, n_obs - n_beta) |
473 | 1x |
n_theta * (m / (m - n_theta - 1)) |
474 |
} |
|
475 | ||
476 | 44x |
2 * component(object, "neg_log_lik") + k * df |
477 |
} |
|
478 | ||
479 |
#' @describeIn mmrm_tmb_methods obtains the Bayesian Information Criterion, |
|
480 |
#' which is using the natural logarithm of the number of subjects for the |
|
481 |
#' penalty parameter `k`. |
|
482 |
#' @importFrom stats BIC |
|
483 |
#' @exportS3Method |
|
484 |
#' @examples |
|
485 |
#' # BIC: |
|
486 |
#' BIC(object) |
|
487 |
BIC.mmrm_tmb <- function(object, ...) { |
|
488 |
# nolint |
|
489 | 21x |
k <- log(component(object, "n_subjects")) |
490 | 21x |
AIC(object, corrected = FALSE, k = k) |
491 |
} |
|
492 | ||
493 | ||
494 |
#' @describeIn mmrm_tmb_methods prints the object. |
|
495 |
#' @exportS3Method |
|
496 |
print.mmrm_tmb <- function(x, |
|
497 |
...) { |
|
498 | 2x |
cat("mmrm fit\n\n") |
499 | ||
500 | 2x |
h_print_call( |
501 | 2x |
component(x, "call"), component(x, "n_obs"), |
502 | 2x |
component(x, "n_subjects"), component(x, "n_timepoints") |
503 |
) |
|
504 | 2x |
h_print_cov(component(x, "cov_type"), component(x, "n_theta"), component(x, "n_groups")) |
505 | ||
506 | 2x |
cat("Inference: ") |
507 | 2x |
cat(ifelse(component(x, "reml"), "REML", "ML")) |
508 | 2x |
cat("\n") |
509 | 2x |
cat("Deviance: ") |
510 | 2x |
cat(deviance(x)) |
511 | ||
512 | 2x |
cat("\n\nCoefficients: ") |
513 | 2x |
n_singular_coefs <- sum(component(x, "beta_aliased")) |
514 | 2x |
if (n_singular_coefs > 0) { |
515 | 1x |
cat("(", n_singular_coefs, " not defined because of singularities)", sep = "") |
516 |
} |
|
517 | 2x |
cat("\n") |
518 | 2x |
print(coef(x, complete = TRUE)) |
519 | ||
520 | 2x |
cat("\nModel Inference Optimization:") |
521 | ||
522 | 2x |
cat(ifelse(component(x, "convergence") == 0, "\nConverged", "\nFailed to converge")) |
523 | 2x |
cat( |
524 | 2x |
" with code", component(x, "convergence"), |
525 | 2x |
"and message:", |
526 | 2x |
if (is.null(component(x, "conv_message"))) "No message provided." else tolower(component(x, "conv_message")) |
527 |
) |
|
528 | 2x |
cat("\n") |
529 | 2x |
invisible(x) |
530 |
} |
|
531 | ||
532 | ||
533 |
#' @describeIn mmrm_tmb_methods to obtain residuals - either unscaled ('response'), 'pearson' or 'normalized'. |
|
534 |
#' @param type (`string`)\cr unscaled (`response`), `pearson` or `normalized`. Default is `response`, |
|
535 |
#' and this is the only type available for use with models with a spatial covariance structure. |
|
536 |
#' @importFrom stats residuals |
|
537 |
#' @exportS3Method |
|
538 |
#' @examples |
|
539 |
#' # residuals: |
|
540 |
#' residuals(object, type = "response") |
|
541 |
#' residuals(object, type = "pearson") |
|
542 |
#' residuals(object, type = "normalized") |
|
543 |
#' @references |
|
544 |
#' - \insertRef{galecki2013linear}{mmrm} |
|
545 |
residuals.mmrm_tmb <- function(object, type = c("response", "pearson", "normalized"), ...) { |
|
546 | 20x |
type <- match.arg(type) |
547 | 20x |
switch(type, |
548 | 8x |
"response" = h_residuals_response(object), |
549 | 5x |
"pearson" = h_residuals_pearson(object), |
550 | 7x |
"normalized" = h_residuals_normalized(object) |
551 |
) |
|
552 |
} |
|
553 |
#' Calculate Pearson Residuals |
|
554 |
#' |
|
555 |
#' This is used by [residuals.mmrm_tmb()] to calculate Pearson residuals. |
|
556 |
#' |
|
557 |
#' @param object (`mmrm_tmb`)\cr the fitted MMRM. |
|
558 |
#' |
|
559 |
#' @return Vector of residuals. |
|
560 |
#' |
|
561 |
#' @keywords internal |
|
562 |
h_residuals_pearson <- function(object) { |
|
563 | 6x |
assert_class(object, "mmrm_tmb") |
564 | 6x |
h_residuals_response(object) * object$tmb_object$report()$diag_cov_inv_sqrt |
565 |
} |
|
566 | ||
567 |
#' Calculate normalized residuals |
|
568 |
#' |
|
569 |
#' This is used by [residuals.mmrm_tmb()] to calculate normalized / scaled residuals. |
|
570 |
#' |
|
571 |
#' @param object (`mmrm_tmb`)\cr the fitted MMRM. |
|
572 |
#' |
|
573 |
#' @return Vector of residuals |
|
574 |
#' |
|
575 |
#' @keywords internal |
|
576 |
h_residuals_normalized <- function(object) { |
|
577 | 8x |
assert_class(object, "mmrm_tmb") |
578 | 8x |
object$tmb_object$report()$epsilonTilde |
579 |
} |
|
580 |
#' Calculate response residuals. |
|
581 |
#' |
|
582 |
#' This is used by [residuals.mmrm_tmb()] to calculate response residuals. |
|
583 |
#' |
|
584 |
#' @param object (`mmrm_tmb`)\cr the fitted MMRM. |
|
585 |
#' |
|
586 |
#' @return Vector of residuals |
|
587 |
#' |
|
588 |
#' @keywords internal |
|
589 |
h_residuals_response <- function(object) { |
|
590 | 15x |
assert_class(object, "mmrm_tmb") |
591 | 15x |
component(object, "y_vector") - unname(fitted(object)) |
592 |
} |
|
593 | ||
594 |
#' @describeIn mmrm_tmb_methods simulate responses from a fitted model according |
|
595 |
#' to the simulation `method`, returning a `data.frame` of dimension `[n, m]` |
|
596 |
#' where n is the number of rows in `newdata`, |
|
597 |
#' and m is the number `nsim` of simulated responses. |
|
598 |
#' |
|
599 |
#' @param seed unused argument from [stats::simulate()]. |
|
600 |
#' @param method (`string`)\cr simulation method to use. If "conditional", |
|
601 |
#' simulated values are sampled given the estimated covariance matrix of `object`. |
|
602 |
#' If "marginal", the variance of the estimated covariance matrix is taken into account. |
|
603 |
#' |
|
604 |
#' @importFrom stats simulate |
|
605 |
#' @exportS3Method |
|
606 |
simulate.mmrm_tmb <- function(object, |
|
607 |
nsim = 1, |
|
608 |
seed = NULL, |
|
609 |
newdata, |
|
610 |
..., |
|
611 |
method = c("conditional", "marginal")) { |
|
612 | 15x |
assert_count(nsim, positive = TRUE) |
613 | 15x |
assert_null(seed) |
614 | 15x |
if (missing(newdata)) { |
615 | 12x |
newdata <- object$data |
616 |
} |
|
617 | 15x |
assert_data_frame(newdata) |
618 | 15x |
method <- match.arg(method) |
619 | ||
620 | ||
621 | 15x |
tmb_data <- h_mmrm_tmb_data( |
622 | 15x |
object$formula_parts, newdata, |
623 | 15x |
weights = rep(1, nrow(newdata)), |
624 | 15x |
reml = TRUE, |
625 | 15x |
singular = "keep", |
626 | 15x |
drop_visit_levels = FALSE, |
627 | 15x |
allow_na_response = TRUE, |
628 | 15x |
drop_levels = FALSE, |
629 | 15x |
xlev = component(object, "xlev"), |
630 | 15x |
contrasts = component(object, "contrasts") |
631 |
) |
|
632 | 15x |
ret <- if (method == "conditional") { |
633 | 8x |
predict_res <- h_get_prediction(tmb_data, object$theta_est, object$beta_est, object$beta_vcov) |
634 | 8x |
as.data.frame(h_get_sim_per_subj(predict_res, tmb_data$n_subjects, nsim)) |
635 | 15x |
} else if (method == "marginal") { |
636 | 7x |
theta_chol <- t(chol(object$theta_vcov)) |
637 | 7x |
n_theta <- length(object$theta_est) |
638 | 7x |
as.data.frame( |
639 | 7x |
sapply(seq_len(nsim), function(x) { |
640 | 503x |
newtheta <- object$theta_est + theta_chol %*% matrix(stats::rnorm(n_theta), ncol = 1) |
641 |
# Recalculate betas with sampled thetas. |
|
642 | 503x |
hold <- object$tmb_object$report(newtheta) |
643 |
# Resample betas given new beta distribution. |
|
644 |
# We first solve L^\top w = D^{-1/2}z_{sample}: |
|
645 | 503x |
w_sample <- backsolve( |
646 | 503x |
r = hold$XtWX_L, |
647 | 503x |
x = stats::rnorm(length(hold$beta)) / sqrt(hold$XtWX_D), |
648 | 503x |
upper.tri = FALSE, |
649 | 503x |
transpose = TRUE |
650 |
) |
|
651 |
# Then we add the mean vector, the beta estimate. |
|
652 | 503x |
beta_sample <- hold$beta + w_sample |
653 | 503x |
predict_res <- h_get_prediction(tmb_data, newtheta, beta_sample, hold$beta_vcov) |
654 | 503x |
h_get_sim_per_subj(predict_res, tmb_data$n_subjects, 1L) |
655 |
}) |
|
656 |
) |
|
657 |
} |
|
658 | 15x |
orig_row_names <- row.names(newdata) |
659 | 15x |
new_order <- match(orig_row_names, row.names(tmb_data$full_frame)) |
660 | 15x |
ret[new_order, , drop = FALSE] |
661 |
} |
|
662 | ||
663 |
#' Get simulated values by patient. |
|
664 |
#' |
|
665 |
#' @param predict_res (`list`)\cr from [h_get_prediction()]. |
|
666 |
#' @param nsub (`count`)\cr number of subjects. |
|
667 |
#' @param nsim (`count`)\cr number of values to simulate. |
|
668 |
#' |
|
669 |
#' @keywords internal |
|
670 |
h_get_sim_per_subj <- function(predict_res, nsub, nsim) { |
|
671 | 517x |
assert_list(predict_res) |
672 | 517x |
assert_count(nsub, positive = TRUE) |
673 | 516x |
assert_count(nsim, positive = TRUE) |
674 | ||
675 | 515x |
ret <- matrix( |
676 | 515x |
predict_res$prediction[, "fit"], |
677 | 515x |
ncol = nsim, |
678 | 515x |
nrow = nrow(predict_res$prediction) |
679 |
) |
|
680 | 515x |
for (i in seq_len(nsub)) { |
681 |
# Skip subjects which are not included in predict_res. |
|
682 | 82699x |
if (length(predict_res$index[[i]]) > 0) { |
683 |
# Obtain indices of data.frame belonging to subject i |
|
684 |
# (increment by 1, since indices from cpp are 0-order). |
|
685 | 66631x |
inds <- predict_res$index[[i]] + 1 |
686 | 66631x |
obs <- length(inds) |
687 | ||
688 |
# Get relevant covariance matrix for subject i. |
|
689 | 66631x |
covmat_i <- predict_res$covariance[[i]] |
690 | 66631x |
theta_chol <- t(chol(covmat_i)) |
691 | ||
692 |
# Simulate epsilon from covariance matrix. |
|
693 | 66631x |
mus <- ret[inds, , drop = FALSE] |
694 | 66631x |
epsilons <- theta_chol %*% matrix(stats::rnorm(nsim * obs), ncol = nsim) |
695 | 66631x |
ret[inds, ] <- mus + epsilons |
696 |
} |
|
697 |
} |
|
698 | ||
699 | 515x |
ret |
700 |
} |
1 |
#' Processing the Formula for `TMB` Fit |
|
2 |
#' |
|
3 |
#' @param formula (`formula`)\cr Original formula. |
|
4 |
#' @param covariance (`cov_struct`)\cr A covariance structure from which |
|
5 |
#' additional formula parts should be added. |
|
6 |
#' |
|
7 |
#' @return List of class `mmrm_tmb_formula_parts` with elements: |
|
8 |
#' |
|
9 |
#' - `formula`: the original input. |
|
10 |
#' - `model_formula`: `formula` with the covariance term is removed. |
|
11 |
#' - `model_formula`: `formula` with the covariance term removed. |
|
12 |
#' - `full_formula`: same as `model_formula` but includes the covariance |
|
13 |
#' structure's subject, visit and (optionally) group variables. |
|
14 |
#' - `cov_type`: `string` with covariance term type (e.g. `"us"`). |
|
15 |
#' - `is_spatial`: `flag` indicator of whether the covariance structure is |
|
16 |
#' spatial |
|
17 |
#' - `visit_var`: `character` with the visit variable name. |
|
18 |
#' - `subject_var`: `string` with the subject variable name. |
|
19 |
#' - `group_var`: `string` with the group variable name. If no group specified, |
|
20 |
#' this element is `NULL`. |
|
21 |
#' - `model_var`: `character` with the variables names of the formula, except `subject_var`. |
|
22 |
#' |
|
23 |
#' @keywords internal |
|
24 |
h_mmrm_tmb_formula_parts <- function( |
|
25 |
formula, |
|
26 |
covariance = as.cov_struct(formula, warn_partial = FALSE)) { |
|
27 | 270x |
assert_formula(formula) |
28 | 270x |
assert_true(identical(length(formula), 3L)) |
29 | ||
30 | 270x |
model_formula <- h_drop_covariance_terms(formula) |
31 | ||
32 | 270x |
structure( |
33 | 270x |
list( |
34 | 270x |
formula = formula, |
35 | 270x |
model_formula = model_formula, |
36 | 270x |
full_formula = h_add_covariance_terms(model_formula, covariance), |
37 | 270x |
cov_type = tmb_cov_type(covariance), |
38 | 270x |
is_spatial = covariance$type == "sp_exp", |
39 | 270x |
visit_var = covariance$visits, |
40 | 270x |
subject_var = covariance$subject, |
41 | 270x |
group_var = if (length(covariance$group) < 1) NULL else covariance$group, |
42 | 270x |
model_var = setdiff(all.vars(formula[[3]]), covariance$subject) |
43 |
), |
|
44 | 270x |
class = "mmrm_tmb_formula_parts" |
45 |
) |
|
46 |
} |
|
47 | ||
48 |
#' Data for `TMB` Fit |
|
49 |
#' |
|
50 |
#' @param formula_parts (`mmrm_tmb_formula_parts`)\cr list with formula parts |
|
51 |
#' from [h_mmrm_tmb_formula_parts()]. |
|
52 |
#' @param data (`data.frame`)\cr which contains variables used in `formula_parts`. |
|
53 |
#' @param weights (`vector`)\cr weights to be used in the fitting process. |
|
54 |
#' @param reml (`flag`)\cr whether restricted maximum likelihood (REML) estimation is used, |
|
55 |
#' otherwise maximum likelihood (ML) is used. |
|
56 |
#' @param singular (`string`)\cr choices of method deal with rank-deficient matrices. "error" to |
|
57 |
#' stop the function return the error, "drop" to drop these columns, and "keep" to keep all the columns. |
|
58 |
#' @param drop_visit_levels (`flag`)\cr whether to drop levels for visit variable, if visit variable is a factor. |
|
59 |
#' @param allow_na_response (`flag`)\cr whether NA in response is allowed. |
|
60 |
#' @param drop_levels (`flag`)\cr whether drop levels for covariates. If not dropped could lead to singular matrix. |
|
61 |
#' |
|
62 |
#' @return List of class `mmrm_tmb_data` with elements: |
|
63 |
#' - `full_frame`: `data.frame` with `n` rows containing all variables needed in the model. |
|
64 |
#' - `data`: `data.frame` of input dataset. |
|
65 |
#' - `x_matrix`: `matrix` with `n` rows and `p` columns specifying the overall design matrix. |
|
66 |
#' - `x_cols_aliased`: `logical` with potentially more than `p` elements indicating which |
|
67 |
#' columns in the original design matrix have been left out to obtain a full rank |
|
68 |
#' `x_matrix`. |
|
69 |
#' - `y_vector`: length `n` `numeric` specifying the overall response vector. |
|
70 |
#' - `weights_vector`: length `n` `numeric` specifying the weights vector. |
|
71 |
#' - `n_visits`: `int` with the number of visits, which is the dimension of the |
|
72 |
#' covariance matrix. |
|
73 |
#' - `n_subjects`: `int` with the number of subjects. |
|
74 |
#' - `subject_zero_inds`: length `n_subjects` `integer` containing the zero-based start |
|
75 |
#' indices for each subject. |
|
76 |
#' - `subject_n_visits`: length `n_subjects` `integer` containing the number of |
|
77 |
#' observed visits for each subjects. So the sum of this vector equals `n`. |
|
78 |
#' - `cov_type`: `string` value specifying the covariance type. |
|
79 |
#' - `is_spatial_int`: `int` specifying whether the covariance structure is spatial(1) or not(0). |
|
80 |
#' - `reml`: `int` specifying whether REML estimation is used (1), otherwise ML (0). |
|
81 |
#' - `subject_groups`: `factor` specifying the grouping for each subject. |
|
82 |
#' - `n_groups`: `int` with the number of total groups |
|
83 |
#' |
|
84 |
#' @details Note that the `subject_var` must not be factor but can also be character. |
|
85 |
#' If it is character, then it will be converted to factor internally. Here |
|
86 |
#' the levels will be the unique values, sorted alphabetically and numerically if there |
|
87 |
#' is a common string prefix of numbers in the character elements. For full control |
|
88 |
#' on the order please use a factor. |
|
89 |
#' |
|
90 |
#' @keywords internal |
|
91 |
h_mmrm_tmb_data <- function(formula_parts, |
|
92 |
data, |
|
93 |
weights, |
|
94 |
reml, |
|
95 |
singular = c("drop", "error", "keep"), |
|
96 |
drop_visit_levels, |
|
97 |
allow_na_response = FALSE, |
|
98 |
drop_levels = TRUE, |
|
99 |
xlev = NULL, |
|
100 |
contrasts = NULL) { |
|
101 | 312x |
assert_class(formula_parts, "mmrm_tmb_formula_parts") |
102 | 312x |
assert_data_frame(data) |
103 | 312x |
varname <- formula_parts[grepl("_var", names(formula_parts))] |
104 | 312x |
assert_names( |
105 | 312x |
names(data), |
106 | 312x |
must.include = unlist(varname, use.names = FALSE) |
107 |
) |
|
108 | 312x |
assert_true(is.factor(data[[formula_parts$subject_var]]) || is.character(data[[formula_parts$subject_var]])) |
109 | 312x |
assert_numeric(weights, len = nrow(data)) |
110 | 312x |
assert_flag(reml) |
111 | 312x |
singular <- match.arg(singular) |
112 | 312x |
assert_flag(drop_visit_levels) |
113 | ||
114 | 312x |
if (is.character(data[[formula_parts$subject_var]])) { |
115 | 5x |
data[[formula_parts$subject_var]] <- factor( |
116 | 5x |
data[[formula_parts$subject_var]], |
117 | 5x |
levels = stringr::str_sort(unique(data[[formula_parts$subject_var]]), numeric = TRUE) |
118 |
) |
|
119 |
} |
|
120 | 312x |
data_order <- if (formula_parts$is_spatial) { |
121 | 16x |
order(data[[formula_parts$subject_var]]) |
122 |
} else { |
|
123 | 296x |
subject_visit_data <- data[, c(formula_parts$subject_var, formula_parts$visit_var)] |
124 | 296x |
is_duplicated <- duplicated(subject_visit_data) |
125 | 296x |
if (any(is_duplicated)) { |
126 | 1x |
stop( |
127 | 1x |
"time points have to be unique for each subject, detected following duplicates in data:\n", |
128 | 1x |
paste(utils::capture.output(print(subject_visit_data[is_duplicated, ])), collapse = "\n") |
129 |
) |
|
130 |
} |
|
131 | 295x |
order(data[[formula_parts$subject_var]], data[[formula_parts$visit_var]]) |
132 |
} |
|
133 | 311x |
if (identical(formula_parts$is_spatial, FALSE)) { |
134 | 295x |
h_confirm_large_levels(length(levels(data[[formula_parts$visit_var]]))) |
135 |
} |
|
136 | 310x |
data <- data[data_order, ] |
137 | 310x |
weights <- weights[data_order] |
138 | 310x |
data <- data.frame(data, weights) |
139 |
# Weights is always the last column. |
|
140 | 310x |
weights_name <- colnames(data)[ncol(data)] |
141 |
# If `y` is allowed to be NA, then first replace y with 1:n, then replace it with original y. |
|
142 | 310x |
if (!allow_na_response) { |
143 | 260x |
h_warn_na_action() |
144 |
} |
|
145 | 310x |
full_frame <- eval( |
146 | 310x |
bquote(stats::model.frame( |
147 | 310x |
formula_parts$full_formula, |
148 | 310x |
data = data, |
149 | 310x |
weights = .(as.symbol(weights_name)), |
150 | 310x |
na.action = "na.pass", |
151 | 310x |
xlev = xlev |
152 |
)) |
|
153 |
) |
|
154 | 310x |
if (drop_levels) { |
155 | 262x |
full_frame <- h_drop_levels(full_frame, formula_parts$subject_var, formula_parts$visit_var, names(xlev)) |
156 |
} |
|
157 | 310x |
has_response <- !identical(attr(attr(full_frame, "terms"), "response"), 0L) |
158 | 310x |
keep_ind <- if (allow_na_response && has_response) { |
159 |
# Note that response is always the first column if there is response. |
|
160 | 50x |
stats::complete.cases(full_frame[, -1L, drop = FALSE]) |
161 |
} else { |
|
162 | 260x |
stats::complete.cases(full_frame) |
163 |
} |
|
164 | 310x |
full_frame <- full_frame[keep_ind, ] |
165 | 310x |
if (drop_visit_levels && !formula_parts$is_spatial && h_extra_levels(full_frame[[formula_parts$visit_var]])) { |
166 | 3x |
visit_vec <- full_frame[[formula_parts$visit_var]] |
167 | 3x |
old_levels <- levels(visit_vec) |
168 | 3x |
full_frame[[formula_parts$visit_var]] <- droplevels(visit_vec) |
169 | 3x |
new_levels <- levels(full_frame[[formula_parts$visit_var]]) |
170 | 3x |
dropped <- setdiff(old_levels, new_levels) |
171 | 3x |
message( |
172 | 3x |
"In ", formula_parts$visit_var, " there are dropped visits: ", toString(dropped), |
173 | 3x |
".\n Additional attributes including contrasts are lost.\n", |
174 | 3x |
"To avoid this behavior, make sure use `drop_visit_levels = FALSE`." |
175 |
) |
|
176 |
} |
|
177 | 310x |
is_factor_col <- vapply(full_frame, is.factor, FUN.VALUE = TRUE) |
178 | 310x |
is_factor_col <- intersect(names(is_factor_col)[is_factor_col], all.vars(formula_parts$model_formula)) |
179 | 310x |
x_matrix <- stats::model.matrix( |
180 | 310x |
formula_parts$model_formula, |
181 | 310x |
data = full_frame, |
182 | 310x |
contrasts.arg = h_default_value(contrasts, lapply(full_frame[is_factor_col], contrasts)) |
183 |
) |
|
184 | 309x |
x_cols_aliased <- stats::setNames(rep(FALSE, ncol(x_matrix)), nm = colnames(x_matrix)) |
185 | 309x |
qr_x_mat <- qr(x_matrix) |
186 | 309x |
if (qr_x_mat$rank < ncol(x_matrix)) { |
187 | 23x |
cols_to_drop <- utils::tail(qr_x_mat$pivot, ncol(x_matrix) - qr_x_mat$rank) |
188 | 23x |
if (identical(singular, "error")) { |
189 | 1x |
stop( |
190 | 1x |
"design matrix only has rank ", qr_x_mat$rank, " and ", length(cols_to_drop), |
191 | 1x |
" columns (", toString(colnames(x_matrix)[cols_to_drop]), ") could be dropped", |
192 | 1x |
" to achieve full rank ", ncol(x_matrix), " by using `accept_singular = TRUE`" |
193 |
) |
|
194 | 22x |
} else if (identical(singular, "drop")) { |
195 | 11x |
assign_attr <- attr(x_matrix, "assign") |
196 | 11x |
contrasts_attr <- attr(x_matrix, "contrasts") |
197 | 11x |
x_matrix <- x_matrix[, -cols_to_drop, drop = FALSE] |
198 | 11x |
x_cols_aliased[cols_to_drop] <- TRUE |
199 | 11x |
attr(x_matrix, "assign") <- assign_attr[-cols_to_drop] |
200 | 11x |
attr(x_matrix, "contrasts") <- contrasts_attr |
201 |
} |
|
202 |
} |
|
203 | 308x |
y_vector <- if (has_response) { |
204 | 308x |
as.numeric(stats::model.response(full_frame)) |
205 |
} else { |
|
206 | ! |
rep(NA_real_, nrow(full_frame)) |
207 |
} |
|
208 | 308x |
weights_vector <- as.numeric(stats::model.weights(full_frame)) |
209 | 308x |
n_subjects <- length(unique(full_frame[[formula_parts$subject_var]])) |
210 | 308x |
subject_zero_inds <- which(!duplicated(full_frame[[formula_parts$subject_var]])) - 1L |
211 | 308x |
subject_n_visits <- c(utils::tail(subject_zero_inds, -1L), nrow(full_frame)) - subject_zero_inds |
212 |
# It is possible that `subject_var` is factor with more levels (and this does not affect fit) |
|
213 |
# so no check is needed for `subject_visits`. |
|
214 | 308x |
assert_true(all(subject_n_visits > 0)) |
215 | 308x |
if (!is.null(formula_parts$group_var)) { |
216 | 41x |
assert_factor(data[[formula_parts$group_var]]) |
217 | 41x |
subject_groups <- full_frame[[formula_parts$group_var]][subject_zero_inds + 1L] |
218 | 41x |
n_groups <- nlevels(subject_groups) |
219 |
} else { |
|
220 | 267x |
subject_groups <- factor(rep(0L, n_subjects)) |
221 | 267x |
n_groups <- 1L |
222 |
} |
|
223 | 308x |
coordinates <- full_frame[, formula_parts$visit_var, drop = FALSE] |
224 | 308x |
if (formula_parts$is_spatial) { |
225 | 16x |
lapply(coordinates, assert_numeric) |
226 | 16x |
coordinates_matrix <- as.matrix(coordinates) |
227 | 16x |
n_visits <- max(subject_n_visits) |
228 |
} else { |
|
229 | 292x |
assert(identical(ncol(coordinates), 1L)) |
230 | 292x |
assert_factor(coordinates[[1L]]) |
231 | 292x |
coordinates_matrix <- as.matrix(as.integer(coordinates[[1L]]) - 1, ncol = 1) |
232 | 292x |
n_visits <- nlevels(coordinates[[1L]]) |
233 | 292x |
assert_true(all(subject_n_visits <= n_visits)) |
234 |
} |
|
235 | 308x |
structure( |
236 | 308x |
list( |
237 | 308x |
full_frame = full_frame, |
238 | 308x |
data = data, |
239 | 308x |
x_matrix = x_matrix, |
240 | 308x |
x_cols_aliased = x_cols_aliased, |
241 | 308x |
coordinates = coordinates_matrix, |
242 | 308x |
y_vector = y_vector, |
243 | 308x |
weights_vector = weights_vector, |
244 | 308x |
n_visits = n_visits, |
245 | 308x |
n_subjects = n_subjects, |
246 | 308x |
subject_zero_inds = subject_zero_inds, |
247 | 308x |
subject_n_visits = subject_n_visits, |
248 | 308x |
cov_type = formula_parts$cov_type, |
249 | 308x |
is_spatial_int = as.integer(formula_parts$is_spatial), |
250 | 308x |
reml = as.integer(reml), |
251 | 308x |
subject_groups = subject_groups, |
252 | 308x |
n_groups = n_groups |
253 |
), |
|
254 | 308x |
class = "mmrm_tmb_data" |
255 |
) |
|
256 |
} |
|
257 | ||
258 |
#' Start Parameters for `TMB` Fit |
|
259 |
#' |
|
260 |
#' @param formula_parts (`mmrm_tmb_formula_parts`)\cr produced by |
|
261 |
#' [h_mmrm_tmb_formula_parts()]. |
|
262 |
#' @param tmb_data (`mmrm_tmb_data`)\cr produced by [h_mmrm_tmb_data()]. |
|
263 |
#' @param start (`numeric` or `NULL`)\cr optional start values for variance |
|
264 |
#' parameters. |
|
265 |
#' @param n_groups (`int`)\cr number of groups. |
|
266 |
#' @return List with element `theta` containing the start values for the variance |
|
267 |
#' parameters. |
|
268 |
#' |
|
269 |
#' @keywords internal |
|
270 |
h_mmrm_tmb_parameters <- function(formula_parts, |
|
271 |
tmb_data, |
|
272 |
start, |
|
273 |
n_groups = 1L) { |
|
274 | 265x |
assert_class(formula_parts, "mmrm_tmb_formula_parts") |
275 | 265x |
assert_class(tmb_data, "mmrm_tmb_data") |
276 | ||
277 | 265x |
m <- tmb_data$n_visits |
278 | 265x |
start_value0 <- std_start(formula_parts$cov_type, m, n_groups) |
279 | 265x |
theta_dim <- length(start_value0) |
280 | 265x |
start_values <- if (is.null(start)) { |
281 | 15x |
start_value0 |
282 | 265x |
} else if (test_function(start)) { |
283 | 233x |
do.call(start, utils::modifyList(formula_parts, tmb_data)) |
284 |
} else { |
|
285 | 17x |
start |
286 |
} |
|
287 | 264x |
assert_numeric(start_values, len = theta_dim, any.missing = FALSE, finite = TRUE) |
288 | 262x |
list(theta = start_values) |
289 |
} |
|
290 | ||
291 |
#' Asserting Sane Start Values for `TMB` Fit |
|
292 |
#' |
|
293 |
#' @param tmb_object (`list`)\cr created with [TMB::MakeADFun()]. |
|
294 |
#' |
|
295 |
#' @return Nothing, only used for assertions. |
|
296 |
#' |
|
297 |
#' @keywords internal |
|
298 |
h_mmrm_tmb_assert_start <- function(tmb_object) { |
|
299 | 249x |
assert_list(tmb_object) |
300 | 249x |
assert_subset(c("fn", "gr", "par"), names(tmb_object)) |
301 | ||
302 | 249x |
if (is.na(tmb_object$fn(tmb_object$par))) { |
303 | 1x |
stop("negative log-likelihood is NaN at starting parameter values") |
304 |
} |
|
305 | 248x |
if (any(is.na(tmb_object$gr(tmb_object$par)))) { |
306 | 1x |
stop("some elements of gradient are NaN at starting parameter values") |
307 |
} |
|
308 |
} |
|
309 | ||
310 |
#' Checking the `TMB` Optimization Result |
|
311 |
#' |
|
312 |
#' @param tmb_opt (`list`)\cr optimization result. |
|
313 |
#' @param mmrm_tmb (`mmrm_tmb`)\cr result from [h_mmrm_tmb_fit()]. |
|
314 |
#' |
|
315 |
#' @return Nothing, only used to generate warnings in case that the model |
|
316 |
#' did not converge. |
|
317 |
#' |
|
318 |
#' @keywords internal |
|
319 |
h_mmrm_tmb_check_conv <- function(tmb_opt, mmrm_tmb) { |
|
320 | 245x |
assert_list(tmb_opt) |
321 | 245x |
assert_subset(c("par", "objective", "convergence", "message"), names(tmb_opt)) |
322 | 245x |
assert_class(mmrm_tmb, "mmrm_tmb") |
323 | ||
324 | 245x |
if (!is.null(tmb_opt$convergence) && tmb_opt$convergence != 0) { |
325 | 3x |
warning("Model convergence problem: ", tmb_opt$message, ".") |
326 | 3x |
return() |
327 |
} |
|
328 | 242x |
theta_vcov <- mmrm_tmb$theta_vcov |
329 | 242x |
if (is(theta_vcov, "try-error")) { |
330 | 3x |
warning("Model convergence problem: hessian is singular, theta_vcov not available.") |
331 | 3x |
return() |
332 |
} |
|
333 | 239x |
if (!all(is.finite(theta_vcov))) { |
334 | 3x |
warning("Model convergence problem: theta_vcov contains non-finite values.") |
335 | 3x |
return() |
336 |
} |
|
337 | 236x |
eigen_vals <- eigen(theta_vcov, only.values = TRUE)$values |
338 | 236x |
if (mode(eigen_vals) == "complex" || any(eigen_vals <= 0)) { |
339 |
# Note: complex eigen values signal that the matrix is not symmetric, therefore not positive definite. |
|
340 | 3x |
warning("Model convergence problem: theta_vcov is not positive definite.") |
341 | 3x |
return() |
342 |
} |
|
343 | 233x |
qr_rank <- qr(theta_vcov)$rank |
344 | 233x |
if (qr_rank < ncol(theta_vcov)) { |
345 | 1x |
warning("Model convergence problem: theta_vcov is numerically singular.") |
346 |
} |
|
347 |
} |
|
348 | ||
349 |
#' Extract covariance matrix from `TMB` report and input data |
|
350 |
#' |
|
351 |
#' This helper does some simple post-processing to extract covariance matrix or named |
|
352 |
#' list of covariance matrices if the fitting is using grouped covariance matrices. |
|
353 |
#' |
|
354 |
#' @param tmb_report (`list`)\cr report created with [TMB::MakeADFun()] report function. |
|
355 |
#' @param tmb_data (`mmrm_tmb_data`)\cr produced by [h_mmrm_tmb_data()]. |
|
356 |
#' @param visit_var (`character`)\cr character vector of the visit variable |
|
357 |
#' @param is_spatial (`flag`)\cr indicator whether the covariance structure is spatial. |
|
358 |
#' @return Return a simple covariance matrix if there is no grouping, or a named |
|
359 |
#' list of estimated grouped covariance matrices, |
|
360 |
#' with its name equal to the group levels. |
|
361 |
#' |
|
362 |
#' @keywords internal |
|
363 |
h_mmrm_tmb_extract_cov <- function(tmb_report, tmb_data, visit_var, is_spatial) { |
|
364 | 241x |
d <- dim(tmb_report$covariance_lower_chol) |
365 | 241x |
visit_names <- if (!is_spatial) { |
366 | 228x |
levels(tmb_data$full_frame[[visit_var]]) |
367 |
} else { |
|
368 | 13x |
c(0, 1) |
369 |
} |
|
370 | 241x |
cov <- lapply( |
371 | 241x |
seq_len(d[1] / d[2]), |
372 | 241x |
function(i) { |
373 | 278x |
ret <- tcrossprod(tmb_report$covariance_lower_chol[seq(1 + (i - 1) * d[2], i * d[2]), ]) |
374 | 278x |
dimnames(ret) <- list(visit_names, visit_names) |
375 | 278x |
return(ret) |
376 |
} |
|
377 |
) |
|
378 | 241x |
if (identical(tmb_data$n_groups, 1L)) { |
379 | 204x |
cov <- cov[[1]] |
380 |
} else { |
|
381 | 37x |
names(cov) <- levels(tmb_data$subject_groups) |
382 |
} |
|
383 | 241x |
return(cov) |
384 |
} |
|
385 | ||
386 |
#' Build `TMB` Fit Result List |
|
387 |
#' |
|
388 |
#' This helper does some simple post-processing of the `TMB` object and |
|
389 |
#' optimization results, including setting names, inverting matrices etc. |
|
390 |
#' |
|
391 |
#' @param tmb_object (`list`)\cr created with [TMB::MakeADFun()]. |
|
392 |
#' @param tmb_opt (`list`)\cr optimization result. |
|
393 |
#' @param formula_parts (`mmrm_tmb_formula_parts`)\cr produced by |
|
394 |
#' [h_mmrm_tmb_formula_parts()]. |
|
395 |
#' @param tmb_data (`mmrm_tmb_data`)\cr produced by [h_mmrm_tmb_data()]. |
|
396 |
#' |
|
397 |
#' @return List of class `mmrm_tmb` with: |
|
398 |
#' - `cov`: estimated covariance matrix, or named list of estimated group specific covariance matrices. |
|
399 |
#' - `beta_est`: vector of coefficient estimates. |
|
400 |
#' - `beta_vcov`: Variance-covariance matrix for coefficient estimates. |
|
401 |
#' - `beta_vcov_inv_L`: Lower triangular matrix `L` of the inverse variance-covariance matrix decomposition. |
|
402 |
#' - `beta_vcov_inv_D`: vector of diagonal matrix `D` of the inverse variance-covariance matrix decomposition. |
|
403 |
#' - `theta_est`: vector of variance parameter estimates. |
|
404 |
#' - `theta_vcov`: variance-covariance matrix for variance parameter estimates. |
|
405 |
#' - `neg_log_lik`: obtained negative log-likelihood. |
|
406 |
#' - `formula_parts`: input. |
|
407 |
#' - `data`: input. |
|
408 |
#' - `weights`: input. |
|
409 |
#' - `reml`: input as a flag. |
|
410 |
#' - `opt_details`: list with optimization details including convergence code. |
|
411 |
#' - `tmb_object`: original `TMB` object created with [TMB::MakeADFun()]. |
|
412 |
#' - `tmb_data`: input. |
|
413 |
#' |
|
414 |
#' @details Instead of inverting or decomposing `beta_vcov`, it can be more efficient to use its robust |
|
415 |
#' Cholesky decomposition `LDL^T`, therefore we return the corresponding two components `L` and `D` |
|
416 |
#' as well since they have been available on the `C++` side already. |
|
417 |
#' |
|
418 |
#' @keywords internal |
|
419 |
h_mmrm_tmb_fit <- function(tmb_object, |
|
420 |
tmb_opt, |
|
421 |
formula_parts, |
|
422 |
tmb_data) { |
|
423 | 239x |
assert_list(tmb_object) |
424 | 239x |
assert_subset(c("fn", "gr", "par", "he"), names(tmb_object)) |
425 | 239x |
assert_list(tmb_opt) |
426 | 239x |
assert_subset(c("par", "objective", "convergence", "message"), names(tmb_opt)) |
427 | 239x |
assert_class(formula_parts, "mmrm_tmb_formula_parts") |
428 | 239x |
assert_class(tmb_data, "mmrm_tmb_data") |
429 | ||
430 | 239x |
tmb_report <- tmb_object$report(par = tmb_opt$par) |
431 | 239x |
x_matrix_cols <- colnames(tmb_data$x_matrix) |
432 | 239x |
cov <- h_mmrm_tmb_extract_cov(tmb_report, tmb_data, formula_parts$visit_var, formula_parts$is_spatial) |
433 | 239x |
beta_est <- tmb_report$beta |
434 | 239x |
names(beta_est) <- x_matrix_cols |
435 | 239x |
beta_vcov <- tmb_report$beta_vcov |
436 | 239x |
dimnames(beta_vcov) <- list(x_matrix_cols, x_matrix_cols) |
437 | 239x |
beta_vcov_inv_L <- tmb_report$XtWX_L # nolint |
438 | 239x |
beta_vcov_inv_D <- tmb_report$XtWX_D # nolint |
439 | 239x |
theta_est <- tmb_opt$par |
440 | 239x |
names(theta_est) <- NULL |
441 | 239x |
theta_vcov <- try(solve(tmb_object$he(tmb_opt$par)), silent = TRUE) |
442 | 239x |
opt_details_names <- setdiff( |
443 | 239x |
names(tmb_opt), |
444 | 239x |
c("par", "objective") |
445 |
) |
|
446 | 239x |
structure( |
447 | 239x |
list( |
448 | 239x |
cov = cov, |
449 | 239x |
beta_est = beta_est, |
450 | 239x |
beta_vcov = beta_vcov, |
451 | 239x |
beta_vcov_inv_L = beta_vcov_inv_L, |
452 | 239x |
beta_vcov_inv_D = beta_vcov_inv_D, |
453 | 239x |
theta_est = theta_est, |
454 | 239x |
theta_vcov = theta_vcov, |
455 | 239x |
neg_log_lik = tmb_opt$objective, |
456 | 239x |
formula_parts = formula_parts, |
457 | 239x |
data = tmb_data$data, |
458 | 239x |
weights = tmb_data$weights_vector, |
459 | 239x |
reml = as.logical(tmb_data$reml), |
460 | 239x |
opt_details = tmb_opt[opt_details_names], |
461 | 239x |
tmb_object = tmb_object, |
462 | 239x |
tmb_data = tmb_data |
463 |
), |
|
464 | 239x |
class = "mmrm_tmb" |
465 |
) |
|
466 |
} |
|
467 | ||
468 |
#' Low-Level Fitting Function for MMRM |
|
469 |
#' |
|
470 |
#' @description `r lifecycle::badge("stable")` |
|
471 |
#' |
|
472 |
#' This is the low-level function to fit an MMRM. Note that this does not |
|
473 |
#' try different optimizers or adds Jacobian information etc. in contrast to |
|
474 |
#' [mmrm()]. |
|
475 |
#' |
|
476 |
#' @param formula (`formula`)\cr model formula with exactly one special term |
|
477 |
#' specifying the visits within subjects, see details. |
|
478 |
#' @param data (`data.frame`)\cr input data containing the variables used in |
|
479 |
#' `formula`. |
|
480 |
#' @param weights (`vector`)\cr input vector containing the weights. |
|
481 |
#' @inheritParams h_mmrm_tmb_data |
|
482 |
#' @param covariance (`cov_struct`)\cr A covariance structure type definition, |
|
483 |
#' or value that can be coerced to a covariance structure using |
|
484 |
#' [as.cov_struct()]. If no value is provided, a structure is derived from |
|
485 |
#' the provided formula. |
|
486 |
#' @param control (`mmrm_control`)\cr list of control options produced by |
|
487 |
#' [mmrm_control()]. |
|
488 |
#' @inheritParams fit_single_optimizer |
|
489 |
#' |
|
490 |
#' @return List of class `mmrm_tmb`, see [h_mmrm_tmb_fit()] for details. |
|
491 |
#' In addition, it contains elements `call` and `optimizer`. |
|
492 |
#' |
|
493 |
#' @details |
|
494 |
#' The `formula` typically looks like: |
|
495 |
#' |
|
496 |
#' `FEV1 ~ RACE + SEX + ARMCD * AVISIT + us(AVISIT | USUBJID)` |
|
497 |
#' |
|
498 |
#' which specifies response and covariates as usual, and exactly one special term |
|
499 |
#' defines which covariance structure is used and what are the visit and |
|
500 |
#' subject variables. |
|
501 |
#' |
|
502 |
#' Always use only the first optimizer if multiple optimizers are provided. |
|
503 |
#' |
|
504 |
#' @export |
|
505 |
#' |
|
506 |
#' @examples |
|
507 |
#' formula <- FEV1 ~ RACE + SEX + ARMCD * AVISIT + us(AVISIT | USUBJID) |
|
508 |
#' data <- fev_data |
|
509 |
#' system.time(result <- fit_mmrm(formula, data, rep(1, nrow(fev_data)))) |
|
510 |
fit_mmrm <- function(formula, |
|
511 |
data, |
|
512 |
weights, |
|
513 |
reml = TRUE, |
|
514 |
covariance = NULL, |
|
515 |
tmb_data, |
|
516 |
formula_parts, |
|
517 |
control = mmrm_control()) { |
|
518 | 252x |
if (missing(formula_parts) || missing(tmb_data)) { |
519 | 67x |
covariance <- h_reconcile_cov_struct(formula, covariance) |
520 | 65x |
formula_parts <- h_mmrm_tmb_formula_parts(formula, covariance) |
521 | ||
522 | 65x |
if (!formula_parts$is_spatial && !is.factor(data[[formula_parts$visit_var]])) { |
523 | 1x |
stop("Time variable must be a factor for non-spatial covariance structures") |
524 |
} |
|
525 | ||
526 | 64x |
assert_class(control, "mmrm_control") |
527 | 64x |
assert_list(control$optimizers, min.len = 1) |
528 | 64x |
assert_numeric(weights, any.missing = FALSE) |
529 | 64x |
assert_true(all(weights > 0)) |
530 | 64x |
tmb_data <- h_mmrm_tmb_data( |
531 | 64x |
formula_parts, data, weights, reml, |
532 | 64x |
singular = if (control$accept_singular) "drop" else "error", drop_visit_levels = control$drop_visit_levels |
533 |
) |
|
534 |
} else { |
|
535 | 185x |
assert_class(tmb_data, "mmrm_tmb_data") |
536 | 185x |
assert_class(formula_parts, "mmrm_tmb_formula_parts") |
537 |
} |
|
538 | 249x |
tmb_parameters <- h_mmrm_tmb_parameters(formula_parts, tmb_data, start = control$start, n_groups = tmb_data$n_groups) |
539 | ||
540 | 246x |
tmb_object <- TMB::MakeADFun( |
541 | 246x |
data = tmb_data, |
542 | 246x |
parameters = tmb_parameters, |
543 | 246x |
hessian = TRUE, |
544 | 246x |
DLL = "mmrm", |
545 | 246x |
silent = TRUE |
546 |
) |
|
547 | 246x |
h_mmrm_tmb_assert_start(tmb_object) |
548 | 246x |
used_optimizer <- control$optimizers[[1L]] |
549 | 246x |
used_optimizer_name <- names(control$optimizers)[1L] |
550 | 246x |
args <- with( |
551 | 246x |
tmb_object, |
552 | 246x |
c( |
553 | 246x |
list(par, fn, gr), |
554 | 246x |
attr(used_optimizer, "args") |
555 |
) |
|
556 |
) |
|
557 | 246x |
if (identical(attr(used_optimizer, "use_hessian"), TRUE)) { |
558 | 8x |
args$hessian <- tmb_object$he |
559 |
} |
|
560 | 246x |
tmb_opt <- do.call( |
561 | 246x |
what = used_optimizer, |
562 | 246x |
args = args |
563 |
) |
|
564 |
# Ensure negative log likelihood is stored in `objective` element of list. |
|
565 | 237x |
if ("value" %in% names(tmb_opt)) { |
566 | 227x |
tmb_opt$objective <- tmb_opt$value |
567 | 227x |
tmb_opt$value <- NULL |
568 |
} |
|
569 | 237x |
fit <- h_mmrm_tmb_fit(tmb_object, tmb_opt, formula_parts, tmb_data) |
570 | 237x |
h_mmrm_tmb_check_conv(tmb_opt, fit) |
571 | 237x |
fit$call <- match.call() |
572 | 237x |
fit$call$formula <- formula_parts$formula |
573 | 237x |
fit$optimizer <- used_optimizer_name |
574 | 237x |
fit |
575 |
} |
1 |
#' Dynamic Registration for Package Interoperability |
|
2 |
#' |
|
3 |
#' @seealso See `vignette("xtending", package = "emmeans")` for background. |
|
4 |
#' @keywords internal |
|
5 |
#' @noRd |
|
6 |
.onLoad <- function(libname, pkgname) { # nolint |
|
7 | ! |
if (utils::packageVersion("TMB") < "1.9.15") { |
8 | ! |
warning("TMB version 1.9.15 or higher is required for reproducible model fits", call. = FALSE) |
9 |
} |
|
10 | ||
11 | ! |
register_on_load( |
12 | ! |
"emmeans", c("1.6", NA), |
13 | ! |
callback = function() emmeans::.emm_register("mmrm", pkgname), |
14 | ! |
message = "mmrm() registered as emmeans extension" |
15 |
) |
|
16 | ||
17 | ! |
register_on_load( |
18 | ! |
"parsnip", c("1.1.0", NA), |
19 | ! |
callback = parsnip_add_mmrm, |
20 | ! |
message = emit_tidymodels_register_msg |
21 |
) |
|
22 | ! |
register_on_load( |
23 | ! |
"car", c("3.1.2", NA), |
24 | ! |
callback = car_add_mmrm, |
25 | ! |
message = "mmrm() registered as car::Anova extension" |
26 |
) |
|
27 |
} |
|
28 | ||
29 |
#' Helper Function for Registering Functionality With Suggests Packages |
|
30 |
#' |
|
31 |
#' @inheritParams check_package_version |
|
32 |
#' |
|
33 |
#' @param callback (`function(...) ANY`)\cr a callback to execute upon package |
|
34 |
#' load. Note that no arguments are passed to this function. Any necessary |
|
35 |
#' data must be provided upon construction. |
|
36 |
#' |
|
37 |
#' @param message (`NULL` or `string`)\cr an optional message to print after |
|
38 |
#' the callback is executed upon successful registration. |
|
39 |
#' |
|
40 |
#' @return A logical (invisibly) indicating whether registration was successful. |
|
41 |
#' If not, a onLoad hook was set for the next time the package is loaded. |
|
42 |
#' |
|
43 |
#' @keywords internal |
|
44 |
register_on_load <- function(pkg, |
|
45 |
ver = c(NA_character_, NA_character_), |
|
46 |
callback, |
|
47 |
message = NULL) { |
|
48 | 4x |
if (isNamespaceLoaded(pkg) && check_package_version(pkg, ver)) { |
49 | 3x |
callback() |
50 | 2x |
if (is.character(message)) packageStartupMessage(message) |
51 | 1x |
if (is.function(message)) packageStartupMessage(message()) |
52 | 3x |
return(invisible(TRUE)) |
53 |
} |
|
54 | ||
55 | 1x |
setHook( |
56 | 1x |
packageEvent(pkg, event = "onLoad"), |
57 | 1x |
action = "append", |
58 | 1x |
function(...) { |
59 | ! |
register_on_load( |
60 | ! |
pkg = pkg, |
61 | ! |
ver = ver, |
62 | ! |
callback = callback, |
63 | ! |
message = message |
64 |
) |
|
65 |
} |
|
66 |
) |
|
67 | ||
68 | 1x |
invisible(FALSE) |
69 |
} |
|
70 | ||
71 |
#' Check Suggested Dependency Against Version Requirements |
|
72 |
#' |
|
73 |
#' @param pkg (`string`)\cr package name. |
|
74 |
#' @param ver (`character`)\cr of length 2 whose elements can be provided to |
|
75 |
#' [numeric_version()], representing a minimum and maximum (inclusive) version |
|
76 |
#' requirement for interoperability. When `NA`, no version requirement is |
|
77 |
#' imposed. Defaults to no version requirement. |
|
78 |
#' |
|
79 |
#' @return A logical (invisibly) indicating whether the loaded package meets |
|
80 |
#' the version requirements. A warning is emitted otherwise. |
|
81 |
#' |
|
82 |
#' @keywords internal |
|
83 |
check_package_version <- function(pkg, ver = c(NA_character_, NA_character_)) { |
|
84 | 7x |
assert_character(ver, len = 2L) |
85 | 6x |
pkg_ver <- utils::packageVersion(pkg) |
86 | 6x |
ver <- numeric_version(ver, strict = FALSE) |
87 | ||
88 | 6x |
warn_version <- function(pkg, pkg_ver, ver) { |
89 | 2x |
ver_na <- is.na(ver) |
90 | 2x |
warning(sprintf( |
91 | 2x |
"Cannot register mmrm for use with %s (v%s). Version %s required.", |
92 | 2x |
pkg, pkg_ver, |
93 | 2x |
if (!any(ver_na)) { |
94 | ! |
sprintf("%s to %s", ver[1], ver[2]) |
95 | 2x |
} else if (ver_na[2]) { |
96 | 1x |
paste0(">= ", ver[1]) |
97 | 2x |
} else if (ver_na[1]) { |
98 | 1x |
paste0("<= ", ver[2]) |
99 |
} |
|
100 |
)) |
|
101 |
} |
|
102 | ||
103 | 6x |
if (identical(pkg_ver < ver[1], TRUE) || identical(pkg_ver > ver[2], TRUE)) { |
104 | 2x |
warn_version(pkg, pkg_ver, ver) |
105 | 2x |
return(invisible(FALSE)) |
106 |
} |
|
107 | ||
108 | 4x |
invisible(TRUE) |
109 |
} |
|
110 | ||
111 |
#' Format a Message to Emit When Tidymodels is Loaded |
|
112 |
#' |
|
113 |
#' @return A character message to emit. Either a ansi-formatted cli output if |
|
114 |
#' package 'cli' is available or a plain-text message otherwise. |
|
115 |
#' |
|
116 |
#' @keywords internal |
|
117 |
emit_tidymodels_register_msg <- function() { |
|
118 | 1x |
pkg <- utils::packageName() |
119 | 1x |
ver <- utils::packageVersion(pkg) |
120 | ||
121 | 1x |
if (isTRUE(getOption("tidymodels.quiet"))) { |
122 | ! |
return() |
123 |
} |
|
124 | ||
125 |
# if tidymodels is attached, cli packages come as a dependency |
|
126 | 1x |
has_cli <- requireNamespace("cli", quietly = TRUE) |
127 | 1x |
if (has_cli) { |
128 |
# unfortunately, cli does not expose many formatting tools for emitting |
|
129 |
# messages (only via conditions to stderr) which can't be suppressed using |
|
130 |
# suppressPackageStartupMessages() so formatting must be done adhoc, |
|
131 |
# similar to how it's done in {tidymodels} R/attach.R |
|
132 | 1x |
paste0( |
133 | 1x |
cli::rule( |
134 | 1x |
left = cli::style_bold("Model Registration"), |
135 | 1x |
right = paste(pkg, ver) |
136 |
), |
|
137 | 1x |
"\n", |
138 | 1x |
cli::col_green(cli::symbol$tick), " ", |
139 | 1x |
cli::col_blue("mmrm"), "::", cli::col_green("mmrm()") |
140 |
) |
|
141 |
} else { |
|
142 | ! |
paste0(pkg, "::mmrm() registered for use with tidymodels") |
143 |
} |
|
144 |
} |
1 |
#' Register `mmrm` For Use With `car::Anova` |
|
2 |
#' |
|
3 |
#' @inheritParams base::requireNamespace |
|
4 |
#' @return A logical value indicating whether registration was successful. |
|
5 |
#' |
|
6 |
#' @keywords internal |
|
7 |
car_add_mmrm <- function(quietly = FALSE) { |
|
8 | 1x |
if (!requireNamespace("car", quietly = quietly)) { |
9 | ! |
return(FALSE) |
10 |
} |
|
11 | 1x |
envir <- asNamespace("mmrm") |
12 | 1x |
h_register_s3("car", "Anova", "mmrm", envir) |
13 | 1x |
TRUE |
14 |
} |
|
15 | ||
16 | ||
17 |
#' Obtain Contrast for Specified Effect |
|
18 |
#' |
|
19 |
#' This is support function to obtain contrast matrix for type II/III testing. |
|
20 |
#' |
|
21 |
#' @param object (`mmrm`)\cr the fitted MMRM. |
|
22 |
#' @param effect (`string`) the name of the effect. |
|
23 |
#' @param type (`string`) type of test, "II", "III", '2', or '3'. |
|
24 |
#' @param tol (`numeric`) threshold blow which values are treated as 0. |
|
25 |
#' |
|
26 |
#' @return A `matrix` of the contrast. |
|
27 |
#' |
|
28 |
#' @keywords internal |
|
29 |
h_get_contrast <- function(object, effect, type = c("II", "III", "2", "3"), tol = sqrt(.Machine$double.eps)) { |
|
30 | 45x |
assert_class(object, "mmrm") |
31 | 45x |
assert_string(effect) |
32 | 45x |
assert_double(tol, finite = TRUE, len = 1L) |
33 | 45x |
type <- match.arg(type) |
34 | 45x |
mx <- component(object, "x_matrix") |
35 | 45x |
asg <- attr(mx, "assign") |
36 | 45x |
formula <- object$formula_parts$model_formula |
37 | 45x |
tms <- terms(formula) |
38 | 45x |
fcts <- attr(tms, "factors")[-1L, , drop = FALSE] # Discard the response. |
39 | 45x |
ods <- attr(tms, "order") |
40 | 45x |
assert_subset(effect, colnames(fcts)) |
41 | 45x |
idx <- which(effect == colnames(fcts)) |
42 | 45x |
cols <- which(asg == idx) |
43 | 45x |
xlev <- component(object, "xlev") |
44 | 45x |
contains_intercept <- (!0 %in% asg) && h_first_contain_categorical(effect, fcts, names(xlev)) |
45 | 45x |
coef_rows <- length(cols) - as.integer(contains_intercept) |
46 | 45x |
l_mx <- matrix(0, nrow = coef_rows, ncol = length(asg)) |
47 | 45x |
if (coef_rows == 0L) { |
48 | 1x |
return(l_mx) |
49 |
} |
|
50 | 44x |
if (contains_intercept) { |
51 | 4x |
l_mx[, cols] <- cbind(-1, diag(rep(1, coef_rows))) |
52 |
} else { |
|
53 | 40x |
l_mx[, cols] <- diag(rep(1, coef_rows)) |
54 |
} |
|
55 | 44x |
for (i in setdiff(seq_len(ncol(fcts)), idx)) { |
56 | 120x |
additional_vars <- names(which(fcts[, i] > fcts[, idx])) |
57 | 120x |
additional_numeric <- any(!additional_vars %in% names(xlev)) |
58 | 120x |
current_col <- which(asg == i) |
59 | 120x |
if (ods[i] >= ods[idx] && all(fcts[, i] >= fcts[, idx]) && !additional_numeric) { |
60 | 24x |
sub_mat <- switch(type, |
61 | 24x |
"2" = , |
62 | 24x |
"II" = { |
63 | 8x |
x1 <- mx[, cols, drop = FALSE] |
64 | 8x |
x0 <- mx[, -c(cols, current_col), drop = FALSE] |
65 | 8x |
x2 <- mx[, current_col, drop = FALSE] |
66 | 8x |
m <- diag(rep(1, nrow(x0))) - x0 %*% solve(t(x0) %*% x0) %*% t(x0) |
67 | 8x |
ret <- solve(t(x1) %*% m %*% x1) %*% t(x1) %*% m %*% x2 |
68 | 8x |
if (contains_intercept) { |
69 | 1x |
ret[-1, ] - ret[1, ] |
70 |
} else { |
|
71 | 7x |
ret |
72 |
} |
|
73 |
}, |
|
74 | 24x |
"3" = , |
75 | 24x |
"III" = { |
76 | 16x |
lvls <- h_obtain_lvls(effect, additional_vars, xlev) |
77 | 16x |
t_levels <- lvls$total |
78 | 16x |
nms_base <- colnames(mx)[cols] |
79 | 16x |
nms <- colnames(mx)[current_col] |
80 | 16x |
nms_base_split <- strsplit(nms_base, ":") |
81 | 16x |
nms_split <- strsplit(nms, ":") |
82 | 16x |
base_idx <- h_get_index(nms_split, nms_base_split) |
83 | 16x |
mt <- l_mx[, cols, drop = FALSE] / t_levels |
84 | 16x |
ret <- mt[, base_idx, drop = FALSE] |
85 |
# if there is extra levels, replace it with -1/t_levels |
|
86 | 16x |
ret[is.na(ret)] <- -1 / t_levels |
87 | 16x |
ret |
88 |
} |
|
89 |
) |
|
90 | 24x |
l_mx[, current_col] <- sub_mat |
91 |
} |
|
92 |
} |
|
93 | 44x |
l_mx[abs(l_mx) < tol] <- 0 |
94 | 44x |
l_mx |
95 |
} |
|
96 | ||
97 |
#' Conduct type II/III hypothesis testing on the MMRM fit results. |
|
98 |
#' |
|
99 |
#' @param mod (`mmrm`)\cr the fitted MMRM. |
|
100 |
#' @param ... not used. |
|
101 |
#' @inheritParams h_get_contrast |
|
102 |
#' |
|
103 |
#' @details |
|
104 |
#' `Anova` will return `anova` object with one row per variable and columns |
|
105 |
#' `Num Df`(numerator degrees of freedom), `Denom Df`(denominator degrees of freedom), |
|
106 |
#' `F Statistic` and `Pr(>=F)`. |
|
107 |
#' |
|
108 |
#' @keywords internal |
|
109 |
# Please do not load `car` and then create the documentation. The Rd file will be different. |
|
110 |
Anova.mmrm <- function(mod, type = c("II", "III", "2", "3"), tol = sqrt(.Machine$double.eps), ...) { # nolint |
|
111 | 9x |
assert_double(tol, finite = TRUE, len = 1L) |
112 | 9x |
type <- match.arg(type) |
113 | 9x |
vars <- colnames(attr(terms(mod$formula_parts$model_formula), "factors")) |
114 | 9x |
ret <- lapply( |
115 | 9x |
vars, |
116 | 9x |
function(x) df_md(mod, h_get_contrast(mod, x, type, tol)) |
117 |
) |
|
118 | 9x |
ret_df <- do.call(rbind.data.frame, ret) |
119 | 9x |
row.names(ret_df) <- vars |
120 | 9x |
colnames(ret_df) <- c("Num Df", "Denom Df", "F Statistic", "Pr(>=F)") |
121 | 9x |
class(ret_df) <- c("anova", "data.frame") |
122 | 9x |
attr(ret_df, "heading") <- sprintf( |
123 | 9x |
"Analysis of Fixed Effect Table (Type %s F tests)", |
124 | 9x |
switch(type, |
125 | 9x |
"2" = , |
126 | 9x |
"II" = "II", |
127 | 9x |
"3" = , |
128 | 9x |
"III" = "III" |
129 |
) |
|
130 |
) |
|
131 | 9x |
ret_df |
132 |
} |
|
133 | ||
134 | ||
135 |
#' Obtain Levels Prior and Posterior |
|
136 |
#' @param var (`string`) name of the effect. |
|
137 |
#' @param additional_vars (`character`) names of additional variables. |
|
138 |
#' @param xlev (`list`) named list of character levels. |
|
139 |
#' @param factors (`matrix`) the factor matrix. |
|
140 |
#' @keywords internal |
|
141 |
h_obtain_lvls <- function(var, additional_vars, xlev, factors) { |
|
142 | 18x |
assert_string(var) |
143 | 18x |
assert_character(additional_vars) |
144 | 18x |
assert_list(xlev, types = "character") |
145 | 18x |
nms <- names(xlev) |
146 | 18x |
assert_subset(additional_vars, nms) |
147 | 18x |
if (var %in% nms) { |
148 | 14x |
prior_vars <- intersect(nms[seq_len(match(var, nms) - 1)], additional_vars) |
149 | 14x |
prior_lvls <- vapply(xlev[prior_vars], length, FUN.VALUE = 1L) |
150 | 14x |
post_vars <- intersect(nms[seq(match(var, nms) + 1, length(nms))], additional_vars) |
151 | 14x |
post_lvls <- vapply(xlev[post_vars], length, FUN.VALUE = 1L) |
152 | 14x |
total_lvls <- prod(prior_lvls) * prod(post_lvls) |
153 |
} else { |
|
154 | 4x |
prior_lvls <- vapply(xlev[additional_vars], length, FUN.VALUE = 1L) |
155 | 4x |
post_lvls <- 2L |
156 | 4x |
total_lvls <- prod(prior_lvls) |
157 |
} |
|
158 | 18x |
list( |
159 | 18x |
prior = prior_lvls, |
160 | 18x |
post = post_lvls, |
161 | 18x |
total = total_lvls |
162 |
) |
|
163 |
} |
|
164 | ||
165 |
#' Check if the Effect is the First Categorical Effect |
|
166 |
#' @param effect (`string`) name of the effect. |
|
167 |
#' @param categorical (`character`) names of the categorical values. |
|
168 |
#' @param factors (`matrix`) the factor matrix. |
|
169 |
#' @keywords internal |
|
170 |
h_first_contain_categorical <- function(effect, factors, categorical) { |
|
171 | 9x |
assert_string(effect) |
172 | 9x |
assert_matrix(factors) |
173 | 9x |
assert_character(categorical) |
174 | 9x |
mt <- match(effect, colnames(factors)) |
175 | 9x |
varnms <- row.names(factors) |
176 |
# if the effect is not categorical in any value, return FALSE |
|
177 | 9x |
if (!any(varnms[factors[, mt] > 0] %in% categorical)) { |
178 | 2x |
return(FALSE) |
179 |
} |
|
180 |
# keep only categorical rows that is in front of the current factor |
|
181 | 7x |
factors <- factors[row.names(factors) %in% categorical, seq_len(mt - 1L), drop = FALSE] |
182 |
# if previous cols are all numerical, return TRUE |
|
183 | 7x |
if (ncol(factors) < 1L) { |
184 | 4x |
return(TRUE) |
185 |
} |
|
186 | 3x |
col_ind <- apply(factors, 2, prod) |
187 |
# if any of the previous cols are categorical, return FALSE |
|
188 | 3x |
return(!any(col_ind > 0)) |
189 |
} |
|
190 | ||
191 |
#' Test if the First Vector is Subset of the Second Vector |
|
192 |
#' @param x (`vector`) the first list. |
|
193 |
#' @param y (`vector`) the second list. |
|
194 |
#' @keywords internal |
|
195 |
h_get_index <- function(x, y) { |
|
196 | 18x |
assert_list(x) |
197 | 18x |
assert_list(y) |
198 | 18x |
vapply( |
199 | 18x |
x, |
200 | 18x |
\(i) { |
201 | 68x |
r <- vapply(y, \(j) test_subset(j, i), FUN.VALUE = TRUE) |
202 | 68x |
if (sum(r) == 1L) { |
203 | 65x |
which(r) |
204 |
} else { |
|
205 | 18x |
NA_integer_ |
206 |
} |
|
207 |
}, |
|
208 | 18x |
FUN.VALUE = 1L |
209 |
) |
|
210 |
} |
1 |
#' Obtain Kenward-Roger Adjustment Components |
|
2 |
#' |
|
3 |
#' @description Obtains the components needed downstream for the computation of Kenward-Roger degrees of freedom. |
|
4 |
#' Used in [mmrm()] fitting if method is "Kenward-Roger". |
|
5 |
#' |
|
6 |
#' @param tmb_data (`mmrm_tmb_data`)\cr produced by [h_mmrm_tmb_data()]. |
|
7 |
#' @param theta (`numeric`)\cr theta estimate. |
|
8 |
#' |
|
9 |
#' @details the function returns a named list, \eqn{P}, \eqn{Q} and \eqn{R}, which corresponds to the |
|
10 |
#' paper in 1997. The matrices are stacked in columns so that \eqn{P}, \eqn{Q} and \eqn{R} has the same |
|
11 |
#' column number(number of beta parameters). The number of rows, is dependent on |
|
12 |
#' the total number of theta and number of groups, if the fit is a grouped mmrm. |
|
13 |
#' For \eqn{P} matrix, it is stacked sequentially. For \eqn{Q} and \eqn{R} matrix, it is stacked so |
|
14 |
#' that the \eqn{Q_{ij}} and \eqn{R_{ij}} is stacked from \eqn{j} then to \eqn{i}, i.e. \eqn{R_{i1}}, \eqn{R_{i2}}, etc. |
|
15 |
#' \eqn{Q} and \eqn{R} only contains intra-group results and inter-group results should be all zero matrices |
|
16 |
#' so they are not stacked in the result. |
|
17 |
#' |
|
18 |
#' @return Named list with elements: |
|
19 |
#' - `P`: `matrix` of \eqn{P} component. |
|
20 |
#' - `Q`: `matrix` of \eqn{Q} component. |
|
21 |
#' - `R`: `matrix` of \eqn{R} component. |
|
22 |
#' |
|
23 |
#' @keywords internal |
|
24 |
h_get_kr_comp <- function(tmb_data, theta) { |
|
25 | 47x |
assert_class(tmb_data, "mmrm_tmb_data") |
26 | 47x |
assert_class(theta, "numeric") |
27 | 47x |
.Call(`_mmrm_get_pqr`, PACKAGE = "mmrm", tmb_data, theta) |
28 |
} |
|
29 | ||
30 |
#' Calculation of Kenward-Roger Degrees of Freedom for Multi-Dimensional Contrast |
|
31 |
#' |
|
32 |
#' @description Used in [df_md()] if method is "Kenward-Roger" or "Kenward-Roger-Linear". |
|
33 |
#' |
|
34 |
#' @inheritParams h_df_md_sat |
|
35 |
#' @inherit h_df_md_sat return |
|
36 |
#' @keywords internal |
|
37 |
h_df_md_kr <- function(object, contrast) { |
|
38 | 6x |
assert_class(object, "mmrm") |
39 | 6x |
assert_matrix(contrast, mode = "numeric", any.missing = FALSE, ncols = length(component(object, "beta_est"))) |
40 | 6x |
if (component(object, "reml") != 1) { |
41 | ! |
stop("Kenward-Roger is only for REML") |
42 |
} |
|
43 | 6x |
kr_comp <- object$kr_comp |
44 | 6x |
w <- component(object, "theta_vcov") |
45 | 6x |
v_adj <- object$beta_vcov_adj |
46 | 6x |
df <- h_kr_df(v0 = object$beta_vcov, l = contrast, w = w, p = kr_comp$P) |
47 | ||
48 | 6x |
h_test_md(object, contrast, df = df$m, f_stat_factor = df$lambda) |
49 |
} |
|
50 | ||
51 |
#' Calculation of Kenward-Roger Degrees of Freedom for One-Dimensional Contrast |
|
52 |
#' |
|
53 |
#' @description Used in [df_1d()] if method is |
|
54 |
#' "Kenward-Roger" or "Kenward-Roger-Linear". |
|
55 |
#' |
|
56 |
#' @inheritParams h_df_1d_sat |
|
57 |
#' @inherit h_df_1d_sat return |
|
58 |
#' @keywords internal |
|
59 |
h_df_1d_kr <- function(object, contrast) { |
|
60 | 21x |
assert_class(object, "mmrm") |
61 | 21x |
assert_numeric(contrast, len = length(component(object, "beta_est"))) |
62 | 21x |
if (component(object, "reml") != 1) { |
63 | ! |
stop("Kenward-Roger is only for REML!") |
64 |
} |
|
65 | ||
66 | 21x |
df <- h_kr_df( |
67 | 21x |
v0 = object$beta_vcov, |
68 | 21x |
l = matrix(contrast, nrow = 1), |
69 | 21x |
w = component(object, "theta_vcov"), |
70 | 21x |
p = object$kr_comp$P |
71 |
) |
|
72 | ||
73 | 21x |
h_test_1d(object, contrast, df$m) |
74 |
} |
|
75 | ||
76 |
#' Obtain the Adjusted Kenward-Roger degrees of freedom |
|
77 |
#' |
|
78 |
#' @description Obtains the adjusted Kenward-Roger degrees of freedom and F statistic scale parameter. |
|
79 |
#' Used in [h_df_md_kr()] or [h_df_1d_kr]. |
|
80 |
#' |
|
81 |
#' @param v0 (`matrix`)\cr unadjusted covariance matrix. |
|
82 |
#' @param l (`matrix`)\cr linear combination matrix. |
|
83 |
#' @param w (`matrix`)\cr hessian matrix. |
|
84 |
#' @param p (`matrix`)\cr P matrix from [h_get_kr_comp()]. |
|
85 |
#' |
|
86 |
#' @return Named list with elements: |
|
87 |
#' - `m`: `numeric` degrees of freedom. |
|
88 |
#' - `lambda`: `numeric` F statistic scale parameter. |
|
89 |
#' |
|
90 |
#' @keywords internal |
|
91 |
h_kr_df <- function(v0, l, w, p) { |
|
92 | 28x |
n_beta <- ncol(v0) |
93 | 28x |
assert_matrix(v0, ncols = n_beta, nrows = n_beta) |
94 | 28x |
assert_matrix(l, ncols = n_beta) |
95 | 28x |
n_theta <- ncol(w) |
96 | 28x |
assert_matrix(w, ncols = n_theta, nrows = n_theta) |
97 | 28x |
n_visits <- ncol(p) |
98 | 28x |
assert_matrix(p, nrows = n_visits * n_theta) |
99 |
# see vignettes/kenward.Rmd#279 |
|
100 | 28x |
slvol <- solve(h_quad_form_mat(l, v0)) |
101 | 28x |
m <- h_quad_form_mat(t(l), slvol) |
102 | 28x |
nl <- nrow(l) |
103 | 28x |
mv0 <- m %*% v0 |
104 | 28x |
pl <- lapply(seq_len(nrow(p) / ncol(p)), function(x) { |
105 | 108x |
ii <- (x - 1) * ncol(p) + 1 |
106 | 108x |
jj <- x * ncol(p) |
107 | 108x |
p[ii:jj, ] |
108 |
}) |
|
109 | 28x |
mv0pv0 <- lapply(pl, function(x) { |
110 | 108x |
mv0 %*% x %*% v0 |
111 |
}) |
|
112 | 28x |
a1 <- 0 |
113 | 28x |
a2 <- 0 |
114 |
# see vignettes/kenward.Rmd#283 |
|
115 | 28x |
for (i in seq_len(length(pl))) { |
116 | 108x |
for (j in seq_len(length(pl))) { |
117 | 592x |
a1 <- a1 + w[i, j] * h_tr(mv0pv0[[i]]) * h_tr(mv0pv0[[j]]) |
118 | 592x |
a2 <- a2 + w[i, j] * h_tr(mv0pv0[[i]] %*% mv0pv0[[j]]) |
119 |
} |
|
120 |
} |
|
121 | 28x |
b <- 1 / (2 * nl) * (a1 + 6 * a2) |
122 | 28x |
e <- 1 + a2 / nl |
123 | 28x |
e_star <- 1 / (1 - a2 / nl) |
124 | 28x |
g <- ((nl + 1) * a1 - (nl + 4) * a2) / ((nl + 2) * a2) |
125 | 28x |
denom <- (3 * nl + 2 - 2 * g) |
126 | 28x |
c1 <- g / denom |
127 | 28x |
c2 <- (nl - g) / denom |
128 | 28x |
c3 <- (nl + 2 - g) / denom |
129 | 28x |
v_star <- 2 / nl * (1 + c1 * b) / (1 - c2 * b)^2 / (1 - c3 * b) |
130 | 28x |
rho <- v_star / (2 * e_star^2) |
131 | 28x |
m <- 4 + (nl + 2) / (nl * rho - 1) |
132 | 28x |
lambda <- m / (e_star * (m - 2)) |
133 | 28x |
list(m = m, lambda = lambda) |
134 |
} |
|
135 | ||
136 |
#' Obtain the Adjusted Covariance Matrix |
|
137 |
#' |
|
138 |
#' @description Obtains the Kenward-Roger adjusted covariance matrix for the |
|
139 |
#' coefficient estimates. |
|
140 |
#' Used in [mmrm()] fitting if method is "Kenward-Roger" or "Kenward-Roger-Linear". |
|
141 |
#' |
|
142 |
#' @param v (`matrix`)\cr unadjusted covariance matrix. |
|
143 |
#' @param w (`matrix`)\cr hessian matrix. |
|
144 |
#' @param p (`matrix`)\cr P matrix from [h_get_kr_comp()]. |
|
145 |
#' @param q (`matrix`)\cr Q matrix from [h_get_kr_comp()]. |
|
146 |
#' @param r (`matrix`)\cr R matrix from [h_get_kr_comp()]. |
|
147 |
#' @param linear (`flag`)\cr whether to use linear Kenward-Roger approximation. |
|
148 |
#' |
|
149 |
#' @return The matrix of adjusted covariance matrix. |
|
150 |
#' |
|
151 |
#' @keywords internal |
|
152 |
h_var_adj <- function(v, w, p, q, r, linear = FALSE) { |
|
153 | 49x |
assert_flag(linear) |
154 | 49x |
n_beta <- ncol(v) |
155 | 49x |
assert_matrix(v, nrows = n_beta) |
156 | 49x |
n_theta <- ncol(w) |
157 | 49x |
assert_matrix(w, nrows = n_theta) |
158 | 49x |
n_visits <- ncol(p) |
159 | 49x |
theta_per_group <- nrow(q) / nrow(p) |
160 | 49x |
n_groups <- n_theta / theta_per_group |
161 | 49x |
assert_matrix(p, nrows = n_theta * n_visits) |
162 | 49x |
assert_matrix(q, nrows = theta_per_group^2 * n_groups * n_visits, ncols = n_visits) |
163 | 49x |
assert_matrix(r, nrows = theta_per_group^2 * n_groups * n_visits, ncols = n_visits) |
164 | 49x |
if (linear) { |
165 | 13x |
r <- matrix(0, nrow = nrow(r), ncol = ncol(r)) |
166 |
} |
|
167 | ||
168 |
# see vignettes/kenward.Rmd#131 |
|
169 | 49x |
ret <- v |
170 | 49x |
for (i in seq_len(n_theta)) { |
171 | 264x |
for (j in seq_len(n_theta)) { |
172 | 2164x |
gi <- ceiling(i / theta_per_group) |
173 | 2164x |
gj <- ceiling(j / theta_per_group) |
174 | 2164x |
iid <- (i - 1) * n_beta + 1 |
175 | 2164x |
jid <- (j - 1) * n_beta + 1 |
176 | 2164x |
ii <- i - (gi - 1) * theta_per_group |
177 | 2164x |
jj <- j - (gi - 1) * theta_per_group |
178 | 2164x |
ijid <- ((ii - 1) * theta_per_group + jj - 1) * n_beta + (gi - 1) * n_beta * theta_per_group^2 + 1 |
179 | 2164x |
if (gi != gj) { |
180 | 592x |
ret <- ret + 2 * w[i, j] * v %*% (-p[iid:(iid + n_beta - 1), ] %*% v %*% p[jid:(jid + n_beta - 1), ]) %*% v |
181 |
} else { |
|
182 | 1572x |
ret <- ret + 2 * w[i, j] * v %*% ( |
183 | 1572x |
q[ijid:(ijid + n_beta - 1), ] - |
184 | 1572x |
p[iid:(iid + n_beta - 1), ] %*% v %*% p[jid:(jid + n_beta - 1), ] - |
185 | 1572x |
1 / 4 * r[ijid:(ijid + n_beta - 1), ] |
186 | 1572x |
) %*% v |
187 |
} |
|
188 |
} |
|
189 |
} |
|
190 | 49x |
ret |
191 |
} |
1 |
#' Calculation of Degrees of Freedom for One-Dimensional Contrast |
|
2 |
#' |
|
3 |
#' @description `r lifecycle::badge("stable")` |
|
4 |
#' Calculates the estimate, adjusted standard error, degrees of freedom, |
|
5 |
#' t statistic and p-value for one-dimensional contrast. |
|
6 |
#' |
|
7 |
#' @param object (`mmrm`)\cr the MMRM fit. |
|
8 |
#' @param contrast (`numeric`)\cr contrast vector. Note that this should not include |
|
9 |
#' elements for singular coefficient estimates, i.e. only refer to the |
|
10 |
#' actually estimated coefficients. |
|
11 |
#' @return List with `est`, `se`, `df`, `t_stat` and `p_val`. |
|
12 |
#' @export |
|
13 |
#' |
|
14 |
#' @examples |
|
15 |
#' object <- mmrm( |
|
16 |
#' formula = FEV1 ~ RACE + SEX + ARMCD * AVISIT + us(AVISIT | USUBJID), |
|
17 |
#' data = fev_data |
|
18 |
#' ) |
|
19 |
#' contrast <- numeric(length(object$beta_est)) |
|
20 |
#' contrast[3] <- 1 |
|
21 |
#' df_1d(object, contrast) |
|
22 |
df_1d <- function(object, contrast) { |
|
23 | 338x |
assert_class(object, "mmrm") |
24 | 338x |
assert_numeric(contrast, len = length(component(object, "beta_est")), any.missing = FALSE) |
25 | 338x |
contrast <- as.vector(contrast) |
26 | 338x |
switch(object$method, |
27 | 318x |
"Satterthwaite" = h_df_1d_sat(object, contrast), |
28 | 19x |
"Kenward-Roger" = h_df_1d_kr(object, contrast), |
29 | ! |
"Residual" = h_df_1d_res(object, contrast), |
30 | 1x |
"Between-Within" = h_df_1d_bw(object, contrast), |
31 | ! |
stop("Unrecognized degrees of freedom method: ", object$method) |
32 |
) |
|
33 |
} |
|
34 | ||
35 | ||
36 |
#' Calculation of Degrees of Freedom for Multi-Dimensional Contrast |
|
37 |
#' |
|
38 |
#' @description `r lifecycle::badge("stable")` |
|
39 |
#' Calculates the estimate, standard error, degrees of freedom, |
|
40 |
#' t statistic and p-value for one-dimensional contrast, depending on the method |
|
41 |
#' used in [mmrm()]. |
|
42 |
#' |
|
43 |
#' @param object (`mmrm`)\cr the MMRM fit. |
|
44 |
#' @param contrast (`matrix`)\cr numeric contrast matrix, if given a `numeric` |
|
45 |
#' then this is coerced to a row vector. Note that this should not include |
|
46 |
#' elements for singular coefficient estimates, i.e. only refer to the |
|
47 |
#' actually estimated coefficients. |
|
48 |
#' |
|
49 |
#' @return List with `num_df`, `denom_df`, `f_stat` and `p_val` (2-sided p-value). |
|
50 |
#' @export |
|
51 |
#' |
|
52 |
#' @examples |
|
53 |
#' object <- mmrm( |
|
54 |
#' formula = FEV1 ~ RACE + SEX + ARMCD * AVISIT + us(AVISIT | USUBJID), |
|
55 |
#' data = fev_data |
|
56 |
#' ) |
|
57 |
#' contrast <- matrix(data = 0, nrow = 2, ncol = length(object$beta_est)) |
|
58 |
#' contrast[1, 2] <- contrast[2, 3] <- 1 |
|
59 |
#' df_md(object, contrast) |
|
60 |
df_md <- function(object, contrast) { |
|
61 | 150x |
assert_class(object, "mmrm") |
62 | 150x |
assert_numeric(contrast, any.missing = FALSE) |
63 | 150x |
if (!is.matrix(contrast)) { |
64 | 113x |
contrast <- matrix(contrast, ncol = length(contrast)) |
65 |
} |
|
66 | 150x |
assert_matrix(contrast, ncols = length(component(object, "beta_est"))) |
67 | 150x |
if (nrow(contrast) == 0) { |
68 | 1x |
return( |
69 | 1x |
list( |
70 | 1x |
num_df = 0, |
71 | 1x |
denom_df = NA_real_, |
72 | 1x |
f_stat = NA_real_, |
73 | 1x |
p_val = NA_real_ |
74 |
) |
|
75 |
) |
|
76 |
} |
|
77 | 149x |
switch(object$method, |
78 | 145x |
"Satterthwaite" = h_df_md_sat(object, contrast), |
79 | 3x |
"Kenward-Roger" = h_df_md_kr(object, contrast), |
80 | ! |
"Residual" = h_df_md_res(object, contrast), |
81 | 1x |
"Between-Within" = h_df_md_bw(object, contrast), |
82 | ! |
stop("Unrecognized degrees of freedom method: ", object$method) |
83 |
) |
|
84 |
} |
|
85 | ||
86 |
#' Creating T-Statistic Test Results For One-Dimensional Contrast |
|
87 |
#' |
|
88 |
#' @description Creates a list of results for one-dimensional contrasts using |
|
89 |
#' a t-test statistic and the given degrees of freedom. |
|
90 |
#' |
|
91 |
#' @inheritParams df_1d |
|
92 |
#' @param df (`number`)\cr degrees of freedom for the one-dimensional contrast. |
|
93 |
#' |
|
94 |
#' @return List with `est`, `se`, `df`, `t_stat` and `p_val` (2-sided p-value). |
|
95 |
#' |
|
96 |
#' @keywords internal |
|
97 |
h_test_1d <- function(object, |
|
98 |
contrast, |
|
99 |
df) { |
|
100 | 486x |
assert_class(object, "mmrm") |
101 | 486x |
assert_numeric(contrast, len = length(component(object, "beta_est"))) |
102 | 486x |
assert_number(df, lower = .Machine$double.xmin) |
103 | ||
104 | 486x |
est <- sum(contrast * component(object, "beta_est")) |
105 | 486x |
var <- h_quad_form_vec(contrast, component(object, "beta_vcov")) |
106 | 486x |
se <- sqrt(var) |
107 | 486x |
t_stat <- est / se |
108 | 486x |
p_val <- 2 * stats::pt(q = abs(t_stat), df = df, lower.tail = FALSE) |
109 | ||
110 | 486x |
list( |
111 | 486x |
est = est, |
112 | 486x |
se = se, |
113 | 486x |
df = df, |
114 | 486x |
t_stat = t_stat, |
115 | 486x |
p_val = p_val |
116 |
) |
|
117 |
} |
|
118 | ||
119 |
#' Creating F-Statistic Test Results For Multi-Dimensional Contrast |
|
120 |
#' |
|
121 |
#' @description Creates a list of results for multi-dimensional contrasts using |
|
122 |
#' an F-test statistic and the given degrees of freedom. |
|
123 |
#' |
|
124 |
#' @inheritParams df_md |
|
125 |
#' @param contrast (`matrix`)\cr numeric contrast matrix. |
|
126 |
#' @param df (`number`)\cr denominator degrees of freedom for the multi-dimensional contrast. |
|
127 |
#' @param f_stat_factor (`number`)\cr optional scaling factor on top of the standard F-statistic. |
|
128 |
#' |
|
129 |
#' @return List with `num_df`, `denom_df`, `f_stat` and `p_val` (2-sided p-value). |
|
130 |
#' |
|
131 |
#' @keywords internal |
|
132 |
h_test_md <- function(object, |
|
133 |
contrast, |
|
134 |
df, |
|
135 |
f_stat_factor = 1) { |
|
136 | 15x |
assert_class(object, "mmrm") |
137 | 15x |
assert_matrix(contrast, ncols = length(component(object, "beta_est"))) |
138 | 15x |
num_df <- nrow(contrast) |
139 | 15x |
assert_number(df, lower = .Machine$double.xmin) |
140 | 15x |
assert_number(f_stat_factor, lower = .Machine$double.xmin) |
141 | ||
142 | 15x |
prec_contrast <- solve(h_quad_form_mat(contrast, component(object, "beta_vcov"))) |
143 | 15x |
contrast_est <- component(object, "beta_est") %*% t(contrast) |
144 | 15x |
f_statistic <- as.numeric(f_stat_factor / num_df * h_quad_form_mat(contrast_est, prec_contrast)) |
145 | 15x |
p_val <- stats::pf( |
146 | 15x |
q = f_statistic, |
147 | 15x |
df1 = num_df, |
148 | 15x |
df2 = df, |
149 | 15x |
lower.tail = FALSE |
150 |
) |
|
151 | ||
152 | 15x |
list( |
153 | 15x |
num_df = num_df, |
154 | 15x |
denom_df = df, |
155 | 15x |
f_stat = f_statistic, |
156 | 15x |
p_val = p_val |
157 |
) |
|
158 |
} |
1 |
#' Covariance Type Database |
|
2 |
#' |
|
3 |
#' An internal constant for covariance type information. |
|
4 |
#' |
|
5 |
#' @format A data frame with 5 variables and one record per covariance type: |
|
6 |
#' |
|
7 |
#' \describe{ |
|
8 |
#' \item{name}{ |
|
9 |
#' The long-form name of the covariance structure type |
|
10 |
#' } |
|
11 |
#' \item{abbr}{ |
|
12 |
#' The abbreviated name of the covariance structure type |
|
13 |
#' } |
|
14 |
#' \item{habbr}{ |
|
15 |
#' The abbreviated name of the heterogeneous version of a covariance |
|
16 |
#' structure type (The abbreviated name (`abbr`) with a trailing `"h"` if |
|
17 |
#' the structure has a heterogeneous implementation or `NA` otherwise). |
|
18 |
#' } |
|
19 |
#' \item{heterogeneous}{ |
|
20 |
#' A logical value indicating whether the covariance structure has a |
|
21 |
#' heterogeneous counterpart. |
|
22 |
#' } |
|
23 |
#' \item{spatial}{ |
|
24 |
#' A logical value indicating whether the covariance structure is spatial. |
|
25 |
#' } |
|
26 |
#' } |
|
27 |
#' |
|
28 |
#' @keywords internal |
|
29 |
COV_TYPES <- local({ # nolint |
|
30 |
type <- function(name, abbr, habbr, heterogeneous, spatial) { |
|
31 |
args <- as.list(match.call()[-1]) |
|
32 |
do.call(data.frame, args) |
|
33 |
} |
|
34 | ||
35 |
as.data.frame( |
|
36 |
col.names = names(formals(type)), |
|
37 |
rbind( |
|
38 |
type("unstructured", "us", NA, FALSE, FALSE), |
|
39 |
type("Toeplitz", "toep", "toeph", TRUE, FALSE), |
|
40 |
type("auto-regressive order one", "ar1", "ar1h", TRUE, FALSE), |
|
41 |
type("ante-dependence", "ad", "adh", TRUE, FALSE), |
|
42 |
type("compound symmetry", "cs", "csh", TRUE, FALSE), |
|
43 |
type("spatial exponential", "sp_exp", NA, FALSE, TRUE) |
|
44 |
) |
|
45 |
) |
|
46 |
}) |
|
47 | ||
48 |
#' Covariance Types |
|
49 |
#' |
|
50 |
#' @description `r lifecycle::badge("stable")` |
|
51 |
#' |
|
52 |
#' @param form (`character`)\cr covariance structure type name form. One or |
|
53 |
#' more of `"name"`, `"abbr"` (abbreviation), or `"habbr"` (heterogeneous |
|
54 |
#' abbreviation). |
|
55 |
#' @param filter (`character`)\cr covariance structure type filter. One or |
|
56 |
#' more of `"heterogeneous"` or `"spatial"`. |
|
57 |
#' |
|
58 |
#' @return A character vector of accepted covariance structure type names and |
|
59 |
#' abbreviations. |
|
60 |
#' |
|
61 |
#' @section Abbreviations for Covariance Structures: |
|
62 |
#' |
|
63 |
#' ## Common Covariance Structures: |
|
64 |
#' |
|
65 |
#' \tabular{clll}{ |
|
66 |
#' |
|
67 |
#' \strong{Structure} |
|
68 |
#' \tab \strong{Description} |
|
69 |
#' \tab \strong{Parameters} |
|
70 |
#' \tab \strong{\eqn{(i, j)} element} |
|
71 |
#' \cr |
|
72 |
#' |
|
73 |
#' ad |
|
74 |
#' \tab Ante-dependence |
|
75 |
#' \tab \eqn{m} |
|
76 |
#' \tab \eqn{\sigma^{2}\prod_{k=i}^{j-1}\rho_{k}} |
|
77 |
#' \cr |
|
78 |
#' |
|
79 |
#' adh |
|
80 |
#' \tab Heterogeneous ante-dependence |
|
81 |
#' \tab \eqn{2m-1} |
|
82 |
#' \tab \eqn{\sigma_{i}\sigma_{j}\prod_{k=i}^{j-1}\rho_{k}} |
|
83 |
#' \cr |
|
84 |
#' |
|
85 |
#' ar1 |
|
86 |
#' \tab First-order auto-regressive |
|
87 |
#' \tab \eqn{2} |
|
88 |
#' \tab \eqn{\sigma^{2}\rho^{\left \vert {i-j} \right \vert}} |
|
89 |
#' \cr |
|
90 |
#' |
|
91 |
#' ar1h |
|
92 |
#' \tab Heterogeneous first-order auto-regressive |
|
93 |
#' \tab \eqn{m+1} |
|
94 |
#' \tab \eqn{\sigma_{i}\sigma_{j}\rho^{\left \vert {i-j} \right \vert}} |
|
95 |
#' \cr |
|
96 |
#' |
|
97 |
#' cs |
|
98 |
#' \tab Compound symmetry |
|
99 |
#' \tab \eqn{2} |
|
100 |
#' \tab \eqn{\sigma^{2}\left[ \rho I(i \neq j)+I(i=j) \right]} |
|
101 |
#' \cr |
|
102 |
#' |
|
103 |
#' csh |
|
104 |
#' \tab Heterogeneous compound symmetry |
|
105 |
#' \tab \eqn{m+1} |
|
106 |
#' \tab \eqn{\sigma_{i}\sigma_{j}\left[ \rho I(i \neq j)+I(i=j) \right]} |
|
107 |
#' \cr |
|
108 |
#' |
|
109 |
#' toep |
|
110 |
#' \tab Toeplitz |
|
111 |
#' \tab \eqn{m} |
|
112 |
#' \tab \eqn{\sigma_{\left \vert {i-j} \right \vert +1}} |
|
113 |
#' \cr |
|
114 |
#' |
|
115 |
#' toeph |
|
116 |
#' \tab Heterogeneous Toeplitz |
|
117 |
#' \tab \eqn{2m-1} |
|
118 |
#' \tab \eqn{\sigma_{i}\sigma_{j}\rho_{\left \vert {i-j} \right \vert}} |
|
119 |
#' \cr |
|
120 |
#' |
|
121 |
#' us |
|
122 |
#' \tab Unstructured |
|
123 |
#' \tab \eqn{m(m+1)/2} |
|
124 |
#' \tab \eqn{\sigma_{ij}} |
|
125 |
#' |
|
126 |
#' } |
|
127 |
#' |
|
128 |
#' where \eqn{i} and \eqn{j} denote \eqn{i}-th and \eqn{j}-th time points, |
|
129 |
#' respectively, out of total \eqn{m} time points, \eqn{1 \leq i, j \leq m}. |
|
130 |
#' |
|
131 |
#' @note The **ante-dependence** covariance structure in this package refers to |
|
132 |
#' homogeneous ante-dependence, while the ante-dependence covariance structure |
|
133 |
#' from SAS `PROC MIXED` refers to heterogeneous ante-dependence and the |
|
134 |
#' homogeneous version is not available in SAS. |
|
135 |
#' |
|
136 |
#' @note For all non-spatial covariance structures, the time variable must |
|
137 |
#' be coded as a factor. |
|
138 |
#' |
|
139 |
#' ## Spatial Covariance structures: |
|
140 |
#' |
|
141 |
#' \tabular{clll}{ |
|
142 |
#' |
|
143 |
#' \strong{Structure} |
|
144 |
#' \tab \strong{Description} |
|
145 |
#' \tab \strong{Parameters} |
|
146 |
#' \tab \strong{\eqn{(i, j)} element} |
|
147 |
#' \cr |
|
148 |
#' |
|
149 |
#' sp_exp |
|
150 |
#' \tab spatial exponential |
|
151 |
#' \tab \eqn{2} |
|
152 |
#' \tab \eqn{\sigma^{2}\rho^{-d_{ij}}} |
|
153 |
#' |
|
154 |
#' } |
|
155< |