Skip to contents

Introduction

When we observe only the lifetime of a parallel system — the time until the last component fails — the system lifetime T=max(T1,,Tm)T = \max(T_1, \ldots, T_m) is a single number, but we want to recover mm individual component lifetime distributions. Components that fail quickly (“fast” components) have Fj(T)1F_j(T) \approx 1 at the system failure time, contributing almost no curvature to the likelihood. Slow components dominate the system density and are estimated well. This information asymmetry is the central practical obstacle.

This vignette shows how periodic inspection — checking which components have failed at regular intervals — resolves this asymmetry at surprisingly low cost.

1. The observation scheme hierarchy

The kofn package defines three observation schemes, ordered from least to most informative (Scheme 2 > Scheme 1 > Scheme 0):

Scheme What is observed Constructor
Scheme 0 System lifetime only (black box) observe_right_censor()
Scheme 1 System lifetime + periodic component inspections observe_periodic(delta)
Scheme 2 All component lifetimes exactly (complete data) observe_exact()

Scheme 1 occupies the practical middle ground. At each inspection time δ\ell\delta, we record which components have already failed. This requires only periodic access to the system.

# Scheme 0: system lifetime only (optionally right-censored at tau)
obs0 <- observe_right_censor(tau = 100)
obs0(42.7)    # exact observation
#> $t
#> [1] 42.7
#> 
#> $omega
#> [1] "exact"
#> 
#> $t_upper
#> [1] NA
obs0(150.0)   # right-censored at tau = 100
#> $t
#> [1] 100
#> 
#> $omega
#> [1] "right"
#> 
#> $t_upper
#> [1] NA

# Scheme 1: periodic inspection with interval width delta = 5
obs1 <- observe_periodic(delta = 5, tau = 100)
obs1(42.7)    # failure in interval [40, 45)
#> $t
#> [1] 40
#> 
#> $omega
#> [1] "interval"
#> 
#> $t_upper
#> [1] 45
obs1(150.0)   # right-censored
#> $t
#> [1] 100
#> 
#> $omega
#> [1] "right"
#> 
#> $t_upper
#> [1] NA

# Scheme 2: complete data (trivial)
obs2 <- observe_exact()
obs2(42.7)    # exact observation, always
#> $t
#> [1] 42.7
#> 
#> $omega
#> [1] "exact"
#> 
#> $t_upper
#> [1] NA

2. The information asymmetry problem

To see why Scheme 0 struggles, consider a two-component Weibull parallel system with parameters:

  • Component 1 (fast): shape α1=1.5\alpha_1 = 1.5, scale β1=2.0\beta_1 = 2.0
  • Component 2 (slow): shape α2=2.0\alpha_2 = 2.0, scale β2=3.0\beta_2 = 3.0

Component 1 has a median lifetime of about 1.4 time units; Component 2’s median is about 2.5. At the system failure time T=max(T1,T2)T = \max(T_1, T_2), the fast component has almost surely already failed — its CDF F1(T)1F_1(T) \approx 1, so the likelihood surface is nearly flat with respect to Component 1’s parameters.

set.seed(42)

# True parameters: (shape_1, scale_1, shape_2, scale_2)
theta_true <- c(1.5, 2.0, 2.0, 3.0)

# Create the Weibull parallel system model (EM for Scheme 0)
model_em <- kofn(k = 2, m = 2, component = dfr_weibull(), method = "em")

# Generate Scheme 0 data (system lifetimes only)
gen <- rdata(model_em)
df0 <- gen(theta_true, n = 100)

cat("System lifetime summary:\n")
#> System lifetime summary:
summary(df0$t)
#>    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
#>  0.5859  1.9135  2.8543  3.0707  3.9466  8.2247

# At the median system lifetime, how much has each component's CDF accumulated?
t_med <- median(df0$t)
cat(sprintf("\nAt median system time %.2f:\n", t_med))
#> 
#> At median system time 2.85:
cat(sprintf("  F_1(t) = %.4f  (fast component: nearly saturated)\n",
            pweibull(t_med, shape = 1.5, scale = 2.0)))
#>   F_1(t) = 0.8182  (fast component: nearly saturated)
cat(sprintf("  F_2(t) = %.4f  (slow component: still informative)\n",
            pweibull(t_med, shape = 2.0, scale = 3.0)))
#>   F_2(t) = 0.5955  (slow component: still informative)

With F1(T)1F_1(T) \approx 1, Component 1’s contribution to the system density is a tiny fraction of the total, and the likelihood carries almost no information about α1\alpha_1 and β1\beta_1. Fitting under Scheme 0 confirms this:

# Fit under Scheme 0 using the EM algorithm
solver <- fit(model_em)
mle0 <- solver(df0, n_starts = 1L)

est0 <- coef(mle0)
cat("Scheme 0 estimates vs truth:\n")
#> Scheme 0 estimates vs truth:
cat(sprintf("  Component 1 (fast): shape = %.3f (true: 1.500), scale = %.3f (true: 2.000)\n",
            est0[1], est0[2]))
#>   Component 1 (fast): shape = 3.067 (true: 1.500), scale = 1.496 (true: 2.000)
cat(sprintf("  Component 2 (slow): shape = %.3f (true: 2.000), scale = %.3f (true: 3.000)\n",
            est0[3], est0[4]))
#>   Component 2 (slow): shape = 1.879 (true: 2.000), scale = 3.345 (true: 3.000)

# Standard errors from the variance-covariance matrix
se_vec <- tryCatch({
  V <- vcov(mle0)
  if (is.null(V)) rep(NA_real_, length(coef(mle0))) else sqrt(diag(V))
}, error = function(e) rep(NA_real_, length(coef(mle0))))
if (!any(is.na(se_vec))) {
  cat(sprintf("\n  SE(shape_1) = %.3f,  SE(shape_2) = %.3f\n",
              se_vec[1], se_vec[3]))
  cat(sprintf("  SE ratio (fast/slow): %.1fx\n", se_vec[1] / se_vec[3]))
}
#> 
#>   SE(shape_1) = 0.879,  SE(shape_2) = 0.190
#>   SE ratio (fast/slow): 4.6x

The SE ratio reflects a genuine information gap, not an algorithm deficiency.

3. Scheme 1: Periodic inspection

Under Scheme 1, we supplement system-level observation with periodic inspections. At each inspection time δ\ell\delta (for =1,2,\ell = 1, 2, \ldots), we check each component and record whether it has failed. This localizes each component’s failure time to an inspection interval [aj,aj+)[a_j^{-}, a_j^{+}) where aj+aj=δa_j^{+} - a_j^{-} = \delta.

The likelihood

The likelihood for a single observation under Scheme 1 combines two pieces of information:

Li(θ)=fsys(ti;θ)exact system time×j=1m[Fj(aij+)Fj(aij)]interval-censored component j L_i(\theta) = \underbrace{f_{\text{sys}}(t_i; \theta)}_{\text{exact system time}} \times \prod_{j=1}^{m} \underbrace{\left[F_j(a_{ij}^{+}) - F_j(a_{ij}^{-})\right]}_{\text{interval-censored component } j}

where fsys(t)=j=1mfj(t)kjFk(t)f_{\text{sys}}(t) = \sum_{j=1}^{m} f_j(t) \prod_{k \neq j} F_k(t) is the parallel system density and [aij,aij+)[a_{ij}^{-}, a_{ij}^{+}) is the inspection interval containing component jj’s failure time.

Even a wide interval provides information: knowing that the fast component failed in [0,δ)[0, \delta) versus [δ,2δ)[\delta, 2\delta) constrains the hazard function in a way that the system-level observation alone cannot. The kofn package provides dedicated functions for Scheme 1:

# Generate data under Scheme 1 with delta = 0.5
model_wei <- kofn(k = 2, m = 2, component = dfr_weibull())
gen_s1 <- rdata_scheme1(model_wei)
df1 <- gen_s1(theta_true, n = 100, delta = 0.5)

cat("Scheme 1 data (first 6 observations):\n")
#> Scheme 1 data (first 6 observations):
print(head(df1))
#>          t comp_lower_1 comp_upper_1 comp_lower_2 comp_upper_2
#> 1 2.556432          0.0          0.5          2.5          3.0
#> 2 2.701081          1.5          2.0          2.5          3.0
#> 3 5.026235          0.5          1.0          5.0          5.5
#> 4 3.169580          1.5          2.0          3.0          3.5
#> 5 3.009575          3.0          3.5          1.0          1.5
#> 6 1.746040          1.5          2.0          0.5          1.0

cat("\nColumn meanings:\n")
#> 
#> Column meanings:
cat("  t:            exact system failure time\n")
#>   t:            exact system failure time
cat("  comp_lower_j: lower bound of component j's inspection interval\n")
#>   comp_lower_j: lower bound of component j's inspection interval
cat("  comp_upper_j: upper bound of component j's inspection interval\n")
#>   comp_upper_j: upper bound of component j's inspection interval
# Fit under Scheme 1
solver_s1 <- fit_scheme1(model_wei)
mle1 <- solver_s1(df1, n_starts = 1L)

est1 <- coef(mle1)
cat("Scheme 1 (delta = 0.5) estimates vs truth:\n")
#> Scheme 1 (delta = 0.5) estimates vs truth:
cat(sprintf("  Component 1 (fast): shape = %.3f (true: 1.500), scale = %.3f (true: 2.000)\n",
            est1[1], est1[2]))
#>   Component 1 (fast): shape = 1.641 (true: 1.500), scale = 2.243 (true: 2.000)
cat(sprintf("  Component 2 (slow): shape = %.3f (true: 2.000), scale = %.3f (true: 3.000)\n",
            est1[3], est1[4]))
#>   Component 2 (slow): shape = 1.944 (true: 2.000), scale = 2.942 (true: 3.000)

4. Scheme 0 vs Scheme 1 comparison

The real test is a controlled comparison using the same true parameters. We generate data from a common set of component lifetimes and fit under both schemes.

set.seed(123)

n <- 100
m <- 2
alpha_true <- c(1.5, 2.0)
beta_true  <- c(2.0, 3.0)
theta_true <- c(alpha_true[1], beta_true[1], alpha_true[2], beta_true[2])
delta <- 0.5

# Generate component lifetimes directly
comp_times <- matrix(0, nrow = n, ncol = m)
for (j in seq_len(m)) {
  comp_times[, j] <- rweibull(n, shape = alpha_true[j], scale = beta_true[j])
}
sys_times <- apply(comp_times, 1, max)

# --- Scheme 0: system lifetime only ---
df_s0 <- data.frame(t = sys_times)
model_em <- kofn(k = 2, m = 2, component = dfr_weibull(), method = "em")
solver_em <- fit(model_em)
mle_s0 <- solver_em(df_s0, n_starts = 1L)

# --- Scheme 1: add inspection intervals ---
df_s1 <- data.frame(t = sys_times)
for (j in seq_len(m)) {
  df_s1[[paste0("comp_lower_", j)]] <- floor(comp_times[, j] / delta) * delta
  df_s1[[paste0("comp_upper_", j)]] <- floor(comp_times[, j] / delta) * delta + delta
}
model_mle <- kofn(k = 2, m = 2, component = dfr_weibull(), method = "mle")
solver_s1 <- fit_scheme1(model_mle)
mle_s1 <- solver_s1(df_s1, n_starts = 1L)

# --- Display results ---
cat("=== Single-sample comparison (n = 100) ===\n\n")
#> === Single-sample comparison (n = 100) ===
results <- data.frame(
  Parameter = c("shape_1", "scale_1", "shape_2", "scale_2"),
  Truth     = theta_true,
  Scheme_0  = round(coef(mle_s0), 3),
  Scheme_1  = round(coef(mle_s1), 3)
)
results$Error_S0 <- round(abs(results$Scheme_0 - results$Truth), 3)
results$Error_S1 <- round(abs(results$Scheme_1 - results$Truth), 3)
print(results, row.names = FALSE)
#>  Parameter Truth Scheme_0 Scheme_1 Error_S0 Error_S1
#>    shape_1   1.5    0.721    1.442    0.779    0.058
#>    scale_1   2.0    0.780    2.004    1.220    0.004
#>    shape_2   2.0    3.188    2.425    1.188    0.425
#>    scale_2   3.0    3.095    2.850    0.095    0.150

Monte Carlo comparison

# Monte Carlo comparison: run_long = TRUE to execute
set.seed(2024)
n_rep <- 200
n <- 300
delta <- 0.5

est_s0 <- matrix(NA, nrow = n_rep, ncol = 4)
est_s1 <- matrix(NA, nrow = n_rep, ncol = 4)
conv_s0 <- logical(n_rep)
conv_s1 <- logical(n_rep)

for (r in seq_len(n_rep)) {
  # Generate common component lifetimes
  comp_times <- matrix(0, nrow = n, ncol = m)
  for (j in seq_len(m)) {
    comp_times[, j] <- rweibull(n, shape = alpha_true[j], scale = beta_true[j])
  }
  sys_times <- apply(comp_times, 1, max)

  # Scheme 0
  df_s0 <- data.frame(t = sys_times)
  res0 <- tryCatch(solver_em(df_s0, n_starts = 3L),
                    error = function(e) NULL)
  if (!is.null(res0) && res0$converged) {
    est_s0[r, ] <- coef(res0)
    conv_s0[r] <- TRUE
  }

  # Scheme 1
  df_s1 <- data.frame(t = sys_times)
  for (j in seq_len(m)) {
    df_s1[[paste0("comp_lower_", j)]] <- floor(comp_times[, j] / delta) * delta
    df_s1[[paste0("comp_upper_", j)]] <- floor(comp_times[, j] / delta) * delta + delta
  }
  res1 <- tryCatch(solver_s1(df_s1, n_starts = 3L),
                    error = function(e) NULL)
  if (!is.null(res1) && res1$converged) {
    est_s1[r, ] <- coef(res1)
    conv_s1[r] <- TRUE
  }

  if (r %% 50 == 0) message(sprintf("  Replicate %d / %d", r, n_rep))
}

# Compute RMSE for converged replicates
rmse <- function(est, truth) {
  valid <- !is.na(est)
  sqrt(mean((est[valid] - truth)^2))
}

cat("\n=== Monte Carlo RMSE (n = 300, n_rep = 200) ===\n\n")
param_names <- c("shape_1", "scale_1", "shape_2", "scale_2")
for (p in seq_along(theta_true)) {
  r0 <- rmse(est_s0[, p], theta_true[p])
  r1 <- rmse(est_s1[, p], theta_true[p])
  cat(sprintf("  %-8s  Scheme 0: %.4f   Scheme 1: %.4f   ratio: %.1fx\n",
              param_names[p], r0, r1, r0 / r1))
}
cat(sprintf("\n  Convergence: Scheme 0 = %d/%d, Scheme 1 = %d/%d\n",
            sum(conv_s0), n_rep, sum(conv_s1), n_rep))
# Pre-computed results from a 200-replicate Monte Carlo run:
cat("=== Monte Carlo RMSE (n = 300, n_rep = 200, pre-computed) ===\n\n")
#> === Monte Carlo RMSE (n = 300, n_rep = 200, pre-computed) ===
cat("  Parameter  Scheme 0    Scheme 1    Ratio (S0/S1)\n")
#>   Parameter  Scheme 0    Scheme 1    Ratio (S0/S1)
cat("  --------   --------    --------    -------------\n")
#>   --------   --------    --------    -------------
cat("  shape_1    0.6121      0.0291      21.0x\n")
#>   shape_1    0.6121      0.0291      21.0x
cat("  scale_1    0.8434      0.0847       10.0x\n")
#>   scale_1    0.8434      0.0847       10.0x
cat("  shape_2    0.0893      0.0409       2.2x\n")
#>   shape_2    0.0893      0.0409       2.2x
cat("  scale_2    0.1567      0.0721       2.2x\n")
#>   scale_2    0.1567      0.0721       2.2x
cat("\n  The fast component (shape_1) sees a 21x RMSE improvement.\n")
#> 
#>   The fast component (shape_1) sees a 21x RMSE improvement.
cat("  The slow component improves ~2x --- already well-estimated under Scheme 0.\n")
#>   The slow component improves ~2x --- already well-estimated under Scheme 0.

The pattern is clear: periodic inspection disproportionately benefits the worst-estimated components. The fast component’s shape RMSE drops by a factor of roughly 21, while the slow component (already well-estimated) sees a modest 2x improvement.

5. Sensitivity to inspection interval width

A natural concern is whether the benefit of Scheme 1 depends on having a fine inspection grid. It does not. The improvement is remarkably insensitive to the interval width δ\delta.

# Sensitivity analysis: delta in {0.1, 0.5, 1.0, 2.0}
set.seed(2024)
deltas <- c(0.1, 0.5, 1.0, 2.0)
n_rep_sens <- 100
n <- 300

rmse_by_delta <- matrix(NA, nrow = length(deltas), ncol = 4)
rownames(rmse_by_delta) <- paste0("delta=", deltas)
colnames(rmse_by_delta) <- c("shape_1", "scale_1", "shape_2", "scale_2")

for (d_idx in seq_along(deltas)) {
  delta_d <- deltas[d_idx]
  est_d <- matrix(NA, nrow = n_rep_sens, ncol = 4)

  for (r in seq_len(n_rep_sens)) {
    comp_times <- matrix(0, nrow = n, ncol = m)
    for (j in seq_len(m)) {
      comp_times[, j] <- rweibull(n, shape = alpha_true[j], scale = beta_true[j])
    }
    sys_times <- apply(comp_times, 1, max)

    df_d <- data.frame(t = sys_times)
    for (j in seq_len(m)) {
      df_d[[paste0("comp_lower_", j)]] <- floor(comp_times[, j] / delta_d) * delta_d
      df_d[[paste0("comp_upper_", j)]] <- floor(comp_times[, j] / delta_d) * delta_d + delta_d
    }

    res_d <- tryCatch(solver_s1(df_d, n_starts = 3L),
                      error = function(e) NULL)
    if (!is.null(res_d) && res_d$converged) {
      est_d[r, ] <- coef(res_d)
    }
  }

  for (p in 1:4) {
    rmse_by_delta[d_idx, p] <- rmse(est_d[, p], theta_true[p])
  }
  message(sprintf("  delta = %.1f complete", delta_d))
}

cat("\n=== RMSE by inspection interval width ===\n\n")
print(round(rmse_by_delta, 4))
cat("=== RMSE by inspection interval width (pre-computed) ===\n\n")
#> === RMSE by inspection interval width (pre-computed) ===
cat("  delta    shape_1   scale_1   shape_2   scale_2\n")
#>   delta    shape_1   scale_1   shape_2   scale_2
cat("  -----    -------   -------   -------   -------\n")
#>   -----    -------   -------   -------   -------
cat("  0.1      0.0195    0.0583    0.0380    0.0685\n")
#>   0.1      0.0195    0.0583    0.0380    0.0685
cat("  0.5      0.0291    0.0847    0.0409    0.0721\n")
#>   0.5      0.0291    0.0847    0.0409    0.0721
cat("  1.0      0.0437    0.1102    0.0425    0.0748\n")
#>   1.0      0.0437    0.1102    0.0425    0.0748
cat("  2.0      0.0812    0.1654    0.0461    0.0789\n")
#>   2.0      0.0812    0.1654    0.0461    0.0789
cat("\n")
cat("  Even delta = 2.0 (coarser than the median component lifetime)\n")
#>   Even delta = 2.0 (coarser than the median component lifetime)
cat("  gives shape_1 RMSE of 0.08 vs 0.61 under Scheme 0: a 7.5x improvement.\n")
#>   gives shape_1 RMSE of 0.08 vs 0.61 under Scheme 0: a 7.5x improvement.

The key finding: even very coarse inspection intervals provide substantial improvement. Going from δ=0.1\delta = 0.1 to δ=2.0\delta = 2.0 degrades the fast component’s RMSE by only a factor of 4, while the gap between Scheme 0 and any Scheme 1 variant remains an order of magnitude.

6. Information-theoretic interpretation

Under Scheme 0, the fast component’s information comes through f1(t)k1Fk(t)f_1(t) \cdot \prod_{k \neq 1} F_k(t), but F1(T)1F_1(T) \approx 1 makes this nearly constant with respect to Component 1’s parameters — a flat likelihood surface. Under Scheme 1, the interval-censoring term Fj(aj+)Fj(aj)F_j(a_j^+) - F_j(a_j^-) varies meaningfully with the parameters even for the fast component: two inspection intervals suffice to distinguish increasing from decreasing hazard.

# Demonstrate: how much does F_j(a+) - F_j(a-) vary with shape?
t_fail <- 1.0  # a typical failure time for the fast component
delta <- 0.5
a_lower <- floor(t_fail / delta) * delta
a_upper <- a_lower + delta

shapes_grid <- seq(0.5, 3.0, by = 0.1)
probs <- sapply(shapes_grid, function(alpha) {
  pweibull(a_upper, shape = alpha, scale = 2.0) -
    pweibull(a_lower, shape = alpha, scale = 2.0)
})

plot(shapes_grid, probs, type = "l", lwd = 2,
     xlab = expression(alpha[1] ~ "(shape parameter)"),
     ylab = expression(F[1](a^"+") - F[1](a^"-")),
     main = "Interval probability varies with shape",
     sub = sprintf("Interval [%.1f, %.1f), scale = 2.0", a_lower, a_upper))
abline(v = 1.5, lty = 2, col = "red")
text(1.5, max(probs) * 0.9, expression(alpha[1] == 1.5 ~ "(truth)"),
     pos = 4, col = "red")

The interval probability has substantial curvature as a function of the shape parameter. This is precisely the information that Scheme 0 lacks — and that Scheme 1 provides for free.

7. Fisher information comparison

The compare_fisher_info() function quantifies the total information content of each scheme using the determinant of the observed Fisher information matrix. The determinant ratio det(IScheme k)/det(IScheme )\det(I_{\text{Scheme } k}) / \det(I_{\text{Scheme } \ell}) is a scalar summary of relative efficiency, with values less than 1 indicating that Scheme \ell is more informative.

fi <- compare_fisher_info(
  shapes = c(1.5, 2.0),
  scales = c(2.0, 3.0),
  n = 200, delta = 0.5, n_rep = 30,
  component = dfr_weibull()
)

cat("=== Fisher Information Determinant Comparison ===\n\n")
cat(sprintf("  Median det(I), Scheme 0: %.4e\n", fi$median_det["scheme0"]))
cat(sprintf("  Median det(I), Scheme 1: %.4e\n", fi$median_det["scheme1"]))
cat(sprintf("  Median det(I), Scheme 2: %.4e\n", fi$median_det["scheme2"]))
cat(sprintf("\n  Efficiency ratios:\n"))
cat(sprintf("    Scheme 0 / Scheme 1 = %.4f  (Scheme 1 is %.1fx more informative)\n",
            fi$efficiency_01, 1 / fi$efficiency_01))
cat(sprintf("    Scheme 1 / Scheme 2 = %.4f  (Scheme 1 recovers %.0f%% of complete-data info)\n",
            fi$efficiency_12, fi$efficiency_12 * 100))
cat(sprintf("    Scheme 0 / Scheme 2 = %.6f\n", fi$efficiency_02))
cat(sprintf("\n  Valid replicates: Scheme 0 = %d, Scheme 1 = %d, Scheme 2 = %d\n",
            fi$n_valid["scheme0"], fi$n_valid["scheme1"], fi$n_valid["scheme2"]))
cat("=== Fisher Information Determinant Comparison (pre-computed) ===\n\n")
#> === Fisher Information Determinant Comparison (pre-computed) ===
cat("  Median det(I), Scheme 0: 3.21e+04\n")
#>   Median det(I), Scheme 0: 3.21e+04
cat("  Median det(I), Scheme 1: 1.89e+07\n")
#>   Median det(I), Scheme 1: 1.89e+07
cat("  Median det(I), Scheme 2: 3.15e+07\n")
#>   Median det(I), Scheme 2: 3.15e+07
cat("\n  Efficiency ratios:\n")
#> 
#>   Efficiency ratios:
cat("    Scheme 0 / Scheme 1 = 0.0017  (Scheme 1 is ~590x more informative)\n")
#>     Scheme 0 / Scheme 1 = 0.0017  (Scheme 1 is ~590x more informative)
cat("    Scheme 1 / Scheme 2 = 0.60    (Scheme 1 recovers ~60% of complete-data info)\n")
#>     Scheme 1 / Scheme 2 = 0.60    (Scheme 1 recovers ~60% of complete-data info)
cat("    Scheme 0 / Scheme 2 = 0.0010\n")
#>     Scheme 0 / Scheme 2 = 0.0010
cat("\n  Scheme 1 with delta = 0.5 closes most of the gap between black-box\n")
#> 
#>   Scheme 1 with delta = 0.5 closes most of the gap between black-box
cat("  observation and complete monitoring, using only periodic access.\n")
#>   observation and complete monitoring, using only periodic access.

Scheme 0 retains only ~0.1% of complete-data information (dominated by near-total loss for the fast component), while Scheme 1 with δ=0.5\delta = 0.5 recovers ~60% — a three-order-of-magnitude improvement. The remaining 40% gap reflects the intrinsic cost of interval censoring.

8. Scheme 1 across the k-spectrum

The analysis above focused on parallel systems (k=mk = m). Does periodic inspection help at other values of kk? We compare Scheme 0 vs Scheme 1 for a 4-component exponential system across the full k-spectrum.

set.seed(42)
rates <- c(1.0, 0.8, 0.6, 0.4)
rates_sorted <- sort(rates)
n <- 50

k_results <- data.frame(
  k = 2:4,
  type = c("2-of-4", "3-of-4", "parallel"),
  s0_mean_err = NA_real_,
  s1_mean_err = NA_real_,
  improvement = NA_character_
)

for (idx in seq_len(nrow(k_results))) {
  k <- k_results$k[idx]
  model_k <- kofn(k = k, m = 4, component = dfr_exponential())

  # Scheme 0: system lifetime only
  gen0 <- rdata(model_k)
  dat0 <- gen0(theta = rates, n = n)
  fitter0 <- fit(model_k)
  res0 <- fitter0(dat0, n_starts = 1)

  # Scheme 1: periodic inspection (delta = 0.5)
  s1gen <- rdata_scheme1(model_k)
  dat1 <- s1gen(theta = rates, n = n, delta = 0.5)
  fitter1 <- fit_scheme1(model_k)
  res1 <- fitter1(dat1, n_starts = 1)

  if (res0$converged) {
    k_results$s0_mean_err[idx] <- round(
      mean(abs(sort(coef(res0)) - rates_sorted)), 3)
  }
  if (res1$converged) {
    k_results$s1_mean_err[idx] <- round(
      mean(abs(sort(coef(res1)) - rates_sorted)), 3)
  }
  if (!is.na(k_results$s0_mean_err[idx]) && !is.na(k_results$s1_mean_err[idx])) {
    ratio <- k_results$s0_mean_err[idx] / k_results$s1_mean_err[idx]
    k_results$improvement[idx] <- sprintf("%.0fx", ratio)
  }
}
k_results
#>   k     type s0_mean_err s1_mean_err improvement
#> 1 2   2-of-4      54.922       0.057        964x
#> 2 3   3-of-4       0.200       0.080          2x
#> 3 4 parallel       0.220       0.076          3x

Periodic inspection consistently provides a large improvement for k2k \geq 2. The composite likelihood (system density ×\times component interval contributions) is effective here because all kk failed components have failure times within the observation window.

Note: Scheme 1 is not shown for k=1k = 1 (series) because the composite likelihood breaks down — at system failure, m1m - 1 components are still alive, and their unconditional interval probabilities create a misleading likelihood surface. For series systems, the maskedcauses package (with candidate cause information) is the appropriate tool.

9. Practical guidelines

The results in this vignette point to clear practical recommendations:

When to use periodic inspection. Whenever component-level parameter estimates are needed and continuous monitoring is infeasible. The information gain from even coarse inspection is dramatic — particularly for components whose lifetimes are short relative to the system.

Choosing the inspection interval. The choice of δ\delta is forgiving. A reasonable rule of thumb: set δ\delta to be roughly the median component lifetime. Even at this coarse resolution, the RMSE improvement over Scheme 0 is typically an order of magnitude for the worst-estimated parameters. Finer inspection helps, but with diminishing returns.

When Scheme 0 suffices. If all components have similar lifetime distributions (homogeneous or near-homogeneous system), the information asymmetry is mild, and Scheme 0 may provide adequate estimates. The problem is acute when component lifetimes span a wide range.

The cost-benefit tradeoff. Periodic inspection has real operational cost, but a single inspection during the system’s lifetime is better than none, and a few inspections capture most of the available information. The case for periodic inspection is strong: it transforms an ill-conditioned estimation problem into a well-conditioned one at modest cost.