run_extended_tests <- identical(Sys.getenv("DYNAMITE_EXTENDED_TESTS"), "true") data.table::setDTthreads(1) # For CRAN # Capture both message and output types capture_all_output <- function(x) { utils::capture.output( utils::capture.output(x, type = "message"), type = "output" ) } test_that("multivariate gaussian fit and predict work", { skip_if_not(run_extended_tests) set.seed(1) N <- 100 T_ <- 50 S <- crossprod(matrix(rnorm(4), 2, 2)) L <- t(chol(S)) y1 <- matrix(0, N, T_) y2 <- matrix(0, N, T_) x <- matrix(0, N, T_) for (t in 2:T_) { for (i in 1:N){ mu <- c(0.7 * y1[i, t - 1], 0.4 * y2[i, t - 1] - 0.2 * y1[i, t - 1]) y <- mu + L %*% rnorm(2) y1[i, t] <- y[1L] y2[i, t] <- y[2L] x[i, t] <- rnorm(1, c(0.5 * y1[i, t - 1]), 0.5) } } d <- data.frame( y1 = c(y1), y2 = c(y2), x = c(x), t = rep(1:T_, each = N), id = 1:N ) f <- obs(c(y1, y2) ~ -1 + lag(y1) | -1 + lag(y1) + lag(y2), "mvgaussian") + obs(x ~ -1 + lag(y1), "gaussian") fit <- dynamite( dformula = f, data = d, time = "t", group = "id", chains = 1, iter = 2000, refresh = 0 ) expect_error( sumr <- summary(fit, types = "corr"), NA ) expect_equal( sumr$mean, cov2cor(S)[2, 1], tolerance = 0.1 ) expect_error( sumr <- summary(fit, types = "sigma"), NA ) expect_equal( sumr$mean, c(0.5, sqrt(diag(S))), tolerance = 0.1 ) expect_error( sumr <- summary(fit, types = "beta"), NA ) expect_equal( sumr$mean, c(0.5, 0.7, -0.2, 0.4), tolerance = 0.1 ) expect_error( pred <- predict(fit, n_draws = 5), NA ) expect_true( all(!is.na(pred[, c("y1_new", "y2_new", "x_new")])), NA ) expect_error( pred <- predict(fit, n_draws = 5, type = "mean"), NA ) pred <- pred[pred$t > 1, ] expect_true( all(!is.na(pred[, c("y1_mean", "y2_mean", "x_mean")])), NA ) expect_error( pred <- predict(fit, n_draws = 5, type = "link"), NA ) pred <- pred[pred$t > 1, ] expect_true( all(!is.na(pred[, c("y1_link", "y2_link", "x_link")])), NA ) expect_error( pred <- fitted(fit, n_draws = 5), NA ) pred <- pred[pred$t > 1, ] expect_true( all(!is.na(pred[, c("y1_fitted", "y2_fitted", "x_fitted")])), NA ) }) test_that("multinomial fit and predict work", { skip_if_not(run_extended_tests) set.seed(1) n_id <- 100L n_time <- 20L d <- data.frame( y1 = sample(10, size = n_id, replace = TRUE), y2 = sample(12, size = n_id, replace = TRUE), y3 = sample(15, size = n_id, replace = TRUE), time = 1, id = seq_len(n_id) ) d$n <- d$y1 + d$y2 + d$y3 d <- dplyr::right_join( d, data.frame( time = rep(seq_len(n_time), each = n_id), id = seq_len(n_id) ) ) d$z <- rnorm(nrow(d)) d$n[is.na(d$n)] <- sample(37, size = sum(is.na(d$n)), replace = TRUE) f <- obs( c(y1, y2, y3) ~ z + lag(y1) + lag(y2) + lag(y3) + trials(n), family = "multinomial" ) init <- list( beta_y2 = c(1.2, 0.8, 0.2, 0.1), beta_y3 = c(1, 0.5, 0.6, 0.3), a_y2 = -0.1, a_y3 = 0.2 ) expect_error( fit <- dynamite( dformula = f, data = d, time = "time", group = "id", chains = 1, iter = 1, algorithm = "Fixed_param", init = list(init), ), NA ) expect_error( pred <- predict(fit, type = "response"), NA ) expect_identical( pred$y1_new + pred$y2_new + pred$y3_new, pred$n ) expect_error( predict(fit, type = "mean"), NA ) expect_error( predict(fit, type = "link"), NA ) expect_error( fitted(fit), NA ) }) test_that("time-invariant cumulative fit and predict work", { skip_if_not(run_extended_tests) set.seed(0) n <- 100 t <- 30 x <- matrix(0, n, t) y <- matrix(0, n, t) p <- matrix(0, n, 4) alpha <- c(-1, 0, 1) for (i in seq_len(t)) { x[, i] <- rnorm(n) eta <- 0.6 * x[, i] p[, 1] <- 1 - plogis(eta - alpha[1]) p[, 2] <- plogis(eta - alpha[1]) - plogis(eta - alpha[2]) p[, 3] <- plogis(eta - alpha[2]) - plogis(eta - alpha[3]) p[, 4] <- plogis(eta - alpha[3]) y[, i] <- apply(p, 1, sample, x = 1:4, size = 1, replace = FALSE) } d <- data.frame( y = factor(c(y), levels = 1:4), x = c(x), time = rep(seq_len(t), each = n), id = rep(seq_len(n), t) ) expect_error( fit <- dynamite( dformula = obs(y ~ x, family = "cumulative", link = "logit"), data = d, time = "time", group = "id" ), NA ) expect_error( plot(fit), NA ) expect_error( as.data.table(fit, types = "cutpoint"), NA ) expect_error( pred <- predict(fit, type = "response", n_draws = 10), NA ) expect_true( all(pred$y_new %in% 1:4) ) expect_error( pred <- predict(fit, type = "mean", n_draws = 10), NA ) expect_true( all(!is.na(pred[, paste0("y_mean_", 1:4)])) ) expect_error( pred <- predict(fit, type = "link", n_draws = 10), NA ) expect_true( all(!is.na(pred$y_link)) ) expect_error( pred <- fitted(fit, n_draws = 10), NA ) expect_true( all(!is.na(pred[, paste0("y_fitted_", 1:4)])) ) }) test_that("time-varying cutpoints for cumulative works", { skip_if_not(run_extended_tests) set.seed(34) n <- 50 t <- 30 x <- matrix(0, n, t) y <- matrix(0, n, t) alpha_spline <- t(replicate(3, cumsum(rnorm(t, sd = 0.5)))) p <- array(0, c(n, 4, t)) cutpoints <- matrix(0, 3, t) for (i in seq_len(t)) { tmp <- exp(c(0, alpha_spline[, i])) for (j in 1:3) { cutpoints[j, i] <- log(sum(tmp[1:j]) / sum(tmp[(j + 1):4])) } x[, i] <- rnorm(n) eta <- 0.6 * x[, i] p[, 1, i] <- 1 - plogis(eta - cutpoints[1, i]) p[, 2, i] <- plogis(eta - cutpoints[1, i]) - plogis(eta - cutpoints[2, i]) p[, 3, i] <- plogis(eta - cutpoints[2, i]) - plogis(eta - cutpoints[3, i]) p[, 4, i] <- plogis(eta - cutpoints[3, i]) y[, i] <- apply(p[, , i], 1, sample, x = 1:4, size = 1, replace = TRUE) } d <- data.frame( y = factor(y, levels = 1:4), x = c(x), time = rep(seq_len(t), each = n), id = rep(seq_len(n), t) ) expect_error( fit <- dynamite( dformula = obs(y ~ -1 + x + varying(~1), family = "cumulative", link = "logit") + splines(10), data = d, time = "time", group = "id" ), NA ) expect_error( plot(fit), NA ) expect_error( as.data.table(fit, types = "cutpoint"), NA ) expect_error( pred <- predict(fit, type = "response", n_draws = 10), NA ) expect_true( all(pred$y_new %in% 1:4) ) expect_error( pred <- predict(fit, type = "mean", n_draws = 10), NA ) expect_true( all(!is.na(pred[, paste0("y_mean_", 1:4)])) ) expect_error( pred <- predict(fit, type = "link", n_draws = 10), NA ) expect_true( all(!is.na(pred$y_link)) ) expect_error( pred <- fitted(fit, n_draws = 10), NA ) expect_true( all(!is.na(pred[, paste0("y_fitted_", 1:4)])) ) }) test_that("non-glm categorical fit works", { skip_if_not(run_extended_tests) skip_on_os("mac") # Seems to segfault on MacOS expect_error( mockthat::with_mock( stan_supports_categorical_logit_glm = function(...) FALSE, dynamite( dformula = obs(x ~ z + lag(x) + lag(y), family = "categorical") + obs(y ~ z + lag(x) + lag(y), family = "categorical"), data = categorical_example, time = "time", group = "id", iter = 2000, chains = 1, refresh = 0, verbose = FALSE, threads_per_chain = 2 ) ), NA ) }) test_that("get_parameter_dims works for dynamiteformula", { skip_if_not(run_extended_tests) expect_error( dims <- get_parameter_dims( x = obs(x ~ z + lag(x) + lag(y), family = "categorical") + obs(y ~ z + lag(x) + lag(y), family = "categorical"), data = categorical_example, time = "time", group = "id" ), NA ) expect_identical( dims, get_parameter_dims(categorical_example_fit) ) }) test_that("predict with random variable trials works", { skip_if_not(run_extended_tests) set.seed(1) N <- 100 T_ <- 50 y <- matrix(0, N, T_) n <- matrix(0, N, T_) for (t in 2:T_) { for (i in 1:N) { n[i, t] <- rpois(1, 5) lp <- -3.0 + 1.25 * y[i, t - 1] y[i, t] <- rbinom(1, 1 + n[i, t], plogis(lp)) } } d <- data.frame( n = c(n), y = c(y), t = rep(1:T_, each = N), id = 1:N ) f <- obs(n ~ 1, family = "poisson") + aux(integer(nn) ~ n + 1) + obs(y ~ lag(y) + trials(nn), family = "binomial") fit <- dynamite( dformula = f, data = d, time = "t", group = "id", chains = 1, iter = 2000, refresh = 0 ) expect_error(sumr <- summary(fit), NA) expect_equal(sumr$mean, c(log(5), -3.0, 1.25), tolerance = 0.1) expect_error(predict(fit, n_draws = 5), NA) }) test_that("shrinkage for splines is functional", { skip("Shrinkage feature removed at least for now.") set.seed(1) expect_error( fit <- dynamite( dformula = obs( y ~ -1 + z + varying(~ x + lag(y)) + random(~1), family = "gaussian" ) + random_spec() + splines(df = 20, shrinkage = TRUE), data = gaussian_example, time = "time", group = "id", iter = 2000, warmup = 1000, chains = 1, refresh = 0 ), NA ) expect_error( summary(fit, types = "xi"), NA ) }) test_that("update without recompile works", { skip_if_not(run_extended_tests) set.seed(0) gaussian_fit <- dynamite( dformula = obs( y ~ -1 + z + varying(~ x + lag(y)) + random(~1), family = "gaussian" ) + random_spec() + splines(df = 20), data = gaussian_example, time = "time", group = "id", iter = 2000, warmup = 1000, thin = 1, chains = 2, cores = 2, refresh = 0, save_warmup = FALSE, pars = c( "omega_alpha_1_y", "omega_raw_alpha_y", "nu_raw", "nu", "L", "sigma_nu", "a_y" ), include = FALSE ) expect_error( fit <- update( gaussian_fit, data = gaussian_example, warmup = 1000, iter = 2000, thin = 1 ), NA ) # Internal update_ function expect_error( lfo(gaussian_fit, L = 20, chains = 4, verbose_stan = FALSE), NA ) }) test_that("custom stan model works", { skip_if_not(run_extended_tests) # The same as gaussian_example_fit, but we need to refit because # the results may not be reproducible across different platforms set.seed(1) initial_fit <- dynamite( dformula = obs(y ~ -1 + z + varying(~ x + lag(y)) + random(~1), family = "gaussian") + random_spec() + splines(df = 20), data = gaussian_example, time = "time", group = "id", iter = 2000, warmup = 1000, thin = 10, chains = 2, cores = 2, refresh = 0, save_warmup = FALSE, pars = c( "omega_alpha_1_y", "omega_raw_alpha_y", "nu_raw", "nu", "L", "sigma_nu", "a_y" ), include = FALSE ) code <- get_code(initial_fit) set.seed(1) expect_error( custom_fit <- dynamite( dformula = obs(y ~ -1 + z + varying(~ x + lag(y)) + random(~1), family = "gaussian") + random_spec() + splines(df = 20), data = gaussian_example, time = "time", group = "id", iter = 2000, warmup = 1000, thin = 10, chains = 2, cores = 2, refresh = 0, save_warmup = FALSE, pars = c( "omega_alpha_1_y", "omega_raw_alpha_y", "nu_raw", "nu", "L", "sigma_nu", "a_y" ), include = FALSE, custom_stan_model = code ), NA ) expect_equal( rstan::extract(initial_fit$stanfit, permuted = FALSE), rstan::extract(custom_fit$stanfit, permuted = FALSE) ) }) test_that("dynamice works", { skip_if_not(run_extended_tests) set.seed(1) n <- 50 p <- 1000 y <- replicate(p, stats::arima.sim(list(ar = 0.7), n, sd = 0.1)) d <- data.frame(y = c(y), time = 1:n, id = rep(seq_len(p), each = n)) dmiss <- d dmiss$y[sample(seq_len(nrow(d)), size = 0.2 * nrow(d))] <- NA # Long format imputation expect_error( fit_long <- dynamice( obs(y ~ lag(y), "gaussian"), time = "time", group = "id", data = dmiss, chains = 1, refresh = 0, backend = "rstan", impute_format = "long", keep_imputed = FALSE, mice_args = list(m = 3, print = FALSE) ), NA ) # Wide format imputation expect_error( fit_wide <- dynamice( obs(y ~ lag(y), "gaussian"), time = "time", group = "id", data = dmiss, chains = 1, refresh = 0, backend = "rstan", impute_format = "wide", keep_imputed = FALSE, mice_args = list(m = 3, print = FALSE) ), NA ) # single group set.seed(1) n <- 100 y <- stats::arima.sim(list(ar = 0.7), n, sd = 0.1) d <- data.frame(y = c(y), time = 1:n) dmiss_single <- d dmiss_single$y[sample(seq_len(nrow(d)), size = 0.2 * nrow(d))] <- NA expect_error( fit_single <- dynamice( obs(y ~ lag(y), "gaussian"), time = "time", data = dmiss_single, chains = 1, refresh = 0, backend = "rstan", impute_format = "long", keep_imputed = FALSE, mice_args = list(m = 3, print = FALSE) ), NA ) }) test_that("information on >2 chains is summarized in print", { skip_if_not(run_extended_tests) set.seed(1) fit <- dynamite( dformula = obs(y ~ -1 + z + varying(~ x + lag(y)) + random(~1), family = "gaussian") + random_spec() + splines(df = 20), data = gaussian_example, time = "time", group = "id", iter = 2000, warmup = 1000, thin = 10, chains = 4, refresh = 0, save_warmup = FALSE, pars = c( "omega_alpha_1_y", "omega_raw_alpha_y", "nu_raw", "nu", "L", "sigma_nu", "a_y" ), include = FALSE ) out <- capture_all_output(print(fit)) expect_true( any( grepl( "Elapsed time (seconds) for fastest and slowest chains", out, fixed = TRUE ) ) ) }) test_that("latent factor models are identifiable", { skip_if_not(run_extended_tests) generate_data <- function(N, T_, D, alpha, mean_lambda, sd_lambda, sd_alpha) { y <- matrix(0, N, T_) x <- matrix(rnorm(N * T_), N, T_) psi <- c(splines::bs(seq_len(T_), df = D, intercept = TRUE) %*% cumsum(rnorm(D))) lambda <- rnorm(N, 0, sd_lambda) lambda <- mean_lambda + lambda - mean(lambda) a <- rnorm(N, alpha, sd_alpha) for (t in 1:T_) { y[, t] <- rnorm(N, a + lambda * psi[t] + x[, t]) } list(data = data.frame( y = c(y), x = c(x), time = rep(seq_len(T_), each = N), id = rep(seq_len(N), times = T_) ), psi = psi, lambda = lambda, mean_lambda_psi = mean(lambda) * psi, alpha = alpha, beta = 1, kappa = sd_lambda / (1 + sd_lambda), sigma_lambda = sd_lambda, sigma_alpha = sd_alpha, sigma_y = 1, tau_psi = 1, zeta = 1 + sd_lambda) } set.seed(1) sim <- generate_data( N = 100, T_ = 50, D = 20, alpha = 1, mean_lambda = 1, sd_lambda = 1, sd_alpha = 1 ) dformula <- obs(y ~ x + random(~ 1), family = "gaussian") + lfactor() + splines(20) priors <- get_priors(dformula, data = sim$data, time = "time", group = "id") priors$prior[priors$parameter != "kappa_y"] <- "normal(0, 5)" fit1 <- dynamite( dformula, priors = priors, data = sim$data, time = "time", group = "id", backend = "cmdstanr", stanc_options = list("O1"), iter_sampling = 5000, iter_warmup = 5000, parallel_chains = 4, refresh = 0, seed = 1 ) sumr1 <- as_draws(fit1) |> posterior::summarise_draws() expect_true(all(sumr1$rhat < 1.1)) expect_true(all(sumr1$ess_bulk > 500)) expect_true(all(sumr1$ess_tail > 500)) expect_equal(summary(fit1, type = "psi")$mean, sim$mean_lambda_psi, tolerance = 0.5) set.seed(2) sim <- generate_data( 100, 50, 20, alpha = 0, mean_lambda = 1, sd_lambda = 0.1, sd_alpha = 2) dformula <- obs(y ~ -1 + x + random(~ 1), family = "gaussian") + lfactor() + splines(20) priors <- get_priors(dformula, data = sim$data, time = "time", group = "id") priors$prior[priors$parameter != "kappa_y"] <- "normal(0, 5)" fit2 <- dynamite( dformula, priors = priors, data = sim$data, time = "time", group = "id", backend = "cmdstanr", stanc_options = list("O1"), iter_sampling = 5000, iter_warmup = 5000, parallel_chains = 4, refresh = 0, seed = 1 ) sumr2 <- as_draws(fit2) |> posterior::summarise_draws() expect_true(all(sumr2$rhat < 1.1)) expect_true(all(sumr2$ess_bulk > 500)) expect_true(all(sumr2$ess_tail > 500)) expect_equal(summary(fit2, type = "psi")$mean, sim$mean_lambda_psi, tolerance = 0.5) set.seed(3) sim <- generate_data( 100, 50, 20, alpha = -1, mean_lambda = 0.5, sd_lambda = 2, sd_alpha = 0) dformula <- obs(y ~ x, family = "gaussian") + lfactor() + splines(20) priors <- get_priors(dformula, data = sim$data, time = "time", group = "id") priors$prior[priors$parameter != "kappa_y"] <- "normal(0, 5)" # need some informativeness on prior of kappa (or zeta?) priors$prior[priors$parameter == "kappa_y"] <- "beta(10, 10)" fit3 <- dynamite( dformula, priors = priors, data = sim$data, time = "time", group = "id", backend = "cmdstanr", stanc_options = list("O1"), iter_sampling = 5000, iter_warmup = 5000, parallel_chains = 4, refresh = 0, seed = 1 ) sumr3 <- as_draws(fit3) |> posterior::summarise_draws() expect_true(all(sumr3$rhat < 1.1)) expect_true(all(sumr3$ess_bulk > 500)) expect_true(all(sumr3$ess_tail > 500)) expect_equal(summary(fit3, type = "psi")$mean, sim$mean_lambda_psi, tolerance = 0.5) set.seed(4) sim <- generate_data( 100, 50, 20, alpha = 0, mean_lambda = -1, sd_lambda = 0.5, sd_alpha = 0.5) dformula <- obs(y ~ x + random(~ 1), family = "gaussian") + lfactor() + splines(20) priors <- get_priors(dformula, data = sim$data, time = "time", group = "id") priors$prior[priors$parameter != "kappa_y"] <- "normal(0, 5)" fit4 <- dynamite( dformula, priors = priors, data = sim$data, time = "time", group = "id", backend = "cmdstanr", stanc_options = list("O1"), iter_sampling = 5000, iter_warmup = 5000, parallel_chains = 4, refresh = 0, seed = 1 ) sumr4 <- as_draws(fit4) |> posterior::summarise_draws() expect_true(all(sumr4$rhat < 1.1)) expect_true(all(sumr4$ess_bulk > 500)) expect_true(all(sumr4$ess_tail > 500)) expect_equal(summary(fit4, type = "psi")$mean, sim$mean_lambda_psi, tolerance = 0.5) set.seed(5) sim <- generate_data( 100, 50, 20, alpha = 1, mean_lambda = 0, sd_lambda = 0.5, sd_alpha = 1) dformula <- obs(y ~ x + random(~ 1), family = "gaussian") + lfactor(nonzero_lambda = FALSE) + splines(20) priors <- get_priors(dformula, data = sim$data, time = "time", group = "id") priors$prior[] <- "normal(0, 5)" fit5 <- dynamite( dformula, priors = priors, data = sim$data, time = "time", group = "id", backend = "cmdstanr", stanc_options = list("O1"), iter_sampling = 5000, iter_warmup = 5000, parallel_chains = 4, refresh = 0, seed = 1 ) sumr5 <- as_draws(fit5) |> posterior::summarise_draws() expect_true(all(sumr5$rhat < 1.1)) expect_true(all(sumr5$ess_bulk > 500)) expect_true(all(sumr5$ess_tail > 500)) expect_equal(summary(fit5, type = "psi")$mean, -sim$psi, tolerance = 0.5) set.seed(6) sim <- generate_data( 100, 50, 20, alpha = 1, mean_lambda = 0, sd_lambda = 0.5, sd_alpha = 0) dformula <- obs(y ~ x, family = "gaussian") + lfactor(nonzero_lambda = FALSE) + splines(20) priors <- get_priors(dformula, data = sim$data, time = "time", group = "id") priors$prior[] <- "normal(0, 5)" fit6 <- dynamite( dformula, priors = priors, data = sim$data, time = "time", group = "id", backend = "cmdstanr", stanc_options = list("O1"), iter_sampling = 5000, iter_warmup = 5000, parallel_chains = 4, refresh = 0, seed = 1 ) sumr6 <- as_draws(fit6) |> posterior::summarise_draws() expect_true(all(sumr6$rhat < 1.1)) expect_true(all(sumr6$ess_bulk > 500)) expect_true(all(sumr6$ess_tail > 500)) expect_equal(summary(fit6, type = "psi")$mean, -sim$psi, tolerance = 0.5) # Test bivariate case with nonzero_lambda set.seed(123) N <- 50 T_ <- 50 D <- 10 x <- y <- matrix(0, N, T_) psi <- matrix(NA, 2, T_) lambda_y <- rnorm(N) lambda_y <- lambda_y - mean(lambda_y) lambda_x <- rnorm(N) lambda_x <- lambda_x - mean(lambda_x) L <- t(chol(matrix(c(1, 0.7, 0.7, 1), 2, 2))) B <- t(splines::bs(seq_len(T_), df = D, intercept = TRUE)) omega <- matrix(NA, 2, D) omega[, 1] <- c(5, 2) + L %*% rnorm(2) for (i in 2:D) { omega[, i] <- omega[, i - 1] + L %*% rnorm(2) } psi[1, ] <- omega[1, ] %*% B psi[2, ] <- omega[2, ] %*% B for (t in 1:T_) { y[, t] <- rnorm(N, lambda_y * psi[1, t]) x[, t] <- rnorm(N, lambda_x * psi[2, t]) } d <- data.frame( y = c(y), x = c(x), time = rep(seq_len(T_), each = N), id = rep(seq_len(N), times = T_) ) dformula <- obs(y ~ 1, family = "gaussian") + obs(x ~ 1, family = "gaussian") + lfactor(nonzero_lambda = FALSE, flip_sign = TRUE) + splines(10) fit <- dynamite( dformula, data = d, time = "time", group = "id", backend = "cmdstanr", stanc_options = list("O1"), iter_sampling = 5000, iter_warmup = 5000, parallel_chains = 4, refresh = 0, seed = 1 ) sumr <- as_draws(fit) |> posterior::summarise_draws() expect_true(all(sumr$rhat < 1.1)) expect_true(all(sumr$ess_bulk > 500)) expect_true(all(sumr$ess_tail > 500)) })