tests/testthat/test-choice_probabilities.R

test_that("MNP probabilities can be computed", {

  ### meta settings
  set.seed(1)
  J <- 3
  P <- 3
  N <- 100
  beta <- rnorm(P)
  Sigma <- oeli::sample_covariance_matrix(dim = J, df = J, scale = diag(J) * 0.5)
  true_pars <- list("beta" = beta, "Sigma" = Sigma)

  ### normalize parameters
  Sigma <- rbind(0, cbind(0, oeli::diff_cov(Sigma, ref = 1)))
  scale <- sqrt(Sigma[2, 2])
  beta <- beta / scale
  Sigma <- Sigma / scale^2

  ### simulate data
  data <- list("X" = list(), "y" = list())
  for (n in seq_len(N)) {
    X_n <- matrix(rnorm(J * P, sd = 2), nrow = J, ncol = P)
    V_n <- X_n %*% beta
    eps_n <- oeli::rmvnorm(n = 1, mean = 0, Sigma = Sigma + 1)
    U_n <- V_n + eps_n
    y_n <- which.max(U_n)
    data[["X"]][[n]] <- X_n
    data[["y"]][[n]] <- y_n
  }
  theta_true <- c(beta, oeli::cov_to_chol(oeli::diff_cov(Sigma))[-1])
  ind_beta <- seq_len(P)
  ind_Sigma <- P + seq_len(J * (J - 1) / 2 - 1)

  ### calculate MNP probabilities
  probs <- choiceprob_mnp(
    X = data$X,
    y = data$y,
    beta = beta,
    Sigma = Sigma
  )
  checkmate::expect_numeric(
    probs, lower = 0, upper = 1, any.missing = FALSE, len = N
  )
  probs_all <- choiceprob_mnp(
    X = data$X,
    y = NULL,
    beta = beta,
    Sigma = Sigma
  )
  checkmate::expect_matrix(
    probs_all, mode = "numeric", any.missing = FALSE, nrows = N, ncols = J
  )
  expect_equal(
    probs,
    probs_all[cbind(seq_len(nrow(probs_all)), unlist(data$y))]
  )

  ### verify via MLE
  skip() # because Rprobit:::SJ is not exported
  nll <- function(theta, data) {
    beta <- theta[ind_beta]
    Sigma <- oeli::undiff_cov(oeli::chol_to_cov(c(1, theta[ind_Sigma])))
    probs <- choiceprob_mnp(
      X = data$X,
      y = data$y,
      beta = beta,
      Sigma = Sigma,
      gcdf = function(upper, corr) exp(Rprobit:::SJ(x = upper, r = corr)),
      lower_bound = 1e-6
    )
    -sum(log(probs))
  }
  out <- suppressWarnings(
    nlm(nll, theta_true, data, print.level = 0, iterlim = 100)
  )

  ### check deviation
  beta_true <- true_pars$beta
  beta_estimate <- out$estimate[ind_beta] * scale
  expect_lt(sqrt(sum(beta_true - beta_estimate)^2), 2)
  Sigma_true <- rbind(0, cbind(0, oeli::diff_cov(true_pars$Sigma, ref = 1)))
  Sigma_estimate <- oeli::undiff_cov(oeli::chol_to_cov(c(1, out$estimate[ind_Sigma]))) * scale^2
  expect_lt(sqrt(sum(Sigma_true - Sigma_estimate)^2), 2)
})

test_that("MNP ordered probabilities can be computed", {

  ### meta settings
  J <- 5
  P <- 3
  N <- 100
  beta <- rnorm(P)
  d <- rnorm(J - 2)
  # gamma_0 = -Inf, gamma_1 = 0, gamma_2, ..., gamma_J = Inf
  gamma <- c(0, cumsum(exp(d)))
  Sigma <- 2
  true_pars <- list("beta" = beta, "d" = d)

  ### normalize parameters
  scale <- sqrt(Sigma)
  beta <- beta / scale
  gamma <- gamma / scale
  Sigma <- Sigma / scale^2

  ### simulate data
  gamma_augmented <- c(-Inf, gamma, +Inf)
  data <- list("X" = list(), "y" = list())
  for (n in seq_len(N)) {
    X_n <- matrix(rnorm(P, sd = 2), nrow = 1, ncol = P)
    V_n <- as.numeric(X_n %*% beta)
    eps_n <- stats::rnorm(n = 1, mean = 0, sd = Sigma^2)
    U_n <- V_n + eps_n
    y_n <- findInterval(U_n, gamma_augmented, all.inside = TRUE, left.open = TRUE)
    data[["X"]][[n]] <- X_n
    data[["y"]][[n]] <- y_n
  }
  d <- log(diff(gamma))
  theta_true <- c(beta, d)
  ind_beta <- seq_len(P)
  ind_d <- P + seq_len(J - 2)

  ### calculate MNP ordered probabilities
  probs <- choiceprob_mnp_ordered(
    X = data$X,
    y = data$y,
    beta = beta,
    Sigma = Sigma,
    gamma = gamma
  )
  checkmate::expect_numeric(
    probs, lower = 0, upper = 1, any.missing = FALSE, len = N
  )
  probs_all <- choiceprob_mnp_ordered(
    X = data$X,
    y = NULL,
    beta = beta,
    Sigma = Sigma,
    gamma = gamma
  )
  checkmate::expect_matrix(
    probs_all, mode = "numeric", any.missing = FALSE, nrows = N, ncols = J
  )
  expect_equal(
    probs,
    probs_all[cbind(seq_len(nrow(probs_all)), unlist(data$y))]
  )
})

test_that("MNP ranked probabilities can be computed", {

  ### meta settings
  J <- 3
  P <- 3
  N <- 100
  beta <- rnorm(P)
  Sigma <- oeli::sample_covariance_matrix(dim = J, df = J, scale = diag(J) * 0.5)
  true_pars <- list("beta" = beta, "Sigma" = Sigma)

  ### normalize parameters
  Sigma <- rbind(0, cbind(0, oeli::diff_cov(Sigma, ref = 1)))
  scale <- sqrt(Sigma[2, 2])
  beta <- beta / scale
  Sigma <- Sigma / scale^2

  ### simulate data
  data <- list("X" = list(), "y" = list())
  for (n in seq_len(N)) {
    X_n <- matrix(rnorm(J * P, sd = 2), nrow = J, ncol = P)
    V_n <- X_n %*% beta
    eps_n <- oeli::rmvnorm(n = 1, mean = 0, Sigma = Sigma + 1)
    U_n <- V_n + eps_n
    y_n <- order(as.numeric(U_n), decreasing = TRUE)
    data[["X"]][[n]] <- X_n
    data[["y"]][[n]] <- y_n
  }
  theta_true <- c(beta, oeli::cov_to_chol(oeli::diff_cov(Sigma))[-1])
  ind_beta <- seq_len(P)
  ind_Sigma <- P + seq_len(J * (J - 1) / 2 - 1)

  ### calculate MNP probabilities
  probs <- choiceprob_mnp(
    X = data$X,
    y = data$y,
    beta = beta,
    Sigma = Sigma,
    ranked = TRUE
  )
  checkmate::expect_numeric(
    probs, lower = 0, upper = 1, any.missing = FALSE, len = N
  )
  probs_all <- choiceprob_mnp(
    X = data$X,
    y = NULL,
    beta = beta,
    Sigma = Sigma
  )
  checkmate::expect_matrix(
    probs_all, mode = "numeric", any.missing = FALSE, nrows = N, ncols = J
  )
})

test_that("probability evaluation helper matches direct computation", {

  data(train_choice)

  choice_effects <- choice_effects(
    choice_formula = choice_formula(
      formula = choice ~ price | time,
      error_term = "probit"
    ),
    choice_alternatives = choice_alternatives(J = 2, alternatives = c("A", "B"))
  )

  ch_data <- choice_data(
    data_frame = train_choice,
    format = "wide",
    column_choice = "choice",
    column_decider = "deciderID",
    column_occasion = "occasionID"
  )

  params <- generate_choice_parameters(choice_effects)

  direct <- compute_choice_probabilities(
    choice_parameters = params,
    choice_data = ch_data,
    choice_effects = choice_effects,
    choice_only = TRUE
  )

  identifiers <- extract_choice_identifiers(ch_data)
  design <- design_matrices(
    x = ch_data,
    choice_effects = choice_effects,
    choice_identifiers = identifiers
  )
  indices <- extract_choice_indices(
    choice_data = ch_data,
    choice_effects = choice_effects,
    choice_identifiers = identifiers
  )

  evaluated <- evaluate_choice_probabilities(
    design_list = design,
    choice_identifiers = identifiers,
    choice_effects = choice_effects,
    choice_parameters = params,
    choice_only = TRUE,
    choice_indices = indices
  )

  expect_s3_class(evaluated, "choice_probabilities")
  expect_equal(evaluated$choice_probability, direct$choice_probability)
})

test_that("evaluate_choice_probabilities validates probability row counts", {

  data(train_choice)

  choice_effects <- choice_effects(
    choice_formula = choice_formula(
      formula = choice ~ price | time,
      error_term = "probit"
    ),
    choice_alternatives = choice_alternatives(J = 2, alternatives = c("A", "B"))
  )

  ch_data <- choice_data(
    data_frame = train_choice,
    format = "wide",
    column_choice = "choice",
    column_decider = "deciderID",
    column_occasion = "occasionID"
  )

  params <- generate_choice_parameters(choice_effects)

  identifiers <- extract_choice_identifiers(ch_data)
  design <- design_matrices(
    x = ch_data,
    choice_effects = choice_effects,
    choice_identifiers = identifiers
  )
  indices <- extract_choice_indices(
    choice_data = ch_data,
    choice_effects = choice_effects,
    choice_identifiers = identifiers
  )

  truncated_identifiers <- identifiers[seq_len(nrow(identifiers) - 1), , drop = FALSE]
  attr(truncated_identifiers, "column_decider") <- attr(identifiers, "column_decider")
  attr(truncated_identifiers, "column_occasion") <- attr(identifiers, "column_occasion")
  attr(truncated_identifiers, "cross_section") <- attr(identifiers, "cross_section")

  expect_error(
    evaluate_choice_probabilities(
      design_list = design,
      choice_identifiers = truncated_identifiers,
      choice_effects = choice_effects,
      choice_parameters = params,
      choice_only = TRUE,
      choice_indices = indices
    ),
    "mismatched number of rows"
  )
})

test_that("compute_choice_probabilities returns per-alternative probit probabilities", {

  data(train_choice)

  choice_effects <- choice_effects(
    choice_formula = choice_formula(
      formula = choice ~ price | time,
      error_term = "probit"
    ),
    choice_alternatives = choice_alternatives(J = 2, alternatives = c("A", "B"))
  )

  ch_data <- choice_data(
    data_frame = train_choice,
    format = "wide",
    column_choice = "choice",
    column_decider = "deciderID",
    column_occasion = "occasionID"
  )

  params <- generate_choice_parameters(choice_effects)

  all_probabilities <- compute_choice_probabilities(
    choice_parameters = params,
    choice_data = ch_data,
    choice_effects = choice_effects,
    choice_only = FALSE
  )

  expect_s3_class(all_probabilities, "choice_probabilities")

  alternative_columns <- c("A", "B")
  expect_true(all(alternative_columns %in% colnames(all_probabilities)))

  probability_matrix <- as.matrix(all_probabilities[alternative_columns])
  expect_equal(
    unname(rowSums(probability_matrix)),
    rep(1, nrow(probability_matrix)),
    tolerance = 1e-8
  )

  choice_only_probabilities <- compute_choice_probabilities(
    choice_parameters = params,
    choice_data = ch_data,
    choice_effects = choice_effects,
    choice_only = TRUE
  )

  chosen_alternatives <- as.character(train_choice$choice)
  chosen_indices <- match(chosen_alternatives, alternative_columns)
  chosen_probabilities <- probability_matrix[cbind(seq_len(nrow(probability_matrix)), chosen_indices)]

  expect_equal(
    chosen_probabilities,
    choice_only_probabilities$choice_probability,
    tolerance = 1e-8
  )
})

test_that("MNL probabilities can be computed", {
  data(train_choice)

  choice_effects <- choice_effects(
    choice_formula = choice_formula(
      formula = choice ~ price | time,
      error_term = "logit"
    ),
    choice_alternatives = choice_alternatives(J = 2, alternatives = c("A", "B"))
  )

  ch_data <- choice_data(
    data_frame = train_choice,
    format = "wide",
    column_choice = "choice",
    column_decider = "deciderID",
    column_occasion = "occasionID"
  )

  params <- choice_parameters(
    beta = rep(0.2, nrow(choice_effects))
  )

  probs <- compute_choice_probabilities(
    choice_parameters = params,
    choice_data = ch_data,
    choice_effects = choice_effects,
    choice_only = TRUE
  )

  expect_s3_class(probs, "choice_probabilities")
  expect_true(all(probs$choice_probability >= 0))
  expect_true(all(probs$choice_probability <= 1))
})

test_that("logit ordered probabilities cover full and choice-only outputs", {

  X <- list(
    matrix(c(0.3, -0.7), nrow = 1),
    matrix(c(-0.4, 0.2), nrow = 1)
  )
  beta <- c(0.5, -0.2)
  gamma <- c(-0.25, 0.75)
  gamma_augmented <- c(-Inf, gamma, Inf)

  all_probs <- choiceprob_logit(
    X = X, beta = beta, gamma = gamma
  )

  expect_equal(nrow(all_probs), length(X))
  expect_equal(
    rowSums(all_probs),
    rep(1, length(X)),
    tolerance = 1e-10
  )

  y <- list(1L, 3L)
  chosen_probs <- choiceprob_logit(
    X = X, y = y, beta = beta, gamma = gamma
  )

  expected <- vapply(seq_along(X), function(n) {
    V_n <- as.numeric(X[[n]] %*% beta)
    upper <- stats::plogis(gamma_augmented[y[[n]] + 1] - V_n)
    lower <- stats::plogis(gamma_augmented[y[[n]]] - V_n)
    upper - lower
  }, numeric(1))

  expect_equal(chosen_probs, expected, tolerance = 1e-10)
})

test_that("logit ranked probabilities honour rankings", {

  X <- list(
    matrix(c(
      0.2, -0.1,
      -0.3, 0.4,
      0.1, 0.2
    ), nrow = 3, byrow = TRUE),
    matrix(c(
      -0.5, 0.1,
      0.4, -0.3,
      0.6, 0.2
    ), nrow = 3, byrow = TRUE)
  )
  beta <- c(0.3, -0.4)
  rankings <- list(c(2L, 1L, 3L), c(1L, 3L, 2L))

  manual_ranked_prob <- function(utilities, ranking) {
    available <- seq_along(utilities)
    prob <- 1
    for (pos in seq_along(ranking)) {
      probs <- exp(utilities[available] - max(utilities[available]))
      probs <- probs / sum(probs)
      idx <- match(ranking[pos], available)
      prob <- prob * probs[idx]
      available <- available[-idx]
    }
    prob
  }

  expected <- vapply(seq_along(X), function(n) {
    utilities <- as.numeric(X[[n]] %*% beta)
    manual_ranked_prob(utilities, rankings[[n]])
  }, numeric(1))

  probs <- choiceprob_logit(
    X = X, y = rankings, beta = beta
  )

  expect_equal(probs, expected, tolerance = 1e-10)
  expect_true(all(probs > 0 & probs <= 1))
})

test_that("logit panel probabilities respect Tp", {

  X <- list(
    matrix(c(
      0.2, -0.1,
      -0.3, 0.5
    ), nrow = 2, byrow = TRUE),
    matrix(c(
      -0.6, 0.2,
      0.4, -0.3
    ), nrow = 2, byrow = TRUE),
    matrix(c(
      0.1, 0.3,
      -0.2, -0.4
    ), nrow = 2, byrow = TRUE)
  )
  y <- list(1L, 2L, 1L)
  Tp <- c(2L, 1L)
  beta <- c(0.4, -0.2)

  per_observation <- vapply(seq_along(X), function(n) {
    utilities <- as.numeric(X[[n]] %*% beta)
    probs <- exp(utilities - max(utilities))
    probs <- probs / sum(probs)
    probs[y[[n]]]
  }, numeric(1))
  expected <- c(prod(per_observation[1:2]), per_observation[3])

  panel_probs <- choiceprob_logit(
    X = X, y = y, Tp = Tp, beta = beta
  )

  expect_equal(panel_probs, expected, tolerance = 1e-10)
  expect_true(all(panel_probs > 0 & panel_probs <= 1))
})

test_that("latent class logit probabilities combine class panels correctly", {

  X <- list(
    matrix(c(
      0.1, -0.2,
      -0.3, 0.1
    ), nrow = 2, byrow = TRUE),
    matrix(c(
      0.4, 0.2,
      -0.5, -0.1
    ), nrow = 2, byrow = TRUE),
    matrix(c(
      -0.2, 0.3,
      0.1, -0.4
    ), nrow = 2, byrow = TRUE)
  )
  y <- list(1L, 2L, 1L)
  Tp <- c(2L, 1L)
  beta <- list(c(0.25, -0.15), c(-0.35, 0.3))
  weights <- c(0.4, 0.6)

  class_probs <- vapply(seq_along(beta), function(c) {
    per_obs <- vapply(seq_along(X), function(n) {
      utilities <- as.numeric(X[[n]] %*% beta[[c]])
      probs <- exp(utilities - max(utilities))
      probs <- probs / sum(probs)
      probs[y[[n]]]
    }, numeric(1))
    c(prod(per_obs[1:2]), per_obs[3])
  }, numeric(length(Tp)))

  expected <- Reduce(
    `+`,
    Map(function(idx) weights[idx] * class_probs[, idx], seq_along(weights))
  )

  lc_probs <- choiceprob_logit(
    X = X, y = y, Tp = Tp, beta = beta, weights = weights
  )

  expect_equal(lc_probs, expected, tolerance = 1e-10)
  expect_true(all(lc_probs > 0 & lc_probs <= 1))

  ### also ensure per-alternative probabilities mix correctly
  alt_probs <- choiceprob_logit(
    X = X, beta = beta, weights = weights
  )
  expect_equal(nrow(alt_probs), length(X))
  expect_equal(
    rowSums(alt_probs),
    rep(1, length(X)),
    tolerance = 1e-10
  )
})

test_that("mixed logit probabilities average over draws", {

  X <- list(
    matrix(c(
      1, 0,
      1, 1
    ), nrow = 2, byrow = TRUE)
  )
  y <- list(1L)
  beta <- c(0.3, -0.2)
  Omega <- matrix(0.04, nrow = 1)
  draws <- matrix(sqrt(0.04) * c(-1, 0, 1), ncol = 1)

  manual_probs <- Reduce(
    `+`,
    lapply(seq_len(nrow(draws)), function(i) {
      beta_draw <- beta
      beta_draw[2] <- beta_draw[2] + draws[i, 1]
      utilities <- X[[1]] %*% beta_draw
      probs <- exp(utilities - max(utilities))
      probs <- probs / sum(probs)
      probs
    })
  ) / nrow(draws)

  chosen <- choiceprob_logit(
    X = X, y = y, beta = beta, Omega = Omega, draws = draws
  )
  expect_equal(chosen, manual_probs[1], tolerance = 1e-10)

  all_probs <- choiceprob_logit(
    X = X, beta = beta, Omega = Omega, draws = draws
  )
  expect_equal(all_probs, matrix(manual_probs, nrow = 1), tolerance = 1e-10)
  expect_equal(rowSums(all_probs), 1, tolerance = 1e-10)
})

test_that("mixed logit panel probabilities average products over draws", {

  X <- list(
    matrix(c(
      0.2, -0.1,
      -0.3, 0.5
    ), nrow = 2, byrow = TRUE),
    matrix(c(
      -0.6, 0.2,
      0.4, -0.3
    ), nrow = 2, byrow = TRUE),
    matrix(c(
      0.1, 0.3,
      -0.2, -0.4
    ), nrow = 2, byrow = TRUE)
  )
  y <- list(1L, 2L, 1L)
  Tp <- c(2L, 1L)
  beta <- c(0.4, -0.2)
  Omega <- matrix(0.01, nrow = 1)
  draws <- matrix(sqrt(0.01) * c(-1, 0.5, 1.5), ncol = 1)

  manual <- Reduce(
    `+`,
    lapply(seq_len(nrow(draws)), function(i) {
      beta_draw <- beta
      beta_draw[2] <- beta_draw[2] + draws[i, 1]
      per_obs <- vapply(seq_along(X), function(n) {
        utilities <- as.numeric(X[[n]] %*% beta_draw)
        probs <- exp(utilities - max(utilities))
        probs <- probs / sum(probs)
        probs[y[[n]]]
      }, numeric(1))
      c(prod(per_obs[1:2]), per_obs[3])
    })
  ) / nrow(draws)

  panel_probs <- choiceprob_logit(
    X = X, y = y, Tp = Tp, beta = beta,
    Omega = Omega, draws = draws
  )

  expect_equal(panel_probs, manual, tolerance = 1e-10)
  expect_true(all(panel_probs > 0 & panel_probs <= 1))
})

test_that("choice probability computation supports ordered data", {
  ordered_df <- data.frame(
    deciderID = 1:4,
    choice = factor(
      c("low", "medium", "high", "medium"),
      levels = c("low", "medium", "high"),
      ordered = TRUE
    )
  )

  ch_data <- choice_data(
    data_frame = ordered_df,
    format = "wide",
    column_choice = "choice",
    column_decider = "deciderID",
    choice_type = "ordered"
  )

  ordered_effects <- choice_effects(
    choice_formula = choice_formula(
      formula = choice ~ 0 | 0,
      error_term = "probit"
    ),
    choice_alternatives = choice_alternatives(
      J = 3,
      alternatives = c("low", "medium", "high"),
      ordered = TRUE
    )
  )

  params <- choice_parameters(
    Sigma = 1,
    gamma = c(0, 1)
  )

  probs <- compute_choice_probabilities(
    choice_parameters = params,
    choice_data = ch_data,
    choice_effects = ordered_effects,
    choice_only = TRUE
  )

  expect_s3_class(probs, "choice_probabilities")
  expect_true(all(probs$choice_probability >= 0))
})

test_that("MMNP probabilities can be computed", {

  ### meta settings
  J <- 3
  P <- 2
  N <- 1000
  beta <- rnorm(P)
  Omega <- matrix(0.5, 1, 1)
  P_r <- nrow(Omega)
  Sigma <- matrix(c(1.8, -1, -0.2, -1, 1.1, 0.4, -0.2, 0.4, 0.2), 3, 3)
  true_pars <- list("beta" = beta, "Omega" = Omega, "Sigma" = Sigma)

  ### normalize parameters
  Sigma <- rbind(0, cbind(0, oeli::diff_cov(Sigma, ref = 1)))
  scale <- sqrt(Sigma[2, 2])
  beta <- beta / scale
  Omega <- Omega / scale^2
  Sigma <- Sigma / scale^2

  ### simulate data
  data <- list("X" = list(), "y" = list())
  preferences <- list()
  Omega_completed <- matrix(0, P, P)
  Omega_completed[seq_len(P_r), seq_len(P_r)] <- Omega
  for (n in seq_len(N)) {
    preferences[[n]] <- oeli::rmvnorm(mean = beta, Sigma = Omega_completed)
  }
  for (n in seq_len(N)) {
    X_n <- matrix(rnorm(J * P, sd = 2), nrow = J, ncol = P)
    V_n <- X_n %*% preferences[[n]]
    eps_n <- oeli::rmvnorm(n = 1, mean = 0, Sigma = Sigma + 1)
    U_n <- V_n + eps_n
    y_n <- which.max(U_n)
    data[["X"]][[n]] <- X_n
    data[["y"]][[n]] <- y_n
  }
  theta_true <- c(
    beta,
    oeli::cov_to_chol(Omega),
    oeli::cov_to_chol(oeli::diff_cov(Sigma))[-1]
  )
  ind_beta <- seq_len(P)
  ind_Omega <- P + seq_len(P_r * (P_r + 1) / 2)
  ind_Sigma <- P + P_r * (P_r + 1) / 2 + (1:(J * (J - 1) / 2 - 1))

  ### calculate MMNP probabilities
  probs <- choiceprob_mmnp(
    X = data$X,
    y = data$y,
    beta = beta,
    Omega = Omega,
    Sigma = Sigma,
    re_position = seq_len(P_r)
  )
  checkmate::expect_numeric(
    probs, lower = 0, upper = 1, any.missing = FALSE, len = N
  )
  probs_all <- choiceprob_mmnp(
    X = data$X,
    y = NULL,
    beta = beta,
    Omega = Omega,
    Sigma = Sigma,
    re_position = seq_len(P_r)
  )
  checkmate::expect_matrix(
    probs_all, mode = "numeric", any.missing = FALSE, nrows = N, ncols = J
  )
})

test_that("MMNP ranked probabilities can be computed", {

  ### meta settings
  J <- 3
  P <- 2
  N <- 100
  beta <- rnorm(P)
  Omega <- matrix(0.5, 1, 1)
  P_r <- nrow(Omega)
  Sigma <- matrix(c(1.8, -1, -0.2, -1, 1.1, 0.4, -0.2, 0.4, 0.2), 3, 3)
  true_pars <- list("beta" = beta, "Omega" = Omega, "Sigma" = Sigma)

  ### normalize parameters
  Sigma <- rbind(0, cbind(0, oeli::diff_cov(Sigma, ref = 1)))
  scale <- sqrt(Sigma[2, 2])
  beta <- beta / scale
  Omega <- Omega / scale^2
  Sigma <- Sigma / scale^2

  ### simulate data
  data <- list("X" = list(), "y" = list())
  preferences <- list()
  Omega_completed <- matrix(0, P, P)
  Omega_completed[seq_len(P_r), seq_len(P_r)] <- Omega
  for (n in seq_len(N)) {
    preferences[[n]] <- oeli::rmvnorm(mean = beta, Sigma = Omega_completed)
  }
  for (n in seq_len(N)) {
    X_n <- matrix(rnorm(J * P, sd = 2), nrow = J, ncol = P)
    V_n <- X_n %*% preferences[[n]]
    eps_n <- oeli::rmvnorm(n = 1, mean = 0, Sigma = Sigma + 1)
    U_n <- V_n + eps_n
    y_n <- order(as.numeric(U_n), decreasing = TRUE)
    data[["X"]][[n]] <- X_n
    data[["y"]][[n]] <- y_n
  }
  theta_true <- c(
    beta,
    oeli::cov_to_chol(Omega),
    oeli::cov_to_chol(oeli::diff_cov(Sigma))[-1]
  )
  ind_beta <- seq_len(P)
  ind_Omega <- P + seq_len(P_r * (P_r + 1) / 2)
  ind_Sigma <- P + P_r * (P_r + 1) / 2 + (1:(J * (J - 1) / 2 - 1))

  ### calculate MMNP probabilities
  probs <- choiceprob_mmnp(
    X = data$X,
    y = data$y,
    beta = beta,
    Omega = Omega,
    Sigma = Sigma,
    re_position = seq_len(P_r),
    ranked = TRUE
  )
  checkmate::expect_numeric(
    probs, lower = 0, upper = 1, any.missing = FALSE, len = N
  )
  probs_all <- choiceprob_mmnp(
    X = data$X,
    y = NULL,
    beta = beta,
    Omega = Omega,
    Sigma = Sigma,
    re_position = seq_len(P_r),
    ranked = TRUE
  )
  checkmate::expect_matrix(
    probs_all, mode = "numeric", any.missing = FALSE, nrows = N, ncols = J
  )
})

test_that("MMNP ordered probabilities can be computed", {

  ### meta settings
  J <- 5
  P <- 3
  N <- 100
  beta <- c(-1, 0.5, 2)
  Omega <- matrix(1, 1, 1)
  P_r <- nrow(Omega)
  d <- rnorm(J - 2)
  # gamma_0 = -Inf, gamma_1 = 0, gamma_2, ..., gamma_J = Inf
  gamma <- c(0, cumsum(exp(d)))
  Sigma <- 2
  true_pars <- list("beta" = beta, "Omega" = Omega, "d" = d)

  ### normalize parameters
  scale <- sqrt(Sigma)
  beta <- beta / scale
  Omega <- Omega / scale^2
  gamma <- gamma / scale
  Sigma <- Sigma / scale^2

  ### simulate data
  gamma_augmented <- c(-Inf, gamma, +Inf)
  data <- list("X" = list(), "y" = list())
  preferences <- list()
  Omega_completed <- matrix(0, P, P)
  Omega_completed[seq_len(P_r), seq_len(P_r)] <- Omega
  for (n in seq_len(N)) {
    preferences[[n]] <- oeli::rmvnorm(mean = beta, Sigma = Omega_completed)
  }
  for (n in seq_len(N)) {
    X_n <- matrix(rnorm(P, sd = 2), nrow = 1, ncol = P)
    V_n <- as.numeric(X_n %*% preferences[[n]])
    eps_n <- stats::rnorm(n = 1, mean = 0, sd = Sigma^2)
    U_n <- V_n + eps_n
    y_n <- findInterval(U_n, gamma_augmented, all.inside = TRUE, left.open = TRUE)
    data[["X"]][[n]] <- X_n
    data[["y"]][[n]] <- y_n
  }
  d <- log(diff(gamma))
  theta_true <- c(beta, oeli::cov_to_chol(Omega), d)
  ind_beta <- seq_len(P)
  ind_Omega <- P + seq_len(P_r * (P_r + 1) / 2)
  ind_d <- P + P_r * (P_r + 1) / 2 + seq_len(J - 2)

  ### calculate MNP ordered probabilities
  probs <- choiceprob_mmnp_ordered(
    X = data$X,
    y = data$y,
    beta = beta,
    Omega = Omega,
    Sigma = Sigma,
    gamma = gamma,
    re_position = seq_len(P_r)
  )
  checkmate::expect_numeric(
    probs, lower = 0, upper = 1, any.missing = FALSE, len = N
  )
  probs_all <- choiceprob_mmnp_ordered(
    X = data$X,
    y = NULL,
    beta = beta,
    Omega = Omega,
    Sigma = Sigma,
    gamma = gamma,
    re_position = seq_len(P_r)
  )
  checkmate::expect_matrix(
    probs_all, mode = "numeric", any.missing = FALSE, nrows = N, ncols = J
  )
  expect_equal(
    probs,
    probs_all[cbind(seq_len(nrow(probs_all)), unlist(data$y))]
  )
})

test_that("MMNP latent class probabilities can be computed", {

  ### meta settings
  J <- 3
  P <- 2
  N <- 250
  weights <- c(0.35, 0.65)
  C <- length(weights)
  beta <- lapply(seq_len(C), function(c) rnorm(P))
  P_r <- 1
  Omega <- lapply(seq_len(C), function(c) matrix(runif(P_r), P_r, P_r))
  Sigma <- matrix(c(1.8, -1, -0.2, -1, 1.1, 0.4, -0.2, 0.4, 0.2), 3, 3)

  ### normalize parameters
  Sigma <- rbind(0, cbind(0, oeli::diff_cov(Sigma, ref = 1)))
  scale <- sqrt(Sigma[2, 2])
  beta <- lapply(beta, `/`, scale)
  Omega <- lapply(Omega, `/`, scale^2)
  Sigma <- Sigma / scale^2

  ### simulate data
  class_id <- sample.int(C, size = N, replace = TRUE, prob = weights)
  data <- list("X" = vector("list", length = N), "y" = vector("list", length = N))
  for (n in seq_len(N)) {
    Omega_completed <- matrix(0, P, P)
    Omega_completed[seq_len(P_r), seq_len(P_r)] <- Omega[[class_id[n]]]
    pref_n <- oeli::rmvnorm(mean = beta[[class_id[n]]], Sigma = Omega_completed)
    X_n <- matrix(rnorm(J * P, sd = 2), nrow = J, ncol = P)
    V_n <- X_n %*% pref_n
    eps_n <- oeli::rmvnorm(n = 1, mean = 0, Sigma = Sigma + diag(J))
    U_n <- V_n + eps_n
    data$X[[n]] <- X_n
    data$y[[n]] <- which.max(U_n)
  }

  ### calculate MMNP latent class probabilities
  probs <- choiceprob_mmnp_lc(
    X = data$X,
    y = data$y,
    beta = beta,
    Omega = Omega,
    Sigma = Sigma,
    weights = weights,
    re_position = seq_len(P_r)
  )
  checkmate::expect_numeric(
    probs, lower = 0, upper = 1, any.missing = FALSE, len = N
  )

  probs_all <- choiceprob_mmnp_lc(
    X = data$X,
    y = NULL,
    beta = beta,
    Omega = Omega,
    Sigma = Sigma,
    weights = weights,
    re_position = seq_len(P_r)
  )
  checkmate::expect_matrix(
    probs_all, mode = "numeric", any.missing = FALSE, nrows = N, ncols = J
  )

  expect_equal(
    probs,
    probs_all[cbind(seq_len(nrow(probs_all)), unlist(data$y))]
  )

  expect_equal(
    probs,
    choiceprob_probit(
      X = data$X,
      y = data$y,
      beta = beta,
      Omega = Omega,
      Sigma = Sigma,
      weights = weights,
      re_position = seq_len(P_r)
    )
  )
})

test_that("latent class weights are validated", {

  X <- list(matrix(c(1, 0), nrow = 2, ncol = 1))
  y <- list(1L)
  beta <- list(0.2, -0.1)
  Omega <- list(matrix(0.3, 1, 1), matrix(0.4, 1, 1))
  Sigma <- diag(2)
  base_args <- list(
    X = X,
    y = y,
    beta = beta,
    Omega = Omega,
    Sigma = Sigma,
    weights = c(0.6, 0.4),
    re_position = 1L
  )

  expect_error(do.call(choiceprob_probit, base_args), NA)

  expect_error(
    do.call(
      choiceprob_probit,
      utils::modifyList(base_args, list(weights = c(0.6, -0.2)))
    ),
    "weights"
  )
  expect_error(
    do.call(
      choiceprob_probit,
      utils::modifyList(base_args, list(weights = c(0.5, 0.25, 0.25)))
    ),
    "weights"
  )
  expect_warning(
    normalized <- do.call(
      choiceprob_probit,
      utils::modifyList(base_args, list(weights = c(3, 1)))
    ),
    "normalized"
  )
  expect_equal(
    normalized,
    do.call(
      choiceprob_probit,
      utils::modifyList(base_args, list(weights = c(0.75, 0.25)))
    )
  )
})

test_that("MMNP ordered latent class probabilities can be computed", {

  ### meta settings
  J <- 4
  P <- 3
  N <- 150
  weights <- c(0.4, 0.6)
  C <- length(weights)
  beta <- lapply(seq_len(C), function(c) rnorm(P))
  P_r <- 1
  Omega <- lapply(seq_len(C), function(c) matrix(runif(P_r), P_r, P_r))
  d <- rnorm(J - 2)
  gamma <- c(0, cumsum(exp(d)))
  Sigma <- 2

  ### normalize parameters
  scale <- sqrt(Sigma)
  beta <- lapply(beta, `/`, scale)
  Omega <- lapply(Omega, `/`, scale^2)
  gamma <- gamma / scale
  Sigma <- Sigma / scale^2
  gamma_augmented <- c(-Inf, gamma, +Inf)

  ### simulate data
  class_id <- sample.int(C, size = N, replace = TRUE, prob = weights)
  data <- list("X" = vector("list", length = N), "y" = vector("list", length = N))
  for (n in seq_len(N)) {
    Omega_completed <- matrix(0, P, P)
    Omega_completed[seq_len(P_r), seq_len(P_r)] <- Omega[[class_id[n]]]
    pref_n <- oeli::rmvnorm(mean = beta[[class_id[n]]], Sigma = Omega_completed)
    X_n <- matrix(rnorm(P, sd = 2), nrow = 1, ncol = P)
    V_n <- as.numeric(X_n %*% pref_n)
    eps_n <- stats::rnorm(n = 1, mean = 0, sd = Sigma^2)
    U_n <- V_n + eps_n
    data$X[[n]] <- X_n
    data$y[[n]] <- findInterval(
      U_n,
      gamma_augmented,
      all.inside = TRUE,
      left.open = TRUE
    )
  }

  ### calculate ordered MMNP latent class probabilities
  probs <- choiceprob_mmnp_ordered_lc(
    X = data$X,
    y = data$y,
    beta = beta,
    Omega = Omega,
    Sigma = Sigma,
    gamma = gamma,
    weights = weights,
    re_position = seq_len(P_r)
  )
  checkmate::expect_numeric(
    probs, lower = 0, upper = 1, any.missing = FALSE, len = N
  )

  probs_all <- choiceprob_mmnp_ordered_lc(
    X = data$X,
    y = NULL,
    beta = beta,
    Omega = Omega,
    Sigma = Sigma,
    gamma = gamma,
    weights = weights,
    re_position = seq_len(P_r)
  )
  checkmate::expect_matrix(
    probs_all, mode = "numeric", any.missing = FALSE, nrows = N, ncols = J
  )

  expect_equal(
    probs,
    probs_all[cbind(seq_len(nrow(probs_all)), unlist(data$y))]
  )

  expect_equal(
    probs,
    choiceprob_probit(
      X = data$X,
      y = data$y,
      beta = beta,
      Omega = Omega,
      Sigma = Sigma,
      gamma = gamma,
      weights = weights,
      re_position = seq_len(P_r)
    )
  )
})

test_that("MMNP panel probabilities can be computed", {

  ### meta settings
  J <- 3
  P <- 2
  N <- 100
  Tp <- sample.int(5, size = N, replace = TRUE)
  beta <- rnorm(P)
  Omega <- matrix(0.5, 1, 1)
  P_r <- nrow(Omega)
  Sigma <- matrix(c(1.8, -1, -0.2, -1, 1.1, 0.4, -0.2, 0.4, 0.2), 3, 3)
  true_pars <- list("beta" = beta, "Omega" = Omega, "Sigma" = Sigma)

  ### normalize parameters
  Sigma <- rbind(0, cbind(0, oeli::diff_cov(Sigma, ref = 1)))
  scale <- sqrt(Sigma[2, 2])
  beta <- beta / scale
  Omega <- Omega / scale^2
  Sigma <- Sigma / scale^2

  ### simulate data
  data <- list("X" = list(), "y" = list())
  preferences <- list()
  Omega_completed <- matrix(0, P, P)
  Omega_completed[seq_len(P_r), seq_len(P_r)] <- Omega
  for (n in seq_len(N)) {
    preferences[[n]] <- oeli::rmvnorm(mean = beta, Sigma = Omega_completed)
  }
  for (n in seq_len(N)) {
    for (t in seq_len(Tp[n])) {
      X_nt <- matrix(rnorm(J * P, sd = 2), nrow = J, ncol = P)
      V_nt <- X_nt %*% preferences[[n]]
      eps_nt <- oeli::rmvnorm(n = 1, mean = 0, Sigma = Sigma + 1)
      U_nt <- V_nt + eps_nt
      y_nt <- which.max(U_nt)
      ind <- length(data[["X"]]) + 1
      data[["X"]][[ind]] <- X_nt
      data[["y"]][[ind]] <- y_nt
    }
  }
  theta_true <- c(
    beta,
    oeli::cov_to_chol(Omega),
    oeli::cov_to_chol(oeli::diff_cov(Sigma))[-1]
  )
  ind_beta <- seq_len(P)
  ind_Omega <- P + seq_len(P_r * (P_r + 1) / 2)
  ind_Sigma <- P + P_r * (P_r + 1) / 2 + (1:(J * (J - 1) / 2 - 1))

  ### calculate MMNP probabilities
  for (cml in c("no", "fp", "ap")) {
    probs <- choiceprob_mmnp_panel(
      X = data$X,
      y = data$y,
      Tp = Tp,
      cml = cml,
      beta = beta,
      Omega = Omega,
      Sigma = Sigma,
      re_position = seq_len(P_r)
    )
    checkmate::expect_numeric(
      probs, lower = 0, upper = 1, any.missing = FALSE, len = N
    )
  }
})

test_that("MMNP ranked panel probabilities can be computed", {

  ### meta settings
  J <- 3
  P <- 2
  N <- 100
  Tp <- sample.int(5, size = N, replace = TRUE)
  beta <- rnorm(P)
  Omega <- matrix(0.5, 1, 1)
  P_r <- nrow(Omega)
  Sigma <- matrix(c(1.8, -1, -0.2, -1, 1.1, 0.4, -0.2, 0.4, 0.2), 3, 3)
  true_pars <- list("beta" = beta, "Omega" = Omega, "Sigma" = Sigma)

  ### normalize parameters
  Sigma <- rbind(0, cbind(0, oeli::diff_cov(Sigma, ref = 1)))
  scale <- sqrt(Sigma[2, 2])
  beta <- beta / scale
  Omega <- Omega / scale^2
  Sigma <- Sigma / scale^2

  ### simulate data
  data <- list("X" = list(), "y" = list())
  preferences <- list()
  Omega_completed <- matrix(0, P, P)
  Omega_completed[seq_len(P_r), seq_len(P_r)] <- Omega
  for (n in seq_len(N)) {
    preferences[[n]] <- oeli::rmvnorm(mean = beta, Sigma = Omega_completed)
  }
  for (n in seq_len(N)) {
    for (t in seq_len(Tp[n])) {
      X_nt <- matrix(rnorm(J * P, sd = 2), nrow = J, ncol = P)
      V_nt <- X_nt %*% preferences[[n]]
      eps_nt <- oeli::rmvnorm(n = 1, mean = 0, Sigma = Sigma + 1)
      U_nt <- V_nt + eps_nt
      y_nt <- order(as.numeric(U_nt), decreasing = TRUE)
      ind <- length(data[["X"]]) + 1
      data[["X"]][[ind]] <- X_nt
      data[["y"]][[ind]] <- y_nt
    }
  }
  theta_true <- c(
    beta,
    oeli::cov_to_chol(Omega),
    oeli::cov_to_chol(oeli::diff_cov(Sigma))[-1]
  )
  ind_beta <- seq_len(P)
  ind_Omega <- P + seq_len(P_r * (P_r + 1) / 2)
  ind_Sigma <- P + P_r * (P_r + 1) / 2 + (1:(J * (J - 1) / 2 - 1))

  ### calculate MMNP probabilities
  for (cml in c("no", "fp", "ap")) {
    probs <- choiceprob_mmnp_panel(
      X = data$X,
      y = data$y,
      Tp = Tp,
      cml = cml,
      beta = beta,
      Omega = Omega,
      Sigma = Sigma,
      re_position = seq_len(P_r),
      ranked = TRUE
    )
    checkmate::expect_numeric(
      probs, lower = 0, upper = 1, any.missing = FALSE, len = N
    )
  }
})

test_that("MMNP ordered panel probabilities can be computed", {

  ### meta settings
  J <- 5
  P <- 3
  N <- 100
  Tp <- sample.int(5, size = N, replace = TRUE)
  beta <- c(-1, 0.5, 2)
  Omega <- matrix(1, 1, 1)
  P_r <- nrow(Omega)
  d <- rnorm(J - 2)
  # gamma_0 = -Inf, gamma_1 = 0, gamma_2, ..., gamma_J = Inf
  gamma <- c(0, cumsum(exp(d)))
  Sigma <- 2
  true_pars <- list("beta" = beta, "Omega" = Omega, "d" = d)

  ### normalize parameters
  scale <- sqrt(Sigma)
  beta <- beta / scale
  Omega <- Omega / scale^2
  gamma <- gamma / scale
  Sigma <- Sigma / scale^2

  ### simulate data
  gamma_augmented <- c(-Inf, gamma, +Inf)
  data <- list("X" = list(), "y" = list())
  preferences <- list()
  Omega_completed <- matrix(0, P, P)
  Omega_completed[seq_len(P_r), seq_len(P_r)] <- Omega
  for (n in seq_len(N)) {
    preferences[[n]] <- oeli::rmvnorm(mean = beta, Sigma = Omega_completed)
  }
  for (n in seq_len(N)) {
    for (t in seq_len(Tp[n])) {
      X_nt <- matrix(rnorm(P, sd = 2), nrow = 1, ncol = P)
      V_nt <- as.numeric(X_nt %*% preferences[[n]])
      eps_nt <- stats::rnorm(n = 1, mean = 0, sd = Sigma^2)
      U_nt <- V_nt + eps_nt
      y_nt <- findInterval(U_nt, gamma_augmented, all.inside = TRUE, left.open = TRUE)
      ind <- length(data[["X"]]) + 1
      data[["X"]][[ind]] <- X_nt
      data[["y"]][[ind]] <- y_nt
    }
  }
  d <- log(diff(gamma))
  theta_true <- c(beta, oeli::cov_to_chol(Omega), d)
  ind_beta <- seq_len(P)
  ind_Omega <- P + seq_len(P_r * (P_r + 1) / 2)
  ind_d <- P + P_r * (P_r + 1) / 2 + seq_len(J - 2)

  ### calculate MMNP probabilities
  for (cml in c("no", "fp", "ap")) {
    probs <- choiceprob_mmnp_ordered_panel(
      X = data$X,
      y = data$y,
      Tp = Tp,
      cml = cml,
      beta = beta,
      Omega = Omega,
      Sigma = Sigma,
      gamma = gamma,
      re_position = seq_len(P_r)
    )
    checkmate::expect_numeric(
      probs, lower = 0, upper = 1, any.missing = FALSE, len = N
    )
  }
})

test_that("MMNP panel latent class probabilities can be computed", {

  ### meta settings
  J <- 3
  P <- 2
  N <- 100
  Tp <- sample.int(5, size = N, replace = TRUE)
  weights <- c(0.2, 0.3, 0.5)
  C <- length(weights)
  stopifnot(all(diff(weights) >= 0)) # ensure that weights are non-decr. for ident.
  beta <- lapply(seq_len(C), function(c) rnorm(P))
  P_r <- 1
  Omega <- lapply(seq_len(C), function(c) matrix(runif(P_r), 1, 1))
  Sigma <- matrix(c(1.8, -1, -0.2, -1, 1.1, 0.4, -0.2, 0.4, 0.2), 3, 3)
  true_pars <- list(
    "beta" = beta, "Omega" = Omega, "Sigma" = Sigma, "weights" = weights
  )

  ### normalize parameters
  Sigma <- rbind(0, cbind(0, oeli::diff_cov(Sigma, ref = 1)))
  scale <- sqrt(Sigma[2, 2])
  beta <- lapply(beta, `/`, scale)
  Omega <- lapply(Omega, `/`, scale^2)
  Sigma <- Sigma / scale^2

  ### simulate data
  data <- list("X" = list(), "y" = list())
  preferences <- list()
  for (n in seq_len(N)) {
    class_n <- sample.int(C, size = 1, prob = weights)
    Omega_completed <- matrix(0, P, P)
    Omega_completed[seq_len(P_r), seq_len(P_r)] <- Omega[[class_n]]
    preferences[[n]] <- oeli::rmvnorm(mean = beta[[class_n]], Sigma = Omega_completed)
  }
  for (n in seq_len(N)) {
    for (t in seq_len(Tp[n])) {
      X_nt <- matrix(rnorm(J * P, sd = 2), nrow = J, ncol = P)
      V_nt <- X_nt %*% preferences[[n]]
      eps_nt <- oeli::rmvnorm(n = 1, mean = 0, Sigma = Sigma + 1)
      U_nt <- V_nt + eps_nt
      y_nt <- which.max(U_nt)
      ind <- length(data[["X"]]) + 1
      data[["X"]][[ind]] <- X_nt
      data[["y"]][[ind]] <- y_nt
    }
  }
  weights_uncon_to_con <- function(weights_uncon) {
    ew <- exp(weights_uncon)
    c(1 / (1 + sum(ew)), ew / (1 + sum(ew)))
  }
  weights_con_to_uncon <- function(weights_con) {
    log(weights_con[-1] / weights_con[1])
  }
  theta_true <- c(
    unlist(beta),
    sapply(Omega, oeli::cov_to_chol),
    oeli::cov_to_chol(oeli::diff_cov(Sigma))[-1],
    weights_con_to_uncon(weights)
  )
  ind_beta <- seq_len(P * C)
  ind_Omega <- P * C + seq_len(C * P_r * (P_r + 1) / 2)
  ind_Sigma <- P * C + C * P_r * (P_r + 1) / 2 + (1:(J * (J - 1) / 2 - 1))
  ind_weights <- P * C + C * P_r * (P_r + 1) / 2 + J * (J - 1) / 2 - 1 + (1:(C - 1))

  ### calculate MMNP probabilities
  for (cml in c("no", "fp", "ap")) {
    probs <- choiceprob_mmnp_panel_lc(
      X = data$X,
      y = data$y,
      Tp = Tp,
      cml = cml,
      beta = beta,
      Omega = Omega,
      Sigma = Sigma,
      weights = weights,
      re_position = seq_len(P_r)
    )
    checkmate::expect_numeric(
      probs, lower = 0, upper = 1, any.missing = FALSE, len = N
    )
  }
})

test_that("latent class panel inputs are validated", {

  Tp <- c(2)
  X <- rep(list(matrix(c(1, 0), nrow = 2, ncol = 1)), sum(Tp))
  y <- list(1L, 2L)
  beta <- list(0.2, -0.1)
  Omega <- list(matrix(0.3, 1, 1), matrix(0.4, 1, 1))
  Sigma <- diag(2)
  base_args <- list(
    X = X,
    y = y,
    Tp = Tp,
    cml = "no",
    beta = beta,
    Omega = Omega,
    Sigma = Sigma,
    weights = c(0.6, 0.4),
    re_position = 1L
  )

  expect_error(do.call(choiceprob_probit, base_args), NA)

  expect_error(
    do.call(
      choiceprob_probit,
      utils::modifyList(base_args, list(Tp = c(3)))
    ),
    "Sum of"
  )
})

test_that("MMNP ordered panel latent class probabilities can be computed", {

  ### meta settings
  J <- 3
  P <- 2
  N <- 100
  Tp <- sample.int(5, size = N, replace = TRUE)
  weights <- c(0.2, 0.3, 0.5)
  C <- length(weights)
  stopifnot(all(diff(weights) >= 0)) # ensure that weights are non-decr. for ident.
  beta <- lapply(seq_len(C), function(c) rnorm(P))
  P_r <- 1
  Omega <- lapply(seq_len(C), function(c) matrix(runif(P_r), 1, 1))
  d <- rnorm(J - 2)
  # gamma_0 = -Inf, gamma_1 = 0, gamma_2, ..., gamma_J = Inf
  gamma <- c(0, cumsum(exp(d)))
  Sigma <- 2
  true_pars <- list(
    "beta" = beta, "Omega" = Omega, "d" = d, "weights" = weights
  )

  ### normalize parameters
  scale <- sqrt(Sigma)
  beta <- lapply(beta, `/`, scale)
  Omega <- lapply(Omega, `/`, scale^2)
  gamma <- gamma / scale
  Sigma <- Sigma / scale^2

  ### simulate data
  gamma_augmented <- c(-Inf, gamma, +Inf)
  data <- list("X" = list(), "y" = list())
  preferences <- list()
  for (n in seq_len(N)) {
    class_n <- sample.int(C, size = 1, prob = weights)
    Omega_completed <- matrix(0, P, P)
    Omega_completed[seq_len(P_r), seq_len(P_r)] <- Omega[[class_n]]
    preferences[[n]] <- oeli::rmvnorm(mean = beta[[class_n]], Sigma = Omega_completed)
  }
  for (n in seq_len(N)) {
    for (t in seq_len(Tp[n])) {
      X_nt <- matrix(rnorm(P, sd = 2), nrow = 1, ncol = P)
      V_nt <- as.numeric(X_nt %*% preferences[[n]])
      eps_nt <- stats::rnorm(n = 1, mean = 0, sd = Sigma^2)
      U_nt <- V_nt + eps_nt
      y_nt <- findInterval(U_nt, gamma_augmented, all.inside = TRUE, left.open = TRUE)
      ind <- length(data[["X"]]) + 1
      data[["X"]][[ind]] <- X_nt
      data[["y"]][[ind]] <- y_nt
    }
  }
  d <- log(diff(gamma))
  weights_uncon_to_con <- function(weights_uncon) {
    ew <- exp(weights_uncon)
    c(1 / (1 + sum(ew)), ew / (1 + sum(ew)))
  }
  weights_con_to_uncon <- function(weights_con) {
    log(weights_con[-1] / weights_con[1])
  }
  theta_true <- c(
    unlist(beta),
    sapply(Omega, oeli::cov_to_chol),
    d,
    weights_con_to_uncon(weights)
  )
  ind_beta <- seq_len(P * C)
  ind_Omega <- P * C + seq_len(C * P_r * (P_r + 1) / 2)
  ind_d <- P * C + C * P_r * (P_r + 1) / 2 + seq_len(J - 2)
  ind_weights <- P * C + C * P_r * (P_r + 1) / 2 + J - 2 + (1:(C - 1))

  ### calculate MMNP probabilities
  for (cml in c("no", "fp", "ap")) {
    probs <- choiceprob_mmnp_ordered_panel_lc(
      X = data$X,
      y = data$y,
      Tp = Tp,
      cml = cml,
      beta = beta,
      Omega = Omega,
      Sigma = Sigma,
      gamma = gamma,
      weights = weights,
      re_position = seq_len(P_r)
    )
    checkmate::expect_numeric(
      probs, lower = 0, upper = 1, any.missing = FALSE, len = N
    )
  }
})

test_that("default Gaussian CDF helper relies on covariance matrices", {

  skip_if_not_installed("Matrix")

  corr <- matrix(c(1, 0.3, 0.3, 1), nrow = 2)
  upper <- c(0.5, -0.2)
  expect_equal(
    as.numeric(choicedata:::pmvnorm_cdf_default(upper = upper, corr = corr)),
    as.numeric(mvtnorm::pmvnorm(
      upper = upper, sigma = corr, algorithm = mvtnorm::GenzBretz()
    ))
  )

  expect_equal(
    as.numeric(choicedata:::pmvnorm_cdf_default(
      upper = 0.7,
      corr = Matrix::Matrix(1)
    )),
    stats::pnorm(0.7)
  )

  expect_identical(
    choicedata:::pmvnorm_cdf_default(
      upper = numeric(),
      corr = matrix(numeric(0), nrow = 0)
    ),
    1
  )
})

test_that("panel helper utilities cover edge cases", {

  expect_error(choicedata:::build_panel_chunks(Tp_n = 2, cml_type = 3L))

  expect_identical(
    choicedata:::compute_chunk_product(
      upper = numeric(),
      corr = matrix(numeric(0), nrow = 0),
      gcdf = function(...) 0,
      lower_bound = 0,
      chunk_indices = list()
    ),
    1
  )
})

test_that("choiceprob_probit validates random effect covariance inputs", {

  X <- list(matrix(c(1, 0, 0, 1), nrow = 2, byrow = TRUE))
  y <- list(1L)
  beta <- c(0.1, -0.1)
  bad_Omega <- matrix(c(1, 2, 3, 4), nrow = 2)
  Sigma <- diag(2)

  expect_error(
    choiceprob_probit(
      X = X,
      y = y,
      beta = beta,
      Omega = bad_Omega,
      Sigma = Sigma
    ),
    "Omega",
    fixed = FALSE
  )

  good_Omega <- diag(2)

  expect_error(
    choiceprob_probit(
      X = X,
      y = y,
      beta = beta,
      Omega = good_Omega,
      Sigma = Sigma
    ),
    NA
  )
})

test_that("choiceprob_probit validates random effect positions", {

  X <- list(matrix(c(1, 0, 0, 1), nrow = 2, byrow = TRUE))
  y <- list(1L)
  beta <- c(0.2, -0.2)
  Omega <- diag(2)
  Sigma <- diag(2)

  expect_error(
    choiceprob_probit(
      X = X,
      y = y,
      beta = beta,
      Omega = Omega,
      Sigma = Sigma,
      re_position = c(1L, 1L)
    ),
    "Random effect positions",
    fixed = FALSE
  )

  expect_error(
    choiceprob_probit(
      X = X,
      y = y,
      beta = beta,
      Omega = Omega,
      Sigma = Sigma,
      re_position = c(1L, 2L)
    ),
    NA
  )
})

Try the choicedata package in your browser

Any scripts or data that you put into this service are public.

choicedata documentation built on Nov. 5, 2025, 5:46 p.m.