R/igp_methods.R

Defines functions plot.igp_fit predict.igp_fit confint.igp_fit BIC.igp_fit AIC.igp_fit logLik.igp_fit vcov.igp_fit coef.igp_fit print.summary_igp_fit summary.igp_fit print.igp_fit

Documented in AIC.igp_fit BIC.igp_fit coef.igp_fit confint.igp_fit logLik.igp_fit plot.igp_fit predict.igp_fit print.igp_fit print.summary_igp_fit summary.igp_fit vcov.igp_fit

#' Methods for Inverse Gaussian Process Fit Objects
#'
#' Standard S3 methods for objects of class \code{"igp_fit"}: \code{print},
#' \code{summary}, \code{coef}, \code{vcov}, \code{logLik}, \code{AIC},
#' \code{BIC}, \code{confint}, \code{predict}, and \code{plot}.
#'
#' @param x,object An object of class \code{"igp_fit"}.
#' @param digits Integer indicating the number of decimal places to format. Default is \code{4}.
#' @param conf_level,level Nominal confidence level (e.g. 0.95 for 95\% confidence intervals). Default is \code{0.95}.
#' @param parm Optional vector of numbers or parameter names to compute confidence intervals for.
#' @param newdata Optional new data frame for prediction.
#' @param threshold Failure threshold \eqn{\rho} for lifetime and quantile calculations.
#' @param times Numeric vector of inspection times for lifetime predictions.
#' @param probs Numeric vector of quantile probabilities. Default is \code{c(0.01, 0.05, 0.1, 0.5, 0.8)}.
#' @param k Optional numeric penalty per parameter for \code{AIC} (default is 2).
#' @param type For \code{predict}, one of \code{"lifetime"}, \code{"frailty"}, or \code{"quantiles"}.
#'   For \code{plot}, one of \code{"paths"} (degradation paths), \code{"frailty"} (individual frailty barplot),
#'   \code{"lifetime"} (lifetime PDF and CDF curves), \code{"diagnostics"} (P-P and Q-Q plots), or \code{"all"}.
#' @param ... Further arguments passed to or from other methods.
#'
#' @return
#' \itemize{
#'   \item \code{print.igp_fit}: Prints model summary and returns \code{x} invisibly.
#'   \item \code{summary.igp_fit}: Returns an object of class \code{"summary_igp_fit"}.
#'   \item \code{coef.igp_fit}: Returns named vector of estimated parameters.
#'   \item \code{vcov.igp_fit}: Returns the variance-covariance matrix.
#'   \item \code{logLik.igp_fit}: Returns log-likelihood with degrees of freedom and observations.
#'   \item \code{AIC.igp_fit}, \code{BIC.igp_fit}: Return model selection criterion values.
#'   \item \code{confint.igp_fit}: Returns confidence interval matrix.
#'   \item \code{predict.igp_fit}: Returns predicted lifetime distribution, quantiles, or frailties.
#'   \item \code{plot.igp_fit}: Generates base graphics diagnostic and predictive plots.
#' }
#'
#' @importFrom stats AIC BIC coef confint logLik predict uniroot rnorm runif pnorm qnorm dnorm optim vcov pchisq rgamma
#' @importFrom graphics abline barplot box grid legend lines mtext par plot points text title
#' @importFrom grDevices rainbow
#' @importFrom utils head tail
#'
#' @seealso \code{\link{igp_fit}}, \code{\link{individual_frailty}}, \code{\link{lifetime_dist}}
#'
#' @examples
#' data(laser)
#' fit <- igp_fit(laser, time_col = "t", deg_col = "increase",
#'                unit_col = "unit", frailty = "gamma")
#' print(fit)
#' summary(fit)
#' coef(fit)
#' vcov(fit)
#' logLik(fit)
#' AIC(fit)
#' BIC(fit)
#' confint(fit)
#'
#' @export
print.igp_fit <- function(x, digits = 4, ...) {
  cat("\n=== Inverse Gaussian Process Degradation Model ===\n")
  frailty_label <- switch(
    x$frailty,
    none = "None (Classical IGP)",
    gamma = "Gamma Frailty (IGP-Gamma)",
    ig = "Inverse Gaussian Frailty (IGP-IG)"
  )
  cat("Frailty Model   :", frailty_label, "\n")
  cat("Mean Function   :", x$mean_fun_name, "\n")
  cat("Number of Units :", x$n_units, "\n")
  cat("Total Increments:", x$n_obs, "\n")
  cat("Log-Likelihood  :", format(round(x$loglik, digits), nsmall = digits), "\n")
  cat("AIC             :", format(round(x$aic, digits), nsmall = digits), "\n")
  cat("BIC             :", format(round(x$bic, digits), nsmall = digits), "\n\n")
  
  cat("Estimated Parameters:\n")
  print(round(x$coefficients, digits), ...)
  cat("\n")
  invisible(x)
}

#' @rdname print.igp_fit
#' @export
summary.igp_fit <- function(object, conf_level = 0.95, ...) {
  coefs <- object$coefficients
  se <- object$std_errors
  z_val <- coefs / se
  p_val <- 2 * (1 - pnorm(abs(z_val)))
  
  z_crit <- qnorm(1 - (1 - conf_level) / 2)
  
  # Standard normal CIs for linear parameters, log-transformed for scale and frailty
  ci_low <- numeric(length(coefs))
  ci_high <- numeric(length(coefs))
  
  for (i in seq_along(coefs)) {
    pname <- names(coefs)[i]
    if (pname %in% c("eta", "xi")) {
      # Log-transformed CI
      se_log <- se[i] / coefs[i]
      ci_low[i] <- coefs[i] * exp(-z_crit * se_log)
      ci_high[i] <- coefs[i] * exp(z_crit * se_log)
    } else {
      ci_low[i] <- coefs[i] - z_crit * se[i]
      ci_high[i] <- coefs[i] + z_crit * se[i]
    }
  }
  
  tab <- data.frame(
    Estimate = coefs,
    `Std. Error` = se,
    `z value` = z_val,
    `Pr(>|z|)` = p_val,
    `CI Lower` = ci_low,
    `CI Upper` = ci_high,
    check.names = FALSE
  )
  
  res <- list(
    fit = object,
    coef_table = tab,
    conf_level = conf_level
  )
  class(res) <- "summary_igp_fit"
  res
}

#' @rdname print.igp_fit
#' @export
print.summary_igp_fit <- function(x, digits = 4, ...) {
  print(x$fit, digits = digits, ...)
  cat(sprintf("Coefficients & %.0f%% Confidence Intervals:\n", x$conf_level * 100))
  print(round(x$coef_table, digits), ...)
  cat("\nNote: Confidence intervals for positive scale/frailty parameters (eta, xi) use log-transformation.\n\n")
  invisible(x)
}

#' @rdname print.igp_fit
#' @export
coef.igp_fit <- function(object, ...) {
  object$coefficients
}

#' @rdname print.igp_fit
#' @export
vcov.igp_fit <- function(object, ...) {
  object$vcov
}

#' @rdname print.igp_fit
#' @export
logLik.igp_fit <- function(object, ...) {
  val <- object$loglik
  attr(val, "df") <- length(object$coefficients)
  attr(val, "nobs") <- object$n_units
  class(val) <- "logLik"
  val
}

#' @rdname print.igp_fit
#' @export
AIC.igp_fit <- function(object, ..., k = 2) {
  if (missing(k) || is.null(k) || k == 2) {
    return(object$aic)
  }
  k * length(object$coefficients) - 2 * object$loglik
}

#' @rdname print.igp_fit
#' @export
BIC.igp_fit <- function(object, ...) {
  object$bic
}

#' @rdname print.igp_fit
#' @export
confint.igp_fit <- function(object, parm, level = 0.95, ...) {
  s <- summary(object, conf_level = level)
  ci_mat <- as.matrix(s$coef_table[, c("CI Lower", "CI Upper")])
  colnames(ci_mat) <- c(sprintf("%.1f %%", (1 - level) / 2 * 100), sprintf("%.1f %%", (1 + level) / 2 * 100))
  rownames(ci_mat) <- names(object$coefficients)
  if (!missing(parm)) {
    ci_mat <- ci_mat[parm, , drop = FALSE]
  }
  ci_mat
}

#' @rdname print.igp_fit
#' @export
predict.igp_fit <- function(object, newdata = NULL, threshold = NULL, times = NULL, probs = c(0.01, 0.05, 0.1, 0.5, 0.8), type = c("lifetime", "frailty", "quantiles"), ...) {
  type <- match.arg(type)
  
  if (type == "lifetime") {
    if (is.null(threshold)) stop("Argument 'threshold' must be specified for lifetime prediction.")
    return(lifetime_dist(object, threshold = threshold, times = times, probs = probs))
  } else if (type == "quantiles") {
    if (is.null(threshold)) stop("Argument 'threshold' must be specified for quantile prediction.")
    lt <- lifetime_dist(object, threshold = threshold, times = times, probs = probs)
    return(lt$quantiles)
  } else if (type == "frailty") {
    return(individual_frailty(object))
  }
}

#' @rdname print.igp_fit
#' @export
plot.igp_fit <- function(x, type = c("paths", "frailty", "lifetime", "diagnostics", "all"), threshold = NULL, ...) {
  type <- match.arg(type)
  
  draw_paths <- function() {
    d <- x$data
    u_col <- as.character(x$call$unit_col)
    t_col <- as.character(x$call$time_col)
    y_col <- as.character(x$call$deg_col)
    if (length(u_col) == 0 || !(u_col %in% names(d))) u_col <- names(d)[1]
    if (length(t_col) == 0 || !(t_col %in% names(d))) t_col <- names(d)[2]
    if (length(y_col) == 0 || !(y_col %in% names(d))) y_col <- names(d)[3]
    
    units <- unique(d[[u_col]])
    col_palette <- rainbow(length(units))
    
    plot(d[[t_col]], d[[y_col]], type = "n",
         xlab = "Inspection Time (t)", ylab = "Degradation D(t)",
         main = "Observed Degradation Paths", las = 1)
    grid()
    
    for (i in seq_along(units)) {
      sub <- d[d[[u_col]] == units[i], ]
      sub <- sub[order(sub[[t_col]]), ]
      lines(sub[[t_col]], sub[[y_col]], col = col_palette[i], lwd = 1.5, type = "o", pch = 16, cex = 0.6)
    }
    
    # Overlay fitted mean curve
    t_seq <- seq(min(d[[t_col]]), max(d[[t_col]]), length.out = 100)
    lines(t_seq, x$mean_fun(t_seq, x$coefficients[1]), col = "black", lwd = 2.5, lty = 2)
    
    if (!is.null(threshold)) {
      abline(h = threshold, col = "red", lty = 3, lwd = 2)
      text(min(d[[t_col]]), threshold, labels = paste("Failure Threshold rho =", threshold), pos = 4, col = "red")
    }
  }
  
  draw_frailty <- function() {
    if (x$frailty == "none") {
      plot(1, type = "n", axes = FALSE, xlab = "", ylab = "", main = "Frailty Estimates Not Applicable")
      text(1, 1, "Model was fitted without frailty term (Classical IGP).", cex = 1.1)
      return()
    }
    frail <- individual_frailty(x)
    df <- frail$estimates
    
    bp <- barplot(df$E_z, names.arg = df$unit, las = 2,
                  col = "lightblue", border = "darkblue",
                  xlab = "Unit ID", ylab = "Posterior Mean E[z_i | Data]",
                  main = paste("Individual Frailty Estimates (", toupper(x$frailty), "Frailty)"))
    abline(h = 1.0, col = "red", lty = 2, lwd = 1.5)
    grid(nx = NA, ny = NULL)
  }
  
  draw_lifetime <- function() {
    if (is.null(threshold)) {
      # Use default threshold as max observed degradation
      threshold <- max(sapply(x$unit_data, function(d) sum(d$dy))) * 1.1
    }
    lt <- lifetime_dist(x, threshold = threshold)
    
    par(mfrow = c(1, 2))
    # CDF
    plot(lt$curve$t, lt$curve$CDF, type = "l", col = "blue", lwd = 2,
         xlab = "Time (t)", ylab = "Failure Probability F_T(t)",
         main = paste("Lifetime CDF (rho =", round(threshold, 2), ")"), las = 1)
    grid()
    
    # PDF
    plot(lt$curve$t, lt$curve$PDF, type = "l", col = "darkgreen", lwd = 2,
         xlab = "Time (t)", ylab = "Failure Density f_T(t)",
         main = paste("Lifetime PDF (rho =", round(threshold, 2), ")"), las = 1)
    grid()
  }
  
  draw_diagnostics <- function() {
    # Extract increments
    dy_all <- unlist(lapply(x$unit_data, function(d) d$dy))
    dt_all <- unlist(lapply(x$unit_data, function(d) d$dt))
    dg_all <- unlist(lapply(x$unit_data, function(d) {
      x$mean_fun(d$t_end, x$coefficients[1]) - x$mean_fun(d$t_start, x$coefficients[1])
    }))
    
    eta_est <- x$coefficients["eta"]
    p_vals <- p_ig(dy_all, mu = dg_all, lambda = eta_est * (dg_all^2))
    p_sorted <- sort(p_vals)
    n <- length(p_sorted)
    p_theor <- (seq_len(n) - 0.5) / n
    
    par(mfrow = c(1, 2))
    # P-P Plot
    plot(p_theor, p_sorted, pch = 19, col = "royalblue", cex = 0.8,
         xlab = "Theoretical Uniform Probabilities", ylab = "Empirical Residual Probabilities",
         main = "P-P Plot of Transformed Residuals", las = 1)
    abline(0, 1, col = "red", lwd = 2, lty = 2)
    grid()
    
    # Residual vs Fitted
    plot(dg_all, dy_all - dg_all, pch = 19, col = "darkgreen", cex = 0.8,
         xlab = "Fitted Mean Increments Delta g(t)", ylab = "Raw Increment Residuals",
         main = "Residuals vs Fitted Increments", las = 1)
    abline(h = 0, col = "red", lwd = 2, lty = 2)
    grid()
  }
  
  if (type == "paths") {
    draw_paths()
  } else if (type == "frailty") {
    draw_frailty()
  } else if (type == "lifetime") {
    draw_lifetime()
  } else if (type == "diagnostics") {
    draw_diagnostics()
  } else if (type == "all") {
    old_par <- par(no.readonly = TRUE)
    on.exit(par(old_par))
    par(mfrow = c(2, 2), mar = c(4, 4, 2, 1))
    draw_paths()
    draw_frailty()
    draw_diagnostics()
  }
  
  invisible(x)
}

Try the IGPFrailty package in your browser

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

IGPFrailty documentation built on Aug. 25, 2026, 9:08 a.m.