R/S3print.R

Defines functions print.data.TIRT print.data.FCGGUM print.data.FCGDINA print.data.FCDCM print.data.FCMIRT print.data.MGGUM print.data.MGPCM print.data.MIRT .print_data_common .fc_print_data_header .gof_print_legacy_summary .gof_print_metric_table .gof_print_overview print.summary.TIRT print.TIRT print.summary.FCGGUM print.FCGGUM print.summary.FCGDINA print.FCGDINA print.summary.FCDCM print.FCDCM print.summary.FCMIRT print.FCMIRT print.summary.MGGUM print.MGGUM print.summary.MGPCM print.MGPCM print.summary.MIRT print.MIRT print.summary.good.of.fit print.good.of.fit .fc_print_convergence .fc_print_matrix .fc_print_header

Documented in print.data.FCDCM print.data.FCGDINA print.data.FCGGUM print.data.FCMIRT print.data.MGGUM print.data.MGPCM print.data.MIRT print.data.TIRT print.FCDCM print.FCGDINA print.FCGGUM print.FCMIRT print.good.of.fit print.MGGUM print.MGPCM print.MIRT print.summary.FCDCM print.summary.FCGDINA print.summary.FCGGUM print.summary.FCMIRT print.summary.good.of.fit print.summary.MGGUM print.summary.MGPCM print.summary.MIRT print.summary.TIRT print.TIRT

#' @title S3 Methods: print
#'
#' @description
#' Provides user-friendly, formatted console output for objects generated by
#' the \pkg{ForceChoice} package. This generic function dispatches to
#' class-specific methods that display concise summaries of fitted models,
#' simulation datasets, goodness-of-fit indices, and rotated solutions.
#' Designed for interactive use and quick diagnostic inspection.
#'
#' @param x An object of one of the following classes:
#'   \itemize{
#'     \item Fitted model objects: \code{\link[ForceChoice]{fit.MIRT}}
#'       (\code{"MIRT"}), \code{\link[ForceChoice]{fit.MGPCM}}
#'       (\code{"MGPCM"}), \code{\link[ForceChoice]{fit.MGGUM}}
#'       (\code{"MGGUM"}), \code{\link[ForceChoice]{fit.FCMIRT}}
#'       (\code{"FCMIRT"}), \code{\link[ForceChoice]{fit.FCDCM}}
#'       (\code{"FCDCM"}), \code{\link[ForceChoice]{fit.FCGGUM}}
#'       (\code{"FCGGUM"}), \code{\link[ForceChoice]{fit.TIRT}}
#'       (\code{"TIRT"})
#'     \item Goodness-of-fit objects: \code{\link[ForceChoice]{good.of.fit}}
#'       (\code{"good.of.fit"})
#'     \item Summary objects: \code{summary.MIRT}, \code{summary.MGPCM},
#'       \code{summary.MGGUM}, \code{summary.FCMIRT}, \code{summary.FCDCM},
#'       \code{summary.FCGGUM}, \code{summary.TIRT},
#'       \code{summary.good.of.fit}
#'   }
#' @param ... Additional arguments passed to methods (currently ignored in
#'   most cases).
#' @param digits Number of decimal places for numeric output (default: 4).
#'   Used by \code{print.good.of.fit}.
#'
#' @return Invisibly returns the input object \code{x}. No data is modified.
#'
#' @details
#' Each method produces a structured, human-readable summary optimized for
#' its object type:
#'
#' \describe{
#'   \item{\strong{Fitted Model Objects}}{
#'     Invokes \code{summary()} internally and prints comprehensive output
#'     including:
#'     \itemize{
#'       \item Model call and configuration (family, method, dimensions)
#'       \item Data characteristics (N, I or B, response type)
#'       \item Fit statistics (LogLik, npar, AIC, BIC)
#'       \item Factor correlation matrix
#'       \item Person parameter summary (N x D)
#'       \item Item / statement / block parameter summary
#'       \item For FCDCM: higher-order delta parameters and classification
#'             diagnostics
#'       \item For TIRT: loading (lambda) parameter summary
#'       \item Convergence diagnostics (algorithm, batches, latent grid length L)
#'     }
#'   }
#'
#'   \item{\strong{Goodness-of-Fit Objects (\code{good.of.fit})}}{
#'     Delegates to \code{summary.good.of.fit} and prints:
#'     \itemize{
#'       \item Overview table (sample size, items, parameters, moments)
#'       \item Limited-information absolute fit (M2, RMSEA, SRMSR)
#'       \item Incremental / comparative fit (CFI, TLI, IFI)
#'       \item Likelihood and information criteria (AIC, BIC, SABIC, CAIC)
#'       \item Pseudo-\eqn{R^2} measures
#'     }
#'   }
#' }
#'
#' @section Output Conventions:
#' \itemize{
#'   \item Numeric values are rounded to the precision specified by
#'         \code{digits} (default 4).
#'   \item Parameter summary tables show Min, Q1, Median, Mean, Q3, Max
#'         across items/statements for each parameter type.
#'   \item Convergence diagnostics adapt to the estimation backend
#'         (iStEM batch diagnostics vs. Stan HMC notes).
#' }
#'
#' @name print
NULL

# ===========================================================================
# Internal display helpers
# ===========================================================================

.fc_print_header <- function(title) {
  cat("\n==============================================\n")
  cat(title, "\n", sep = "")
  cat("==============================================\n\n")
}

.fc_print_matrix <- function(mat, digits, indent = "  ") {
  if (is.null(mat) || length(mat) == 0L) return(invisible(NULL))
  m <- as.matrix(mat)
  if (nrow(m) == 0L || ncol(m) == 0L) return(invisible(NULL))
  fm <- format(round(m, digits), nsmall = digits, justify = "right")
  rn <- rownames(m)
  cn <- colnames(m)

  # ---- compute column widths for proper alignment ----
  row_label_width <- if (!is.null(rn)) max(nchar(rn), na.rm = TRUE) else 0L
  if (row_label_width < 5L) row_label_width <- 5L

  col_widths <- integer(ncol(m))
  if (!is.null(cn)) {
    col_widths <- pmax(col_widths, nchar(cn))
  }
  for (j in seq_len(ncol(m))) {
    col_widths[j] <- max(col_widths[j], max(nchar(fm[, j]), na.rm = TRUE))
  }
  # ensure minimum width of 5 per column
  col_widths <- pmax(col_widths, 5L)

  # ---- print column header ----
  if (!is.null(cn)) {
    header <- sprintf("%-*s", row_label_width, "")
    for (j in seq_len(ncol(m))) {
      header <- paste0(header, "  ", sprintf("%*s", col_widths[j], cn[j]))
    }
    cat(indent, header, "\n", sep = "")
  }

  # ---- print separator line ----
  sep_line <- paste0(indent, paste(rep(" ", row_label_width), collapse = ""))
  for (j in seq_len(ncol(m))) {
    sep_line <- paste0(sep_line, "  ", paste(rep("-", col_widths[j]), collapse = ""))
  }
  cat(sep_line, "\n", sep = "")

  # ---- print data rows ----
  for (i in seq_len(nrow(m))) {
    row_label <- if (!is.null(rn)) rn[i] else ""
    line <- sprintf("%s%-*s", indent, row_label_width, row_label)
    for (j in seq_len(ncol(m))) {
      line <- paste0(line, "  ", sprintf("%*s", col_widths[j], fm[i, j]))
    }
    cat(line, "\n", sep = "")
  }
}

.fc_print_convergence <- function(conv) {
  if (is.null(conv) || length(conv) == 0L) return(invisible(NULL))
  cat("\nConvergence:\n")
  cat(sprintf("  Algorithm: %s\n", conv$algorithm))
  if (!is.null(conv$note)) {
    cat(sprintf("  Note: %s\n", conv$note))
  }
  if (!is.null(conv$burn_in_batches)) {
    cat(sprintf("  Burn-in Batches: %s\n", conv$burn_in_batches))
  }
  if (!is.null(conv$total_batches)) {
    cat(sprintf("  Total Batches: %s\n", conv$total_batches))
  }
  if (!is.null(conv$iterations)) {
    cat(sprintf("  Iterations: %s\n", conv$iterations))
  }
  if (!is.null(conv$burnin_converged)) {
    cat(sprintf("  Burn-in Converged: %s\n", as.character(conv$burnin_converged)))
  }
  if (!is.null(conv$final_converged)) {
    cat(sprintf("  Final Converged: %s\n", as.character(conv$final_converged)))
  }
  if (!is.null(conv$final_logLik) && is.finite(conv$final_logLik)) {
    cat(sprintf("  Final LogLik: %.4f\n", conv$final_logLik))
  }
  if (!is.null(conv$final_ll_change) && is.finite(conv$final_ll_change)) {
    cat(sprintf("  Final LogLik Change: %.6f\n", conv$final_ll_change))
  }
  if (!is.null(conv$final_max_delta_change) &&
      is.finite(conv$final_max_delta_change)) {
    cat(sprintf("  Final Max Delta Change: %.6f\n",
                conv$final_max_delta_change))
  }
  if (!is.null(conv$bootstrap_R) && conv$bootstrap_R > 0L) {
    cat(sprintf("  Bootstrap SE: %s / %s successful\n",
                conv$bootstrap_successful, conv$bootstrap_R))
  }
  if (!is.null(conv$L) && is.finite(conv$L)) {
    cat(sprintf("  Theta Grid L: %d\n", as.integer(conv$L)))
  }
}

# ===========================================================================
# Model print methods
#
# Convention: print.<Class>(x, ...) delegates to
# print.summary.<Class>(summary(x, ...)). The actual formatting is in the
# print.summary.<Class> methods below.
# ===========================================================================

# ---- good.of.fit ----

#' @describeIn print Print method for \code{good.of.fit} objects.
#'   Delegates to \code{summary.good.of.fit} and prints formatted tables.
#' @export
print.good.of.fit <- function(x, digits = 4, ...) {
  summ <- summary(x, digits = digits, ...)
  print(summ, ...)
  invisible(x)
}

#' @describeIn print Print method for \code{summary.good.of.fit} objects.
#'   Displays overview, absolute-fit, comparative-fit, likelihood/IC,
#'   and pseudo-\eqn{R^2} tables.
#' @export
print.summary.good.of.fit <- function(x, ...) {
  cat("\n")
  cat("Goodness-of-Fit Summary\n")
  cat("=======================\n")

  if (is.null(x$tables)) {
    return(.gof_print_legacy_summary(x))
  }

  if (!is.null(x$tables$Overview)) {
    .gof_print_overview(x$tables$Overview)
  }

  section.labels <- c(
    Absolute_Fit_M2        = "1. Limited-information Absolute Fit",
    Comparative_Fit        = "2. Incremental / Comparative Fit",
    Likelihood_and_IC      = "3. Likelihood & Information Criteria",
    Pseudo_R2              = "4. Pseudo-R2"
  )

  for (nm in names(section.labels)) {
    tab <- x$tables[[nm]]
    if (!is.null(tab) && nrow(tab) > 0L) {
      .gof_print_metric_table(section.labels[[nm]], tab)
    }
  }

  cat("Note: guide values are descriptive heuristics, not decision rules.\n")
  invisible(x)
}

# ---- MIRT ----

#' @describeIn print Print method for \code{MIRT} objects.
#'   Multidimensional IRT (1PL--4PL) model summary.
#' @export
print.MIRT <- function(x, ...) {
  print.summary.MIRT(summary(x, ...))
  invisible(x)
}

#' @describeIn print Print method for \code{summary.MIRT} objects.
#'   Shows model config, data info, fit stats, factor correlations,
#'   person/item parameter summaries, and convergence.
#' @export
print.summary.MIRT <- function(x, ...) {
  d <- x$digits
  .fc_print_header("MULTIDIMENSIONAL IRT (MIRT) MODEL SUMMARY")

  cat("Call:\n  ", paste(deparse(x$call), collapse = "\n   "), "\n", sep = "")

  cat("\nModel Configuration:\n")
  cat(sprintf("  Family: %s\n", x$model.info$family))
  cat(sprintf("  Model: %s\n", x$model.info$model))
  cat(sprintf("  Dimensions (D): %d\n", x$model.info$D))
  cat(sprintf("  Estimation Method: %s\n", x$model.info$method))

  cat("\nData Characteristics:\n")
  cat(sprintf("  Sample Size (N): %d\n", x$data.info$N))
  cat(sprintf("  Items (I): %d\n", x$data.info$I))
  cat(sprintf("  Response Type: %s\n", x$data.info$response.type))

  cat("\nFit Statistics:\n")
  cat(sprintf("  Log-likelihood: %.*f\n", d, x$fit.stats$LogLik))
  cat(sprintf("  Free Parameters: %d\n", x$fit.stats$npar))
  cat(sprintf("  AIC: %.*f\n", d, x$fit.stats$AIC))
  cat(sprintf("  BIC: %.*f\n", d, x$fit.stats$BIC))

  if (!is.null(x$Corr)) {
    cat("\nFactor Correlation Matrix:\n")
    .fc_print_matrix(x$Corr, d)
  }

  if (!is.null(x$theta.summary)) {
    cat("\nPerson Parameter Summary (N x D):\n")
    .fc_print_matrix(x$theta.summary, d)
  }

  if (!is.null(x$par.summary)) {
    cat("\nItem Parameter Summary:\n")
    .fc_print_matrix(x$par.summary, d)
  }

  if (!is.null(x$convergence)) {
    .fc_print_convergence(x$convergence)
  }

  cat("\nUse get.fit.index() to compute goodness-of-fit indices.\n\n")
  invisible(x)
}

# ---- MGPCM ----

#' @describeIn print Print method for \code{MGPCM} objects.
#'   Multidimensional Generalized Partial Credit Model summary.
#' @export
print.MGPCM <- function(x, ...) {
  print.summary.MGPCM(summary(x, ...))
  invisible(x)
}

#' @describeIn print Print method for \code{summary.MGPCM} objects.
#'   Shows polytomous model config, fit stats, factor correlations,
#'   person/item parameter summaries, and convergence.
#' @export
print.summary.MGPCM <- function(x, ...) {
  d <- x$digits
  .fc_print_header("MULTIDIMENSIONAL GENERALIZED PARTIAL CREDIT MODEL (MGPCM) SUMMARY")

  cat("Call:\n  ", paste(deparse(x$call), collapse = "\n   "), "\n", sep = "")

  cat("\nModel Configuration:\n")
  cat(sprintf("  Family: %s\n", x$model.info$family))
  cat(sprintf("  Dimensions (D): %d\n", x$model.info$D))
  cat(sprintf("  Estimation Method: %s\n", x$model.info$method))

  cat("\nData Characteristics:\n")
  cat(sprintf("  Sample Size (N): %d\n", x$data.info$N))
  cat(sprintf("  Items (I): %d\n", x$data.info$I))
  cat(sprintf("  Response Type: %s\n", x$data.info$response.type))

  cat("\nFit Statistics:\n")
  cat(sprintf("  Log-likelihood: %.*f\n", d, x$fit.stats$LogLik))
  cat(sprintf("  Free Parameters: %d\n", x$fit.stats$npar))
  cat(sprintf("  AIC: %.*f\n", d, x$fit.stats$AIC))
  cat(sprintf("  BIC: %.*f\n", d, x$fit.stats$BIC))

  if (!is.null(x$Corr)) {
    cat("\nFactor Correlation Matrix:\n")
    .fc_print_matrix(x$Corr, d)
  }

  if (!is.null(x$theta.summary)) {
    cat("\nPerson Parameter Summary (N x D):\n")
    .fc_print_matrix(x$theta.summary, d)
  }

  if (!is.null(x$par.summary)) {
    cat("\nItem Parameter Summary:\n")
    .fc_print_matrix(x$par.summary, d)
  }

  if (!is.null(x$convergence)) {
    .fc_print_convergence(x$convergence)
  }

  cat("\nUse get.fit.index() to compute goodness-of-fit indices.\n\n")
  invisible(x)
}

# ---- MGGUM ----

#' @describeIn print Print method for \code{MGGUM} objects.
#'   Multidimensional Generalized Graded Unfolding Model summary.
#' @export
print.MGGUM <- function(x, ...) {
  print.summary.MGGUM(summary(x, ...))
  invisible(x)
}

#' @describeIn print Print method for \code{summary.MGGUM} objects.
#'   Shows unfolding model config, fit stats, factor correlations,
#'   person/item parameter summaries, and convergence.
#' @export
print.summary.MGGUM <- function(x, ...) {
  d <- x$digits
  .fc_print_header("MULTIDIMENSIONAL GENERALIZED GRADED UNFOLDING MODEL (MGGUM) SUMMARY")

  cat("Call:\n  ", paste(deparse(x$call), collapse = "\n   "), "\n", sep = "")

  cat("\nModel Configuration:\n")
  cat(sprintf("  Family: %s\n", x$model.info$family))
  cat(sprintf("  Dimensions (D): %d\n", x$model.info$D))
  cat(sprintf("  Estimation Method: %s\n", x$model.info$method))

  cat("\nData Characteristics:\n")
  cat(sprintf("  Sample Size (N): %d\n", x$data.info$N))
  cat(sprintf("  Items (I): %d\n", x$data.info$I))
  cat(sprintf("  Response Type: %s\n", x$data.info$response.type))

  cat("\nFit Statistics:\n")
  cat(sprintf("  Log-likelihood: %.*f\n", d, x$fit.stats$LogLik))
  cat(sprintf("  Free Parameters: %d\n", x$fit.stats$npar))
  cat(sprintf("  AIC: %.*f\n", d, x$fit.stats$AIC))
  cat(sprintf("  BIC: %.*f\n", d, x$fit.stats$BIC))

  if (!is.null(x$Corr)) {
    cat("\nFactor Correlation Matrix:\n")
    .fc_print_matrix(x$Corr, d)
  }

  if (!is.null(x$theta.summary)) {
    cat("\nPerson Parameter Summary (N x D):\n")
    .fc_print_matrix(x$theta.summary, d)
  }

  if (!is.null(x$par.summary)) {
    cat("\nItem Parameter Summary:\n")
    .fc_print_matrix(x$par.summary, d)
  }

  if (!is.null(x$convergence)) {
    .fc_print_convergence(x$convergence)
  }

  cat("\nUse get.fit.index() to compute goodness-of-fit indices.\n\n")
  invisible(x)
}

# ---- FCMIRT ----

#' @describeIn print Print method for \code{FCMIRT} objects.
#'   Forced-Choice Multidimensional IRT model summary.
#' @export
print.FCMIRT <- function(x, ...) {
  print.summary.FCMIRT(summary(x, ...))
  invisible(x)
}

#' @describeIn print Print method for \code{summary.FCMIRT} objects.
#'   Shows forced-choice model config with block info, fit stats,
#'   factor correlations, person/statement parameter summaries.
#' @export
print.summary.FCMIRT <- function(x, ...) {
  d <- x$digits
  .fc_print_header("FORCED-CHOICE MULTIDIMENSIONAL IRT (FCMIRT) MODEL SUMMARY")

  cat("Call:\n  ", paste(deparse(x$call), collapse = "\n   "), "\n", sep = "")

  cat("\nModel Configuration:\n")
  cat(sprintf("  Family: %s\n", x$model.info$family))
  cat(sprintf("  Model: %s\n", x$model.info$model))
  cat(sprintf("  Dimensions (D): %d\n", x$model.info$D))
  cat(sprintf("  Estimation Method: %s\n", x$model.info$method))
  cat(sprintf("  FC Type: %s\n", x$model.info$fc.type))

  cat("\nData Characteristics:\n")
  cat(sprintf("  Sample Size (N): %d\n", x$data.info$N))
  cat(sprintf("  Blocks (B): %d\n", x$data.info$B))
  cat(sprintf("  Total Statements (I): %d\n", x$data.info$I))
  cat(sprintf("  Response Type: %s\n", x$data.info$response.type))

  cat("\nFit Statistics:\n")
  cat(sprintf("  Log-likelihood: %.*f\n", d, x$fit.stats$LogLik))
  cat(sprintf("  Free Parameters: %d\n", x$fit.stats$npar))
  cat(sprintf("  AIC: %.*f\n", d, x$fit.stats$AIC))
  cat(sprintf("  BIC: %.*f\n", d, x$fit.stats$BIC))

  if (!is.null(x$Corr)) {
    cat("\nFactor Correlation Matrix:\n")
    .fc_print_matrix(x$Corr, d)
  }

  if (!is.null(x$theta.summary)) {
    cat("\nPerson Parameter Summary (N x D):\n")
    .fc_print_matrix(x$theta.summary, d)
  }

  if (!is.null(x$par.summary)) {
    cat("\nStatement Parameter Summary:\n")
    .fc_print_matrix(x$par.summary, d)
  }

  if (!is.null(x$convergence)) {
    .fc_print_convergence(x$convergence)
  }

  cat("\nUse get.fit.index() to compute goodness-of-fit indices.\n\n")
  invisible(x)
}

# ---- FCDCM ----

#' @describeIn print Print method for \code{FCDCM} objects.
#'   Forced-Choice Diagnostic Classification Model summary with
#'   higher-order trait, attribute profiles, and classification diagnostics.
#' @export
print.FCDCM <- function(x, ...) {
  print.summary.FCDCM(summary(x, ...))
  invisible(x)
}

#' @describeIn print Print method for \code{summary.FCDCM} objects.
#'   Shows higher-order delta parameters, block eta parameters,
#'   attribute classification summary, and convergence.
#' @export
print.summary.FCDCM <- function(x, ...) {
  d <- x$digits
  .fc_print_header("FORCED-CHOICE DIAGNOSTIC CLASSIFICATION MODEL (FCDCM) SUMMARY")

  cat("Call:\n  ", paste(deparse(x$call), collapse = "\n   "), "\n", sep = "")

  cat("\nModel Configuration:\n")
  cat(sprintf("  Family: %s\n", x$model.info$family))
  cat(sprintf("  Attributes (D): %d\n", x$model.info$D))
  cat(sprintf("  DCM Type: %s\n", x$model.info$model))
  cat(sprintf("  Estimation Method: %s\n", x$model.info$method))

  cat("\nData Characteristics:\n")
  cat(sprintf("  Sample Size (N): %d\n", x$data.info$N))
  cat(sprintf("  Blocks (B): %d\n", x$data.info$B))
  cat(sprintf("  Response Type: %s\n", x$data.info$response.type))

  cat("\nFit Statistics:\n")
  cat(sprintf("  Log-likelihood: %.*f\n", d, x$fit.stats$LogLik))
  cat(sprintf("  Free Parameters: %d\n", x$fit.stats$npar))
  cat(sprintf("  AIC: %.*f\n", d, x$fit.stats$AIC))
  cat(sprintf("  BIC: %.*f\n", d, x$fit.stats$BIC))

  if (!is.null(x$delta.summary)) {
    cat("\nHigher-Order Parameters (delta):\n")
    .fc_print_matrix(x$delta.summary, d)
  }

  if (!is.null(x$par.summary)) {
    cat("\nBlock Parameters (eta):\n")
    .fc_print_matrix(x$par.summary, d)
  }

  if (!is.null(x$theta.summary)) {
    cat("\nHigher-Order Trait Summary (N x 1):\n")
    .fc_print_matrix(x$theta.summary, d)
  }

  if (!is.null(x$classification)) {
    cat("\nClassification:\n")
    cat(sprintf("  Observed Attribute Patterns: %s / %d possible\n",
                x$classification$n_patterns_observed,
                x$classification$n_patterns_possible))
    if (is.finite(x$classification$mean_max_posterior)) {
      cat(sprintf("  Mean Max Posterior: %.*f\n", d, x$classification$mean_max_posterior))
    }
  }

  if (!is.null(x$convergence)) {
    .fc_print_convergence(x$convergence)
  }

  cat("\nUse get.fit.index() to compute goodness-of-fit indices.\n\n")
  invisible(x)
}

# ---- FCGDINA ----

#' @describeIn print Print method for \code{FCGDINA} objects.
#'   Forced-Choice GDINA model summary.
#' @export
print.FCGDINA <- function(x, ...) {
  print.summary.FCGDINA(summary(x, ...))
  invisible(x)
}

#' @describeIn print Print method for \code{summary.FCGDINA} objects.
#'   Shows CDM type, forced-choice block info, fit stats, delta summaries,
#'   posterior attribute probabilities, and convergence.
#' @export
print.summary.FCGDINA <- function(x, ...) {
  d <- x$digits
  .fc_print_header("FORCED-CHOICE GDINA (FCGDINA) MODEL SUMMARY")

  cat("Call:\n  ", paste(deparse(x$call), collapse = "\n   "), "\n", sep = "")

  cat("\nModel Configuration:\n")
  cat(sprintf("  Family: %s\n", x$model.info$family))
  cat(sprintf("  CDM Model: %s\n", x$model.info$model))
  cat(sprintf("  Attributes (D): %d\n", x$model.info$D))
  cat(sprintf("  Estimation Method: %s\n", x$model.info$method))
  cat(sprintf("  FC Type: %s\n", x$model.info$fc.type))

  cat("\nData Characteristics:\n")
  cat(sprintf("  Sample Size (N): %d\n", x$data.info$N))
  cat(sprintf("  Blocks (B): %d\n", x$data.info$B))
  cat(sprintf("  Total Statements (I): %d\n", x$data.info$I))
  cat(sprintf("  Response Type: %s\n", x$data.info$response.type))

  cat("\nFit Statistics:\n")
  cat(sprintf("  Log-likelihood: %.*f\n", d, x$fit.stats$LogLik))
  cat(sprintf("  Free Parameters: %d\n", x$fit.stats$npar))
  cat(sprintf("  AIC: %.*f\n", d, x$fit.stats$AIC))
  cat(sprintf("  BIC: %.*f\n", d, x$fit.stats$BIC))

  if (!is.null(x$delta.summary)) {
    cat("\nCDM Delta Parameter Summary:\n")
    .fc_print_matrix(x$delta.summary, d)
  }

  if (!is.null(x$alpha.summary)) {
    cat("\nPosterior Attribute Probability Summary (N x D):\n")
    .fc_print_matrix(x$alpha.summary, d)
  }

  if (!is.null(x$classification)) {
    cat("\nClassification:\n")
    cat(sprintf("  Observed Attribute Patterns: %s / %d possible\n",
                x$classification$n_patterns_observed,
                x$classification$n_patterns_possible))
    if (is.finite(x$classification$mean_max_posterior)) {
      cat(sprintf("  Mean Max Posterior Probability: %.*f\n",
                  d, x$classification$mean_max_posterior))
    }
  }

  if (!is.null(x$pi.stats)) {
    cat(sprintf("\nStructural Parameters (class proportions):\n"))
    cat(sprintf("  Non-zero classes: %d / %d\n",
                x$pi.stats$n_nonzero,
                x$classification$n_patterns_possible))
    cat(sprintf("  Range: [%.*f, %.*f]\n",
                d, x$pi.stats$min, d, x$pi.stats$max))
    cat(sprintf("  Entropy: %.*f\n", d, x$pi.stats$entropy))
  }

  if (!is.null(x$convergence)) {
    .fc_print_convergence(x$convergence)
  }

  cat("\nUse get.fit.index() to compute goodness-of-fit indices.\n\n")
  invisible(x)
}

# ---- FCGGUM ----

#' @describeIn print Print method for \code{FCGGUM} objects.
#'   Forced-Choice Generalized Graded Unfolding Model summary.
#' @export
print.FCGGUM <- function(x, ...) {
  print.summary.FCGGUM(summary(x, ...))
  invisible(x)
}

#' @describeIn print Print method for \code{summary.FCGGUM} objects.
#'   Shows forced-choice unfolding model config, fit stats,
#'   factor correlations, person/statement parameter summaries.
#' @export
print.summary.FCGGUM <- function(x, ...) {
  d <- x$digits
  .fc_print_header("FORCED-CHOICE GENERALIZED GRADED UNFOLDING MODEL (FCGGUM) SUMMARY")

  cat("Call:\n  ", paste(deparse(x$call), collapse = "\n   "), "\n", sep = "")

  cat("\nModel Configuration:\n")
  cat(sprintf("  Family: %s\n", x$model.info$family))
  cat(sprintf("  Dimensions (D): %d\n", x$model.info$D))
  cat(sprintf("  Estimation Method: %s\n", x$model.info$method))
  cat(sprintf("  FC Type: %s\n", x$model.info$fc.type))

  cat("\nData Characteristics:\n")
  cat(sprintf("  Sample Size (N): %d\n", x$data.info$N))
  cat(sprintf("  Blocks (B): %d\n", x$data.info$B))
  cat(sprintf("  Total Statements (I): %d\n", x$data.info$I))
  cat(sprintf("  Response Type: %s\n", x$data.info$response.type))

  cat("\nFit Statistics:\n")
  cat(sprintf("  Log-likelihood: %.*f\n", d, x$fit.stats$LogLik))
  cat(sprintf("  Free Parameters: %d\n", x$fit.stats$npar))
  cat(sprintf("  AIC: %.*f\n", d, x$fit.stats$AIC))
  cat(sprintf("  BIC: %.*f\n", d, x$fit.stats$BIC))

  if (!is.null(x$Corr)) {
    cat("\nFactor Correlation Matrix:\n")
    .fc_print_matrix(x$Corr, d)
  }

  if (!is.null(x$theta.summary)) {
    cat("\nPerson Parameter Summary (N x D):\n")
    .fc_print_matrix(x$theta.summary, d)
  }

  if (!is.null(x$par.summary)) {
    cat("\nStatement Parameter Summary:\n")
    .fc_print_matrix(x$par.summary, d)
  }

  if (!is.null(x$convergence)) {
    .fc_print_convergence(x$convergence)
  }

  cat("\nUse get.fit.index() to compute goodness-of-fit indices.\n\n")
  invisible(x)
}

# ---- TIRT ----

#' @describeIn print Print method for \code{TIRT} objects.
#'   Thurstonian IRT for Forced-Choice model summary.
#' @export
print.TIRT <- function(x, ...) {
  print.summary.TIRT(summary(x, ...))
  invisible(x)
}

#' @describeIn print Print method for \code{summary.TIRT} objects.
#'   Shows pairwise-comparison model config, fit stats,
#'   factor correlations, loading/person parameter summaries.
#' @export
print.summary.TIRT <- function(x, ...) {
  d <- x$digits
  .fc_print_header("THURSTONIAN IRT FOR FORCED-CHOICE (TIRT) MODEL SUMMARY")

  cat("Call:\n  ", paste(deparse(x$call), collapse = "\n   "), "\n", sep = "")

  cat("\nModel Configuration:\n")
  cat(sprintf("  Family: %s\n", x$model.info$family))
  cat(sprintf("  Dimensions (D): %d\n", x$model.info$D))
  cat(sprintf("  Statements (I): %d\n", x$model.info$I.states))
  cat(sprintf("  Blocks: %d\n", x$model.info$N.block))
  cat(sprintf("  Estimation Method: %s\n", x$model.info$method))
  cat(sprintf("  FC Type: %s\n", x$model.info$fc.type))

  cat("\nData Characteristics:\n")
  cat(sprintf("  Sample Size (N): %d\n", x$data.info$N))
  cat(sprintf("  Pairwise Responses: %d\n", x$data.info$I.pairs))
  cat(sprintf("  Statements: %d\n", x$data.info$I.states))
  cat(sprintf("  Response Type: %s\n", x$data.info$response.type))

  cat("\nFit Statistics:\n")
  cat(sprintf("  Log-likelihood: %.*f\n", d, x$fit.stats$LogLik))
  cat(sprintf("  Free Parameters: %d\n", x$fit.stats$npar))
  cat(sprintf("  AIC: %.*f\n", d, x$fit.stats$AIC))
  cat(sprintf("  BIC: %.*f\n", d, x$fit.stats$BIC))

  if (!is.null(x$Corr)) {
    cat("\nFactor Correlation Matrix:\n")
    .fc_print_matrix(x$Corr, d)
  }

  if (!is.null(x$lambda.summary)) {
    cat("\nLoading (Lambda) Summary:\n")
    .fc_print_matrix(x$lambda.summary, d)
  }

  if (!is.null(x$theta.summary)) {
    cat("\nPerson Parameter Summary (N x D):\n")
    .fc_print_matrix(x$theta.summary, d)
  }

  if (!is.null(x$convergence)) {
    .fc_print_convergence(x$convergence)
  }

  cat("\nUse get.fit.index() to compute goodness-of-fit indices.\n\n")
  invisible(x)
}


# ===========================================================================
# Internal gof table printers (used by print.summary.good.of.fit)
# ===========================================================================

.gof_print_overview <- function(tab) {
  cat("\nOverview:\n")
  width <- max(nchar(tab$Field), na.rm = TRUE)
  for (i in seq_len(nrow(tab))) {
    cat(sprintf("  %-*s  %s\n", width, tab$Field[i], tab$Value[i]))
  }
}

.gof_print_metric_table <- function(title, tab) {
  cat("\n")
  cat(title, ":\n", sep = "")

  cols <- c("Metric", "Estimate", "Recommended")
  is_sub <- grepl("^SUB:", tab$Metric, perl = TRUE)
  tab$Metric <- sub("^SUB:\\s*", "", tab$Metric, perl = TRUE)

  metric_rows <- tab[!is_sub, , drop = FALSE]
  if (nrow(metric_rows) == 0L) return(invisible(NULL))

  widths <- vapply(cols, function(col) {
    header <- if (col == "Recommended") "Guide" else col
    max(nchar(c(header, metric_rows[[col]])), na.rm = TRUE)
  }, numeric(1L))
  widths <- pmax(widths, nchar(c("Metric", "Estimate", "Guide")))

  header <- sprintf("  %-*s  %*s  %-*s",
                    widths["Metric"], "Metric",
                    widths["Estimate"], "Estimate",
                    widths["Recommended"], "Guide")
  cat(header, "\n", sep = "")
  cat("  ", paste(
    vapply(widths, function(w) paste(rep("-", w), collapse = ""), character(1L)),
    collapse = "  "
  ), "\n", sep = "")

  mi <- 1L
  for (i in seq_len(nrow(tab))) {
    if (is_sub[i]) {
      cat("\n  ", tab$Metric[i], ":\n", sep = "")
    } else {
      r <- metric_rows[mi, , drop = FALSE]
      est <- if (nchar(r$Estimate) == 0L || r$Estimate == "NA") "NA" else r$Estimate
      rec <- if (nchar(r$Recommended) == 0L) "" else r$Recommended
      cat(sprintf("  %-*s  %*s  %-*s\n",
                  widths["Metric"], r$Metric,
                  widths["Estimate"], est,
                  widths["Recommended"], rec))
      mi <- mi + 1L
    }
  }
}

.gof_print_legacy_summary <- function(x) {
  digits <- attr(x, "digits")
  if (is.null(digits)) digits <- 4

  fmt <- function(val, d = digits, int = FALSE) {
    if (is.null(val) || length(val) == 0) return(character(0))
    res <- sapply(val, function(v) {
      if (is.na(v)) return("NA")
      if (int || abs(v - round(v)) < .Machine$double.eps^0.5) {
        formatC(round(v), format = "d", big.mark = ",")
      } else {
        formatC(v, format = "f", digits = d)
      }
    })
    names(res) <- names(val)
    return(res)
  }

  print_table <- function(labels, values) {
    if (length(labels) == 0) return(invisible(NULL))
    keep <- !(is.na(values) | values == "NA")
    labels <- labels[keep]
    values <- values[keep]
    if (length(labels) == 0) return(invisible(NULL))
    width <- max(nchar(labels), na.rm = TRUE)
    for (i in seq_along(labels)) {
      cat(sprintf("  %-*s %s\n", width, labels[i], values[i]))
    }
  }

  cat(sprintf("\nSample Size (N):       %s\n", fmt(x$Sample_Size, int = TRUE)))
  cat(sprintf("Items/Indicators:      %s\n", fmt(x$Items, int = TRUE)))
  cat(sprintf("Response Type:         %s\n", x$Response_Type))
  cat(sprintf("Parameters:            %s\n", fmt(x$Parameters, int = TRUE)))
  cat(sprintf("Moments:               %s total, %s bivariate\n",
              fmt(x$Moments, int = TRUE), fmt(x$Bivariate_Moments, int = TRUE)))
  cat(sprintf("Log-Likelihood:        %s\n", fmt(x$LogLik)))
  cat(sprintf("Deviance (-2LL):       %s\n\n", fmt(x$Deviance)))

  if (length(x$IC) > 0 && !all(is.na(x$IC))) {
    cat("Information Criteria:\n")
    print_table(names(x$IC), fmt(x$IC))
    cat("\n")
  }
  invisible(x)
}


# ===========================================================================
# Simulated data print methods
# ===========================================================================

.fc_print_data_header <- function(title) {
  cat(sprintf("\n%s\n", title))
  cat(paste(rep("=", nchar(title)), collapse = ""), "\n")
}

.print_data_common <- function(x, label) {
  .fc_print_data_header(paste0("SIMULATED ", label, " DATA"))
  cat(sprintf("Sample Size (N)     : %d\n", x$N))
  cat(sprintf("Items/Indicators (I): %d\n", x$I))
  cat(sprintf("Dimensions (D)      : %d\n", x$D))
  if (!is.null(x$model)) cat(sprintf("Model               : %s\n", x$model))
  cat(sprintf("Call: %s\n", paste(deparse(x$call), collapse = " ")))
  invisible(x)
}

#' @describeIn print Print method for \code{data.MIRT} objects.
#' @export
print.data.MIRT <- function(x, ...) {
  .print_data_common(x, "MIRT")
}

#' @describeIn print Print method for \code{data.MGPCM} objects.
#' @export
print.data.MGPCM <- function(x, ...) {
  .print_data_common(x, "MGPCM")
}

#' @describeIn print Print method for \code{data.MGGUM} objects.
#' @export
print.data.MGGUM <- function(x, ...) {
  .print_data_common(x, "MGGUM")
}

#' @describeIn print Print method for \code{data.FCMIRT} objects.
#' @export
print.data.FCMIRT <- function(x, ...) {
  .fc_print_data_header("SIMULATED FCMIRT DATA")
  cat(sprintf("Sample Size (N)      : %d\n", x$N.person))
  cat(sprintf("Blocks (B)           : %d\n", x$N.block))
  cat(sprintf("Statements (I)       : %d\n", x$I.states))
  cat(sprintf("Dimensions (D)       : %d\n", x$D))
  cat(sprintf("Model                : %s\n", x$model))
  cat(sprintf("FC Type              : %s\n",
              paste(x$fc.type, collapse = ", ")))
  cat(sprintf("Call: %s\n", paste(deparse(x$call), collapse = " ")))
  invisible(x)
}

#' @describeIn print Print method for \code{data.FCDCM} objects.
#' @export
print.data.FCDCM <- function(x, ...) {
  .fc_print_data_header("SIMULATED FCDCM DATA")
  cat(sprintf("Sample Size (N)      : %d\n", x$N.person))
  cat(sprintf("Blocks (B)           : %d\n", x$N.block))
  cat(sprintf("Attributes (D)       : %d\n", x$D))
  cat(sprintf("DCM Type             : %s\n",
              paste(unique(x$dcm.type), collapse = ", ")))
  cat(sprintf("Call: %s\n", paste(deparse(x$call), collapse = " ")))
  invisible(x)
}

#' @describeIn print Print method for \code{data.FCGDINA} objects.
#' @export
print.data.FCGDINA <- function(x, ...) {
  .fc_print_data_header("SIMULATED FCGDINA DATA")
  cat(sprintf("Sample Size (N)      : %d\n", x$N.person))
  cat(sprintf("Blocks (B)           : %d\n", x$N.block))
  cat(sprintf("Statements (I)       : %d\n", x$I.states))
  cat(sprintf("Attributes (D)       : %d\n", x$D))
  cat(sprintf("CDM Model            : %s\n", x$model))
  if (length(unique(x$fc.type)) == 1L) {
    cat(sprintf("FC Type              : %s\n", x$fc.type[1L]))
  } else {
    cat(sprintf("FC Type              : %s\n", paste(x$fc.type, collapse = ", ")))
  }
  cat(sprintf("Call: %s\n", paste(deparse(x$call), collapse = " ")))
  invisible(x)
}

#' @describeIn print Print method for \code{data.FCGGUM} objects.
#' @export
print.data.FCGGUM <- function(x, ...) {
  .fc_print_data_header("SIMULATED FCGGUM DATA")
  cat(sprintf("Sample Size (N)      : %d\n", x$N.person))
  cat(sprintf("Blocks (B)           : %d\n", x$N.block))
  cat(sprintf("Statements (I)       : %d\n", x$I.states))
  cat(sprintf("Dimensions (D)       : %d\n", x$D))
  cat(sprintf("FC Type              : %s\n",
              paste(x$fc.type, collapse = ", ")))
  cat(sprintf("Call: %s\n", paste(deparse(x$call), collapse = " ")))
  invisible(x)
}

#' @describeIn print Print method for \code{data.TIRT} objects.
#' @export
print.data.TIRT <- function(x, ...) {
  .fc_print_data_header("SIMULATED TIRT DATA")
  cat(sprintf("Sample Size (N)      : %d\n", x$N.person))
  cat(sprintf("Items (I)            : %d\n", x$I.states))
  cat(sprintf("Dimensions (D)       : %d\n", x$D))
  cat(sprintf("FC Type              : %s\n",
              paste(x$fc.type, collapse = ", ")))
  cat(sprintf("Call: %s\n", paste(deparse(x$call), collapse = " ")))
  invisible(x)
}

Try the ForceChoice package in your browser

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

ForceChoice documentation built on Sept. 13, 2026, 1:06 a.m.