---
title: "Tutorial 3: Diagnostics, Influence and Troubleshooting"
output:
  rmarkdown::html_vignette:
    css: ijse.css
vignette: >
  %\VignetteIndexEntry{Tutorial 3: Diagnostics, Influence and Troubleshooting}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r diag-setup, include=FALSE}
library(IJSE)
options(width = 62)
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
show_error <- function(expr) {
  message <- tryCatch({force(expr); NULL},
    error = function(e) conditionMessage(e))
  if (is.null(message)) stop("Expected an error in this example.")
  message
}
```

## What you will learn

This tutorial runs without brms or Stan. You will choose a diagnostic mode,
preserve unequal chain metadata, interpret saved influence values, and reproduce
common input errors. Every error demonstration catches its intended failure, so
the complete script can run successfully.

## Set up clustered observations and a working posterior

Generate 20 independent groups of five with within-group correlation 0.6. Use a
normal working-independence likelihood with observation variance one and prior
variance four. As in Tutorial 1, its posterior is available exactly.

```{r diag-data}
set.seed(452)
group <- rep(paste0("g", seq_len(20)), each = 5)
u <- rnorm(20)
y <- 0.5 + sqrt(0.6) * rep(u, each = 5) +
     sqrt(0.4) * rnorm(length(group))
v <- 1 / (length(y) + 1 / 4)
m <- v * sum(y)
mu <- rnorm(2000, m, sqrt(v))
draws <- cbind(mu = mu)
ll <- vapply(y, function(yi) dnorm(yi, mu, log = TRUE),
             numeric(length(mu)))
chain <- rep(c("A", "B"), each = 1000)
```

The two chains here are independently generated normal draws, assigned labels
to demonstrate the interface. They are not outputs from an MCMC sampler.

## Choose how much diagnostic computation to do

```{r diag-modes}
full <- ijse_draws(draws, ll, cluster_var = group,
  chain_id = chain, save_influence = TRUE)
posterior_only <- ijse_draws(draws, ll, cluster_var = group,
  chain_id = chain, diagnostics = "posterior")
pooled <- ijse_draws(draws, ll, cluster_var = group,
  chain_id = chain, diagnostics = "none")
stopifnot(isTRUE(all.equal(vcov(full), vcov(posterior_only))),
          isTRUE(all.equal(vcov(full), vcov(pooled))),
          is.null(posterior_only$diagnostics$chain_se),
          is.null(pooled$diagnostics$rhat))
```

The default `"all"` calculates chain-specific IJ SEs and quantity-level posterior
diagnostics. `"posterior"` retains R-hat and ESS without additional chain IJ
calculations. `"none"` computes pooled inference only. The modes do not change
the pooled estimator or covariance. For a plain matrix, supply `chain_id` when
you want chain diagnostics; an unlabeled matrix does not contain chain identity.

```{r diag-values}
full$diagnostics$chain_se
full$diagnostics$chain_relative_range
full$diagnostics$chain_ids
c(Rhat = unname(full$diagnostics$rhat),
  Bulk_ESS = unname(full$diagnostics$ess_bulk),
  Tail_ESS = unname(full$diagnostics$ess_tail))
```

The relative range is the maximum minus minimum chain IJ SE divided by the pooled
IJ SE. It has no universal pass threshold and is not a calibrated Monte Carlo
SE. R-hat and ESS diagnose the selected quantities, rather than every
parameter-likelihood covariance. For an actual brms fit, inspect sampler
convergence, divergences and nuisance parameters as well.

## Preserve unequal retained chains

A posterior draws data frame retains `.chain`, `.iteration` and `.draw`. Remove
one draw while preserving that metadata and use the same row selection for the
likelihood. Within each chain, retain iteration order.

```{r diag-unequal}
arr <- posterior::as_draws_array(array(mu, c(1000, 2, 1),
  dimnames = list(NULL, NULL, "mu")))
df <- posterior::as_draws_df(arr)
keep <- !(df$.chain == 2 & df$.iteration == 1000)
unequal <- ijse_draws(df[keep, ], ll[keep, , drop = FALSE],
                     cluster_var = group)
stopifnot(unequal$n_draws == 1999L,
  is.null(unequal$diagnostics$rhat),
  nrow(unequal$diagnostics$chain_se) == 2L)
```

The pooled result and separate chain IJ calculations are available. The current
R-hat/ESS implementation requires equal chain lengths with at least four draws
each, so these entries are omitted. A missing diagnostic is not a failed test or
proof of convergence. A matrix whose metadata were lost cannot reconstruct
unequal chain identities reliably; supply correct `chain_id` explicitly.

## Interpret influence as local sensitivity

```{r diag-influence}
case_derivatives <- influence(full) / full$n_units
id <- colnames(case_derivatives)[
  which.max(abs(case_derivatives["mu", ]))]
c(Cluster = id,
  Derivative = format(case_derivatives["mu", id]))
```

Exact original cluster IDs are retained in `full$unit_ids`, named by the
matrix's display labels. Distinct numeric IDs can otherwise print alike;
display names are made unique and should not be parsed back into IDs.
`full$diagnostics$chain_ids` provides the same mapping for enabled chain diagnostics.

`influence(full)` is a quantities-by-units matrix. Divide by the number of units
to obtain sample-covariance estimates of derivatives with respect to likelihood
weights. A positive derivative means that upweighting this group locally raises
the posterior expectation. Downweighting has the opposite sign.

These returned values are uncentered case-weight sensitivities multiplied by
the unit count; their row means need not be zero. IJ covariance centers across
units when constructing the sampling variance. The following reconstructs it.

```{r diag-covariance}
stopifnot(isTRUE(all.equal(
  cov(t(influence(full))) / full$n_units, vcov(full))))
```

A small weight change $\Delta w$ gives the local approximation
$\Delta T\approx C_j\Delta w$. Deleting a whole group is a finite perturbation
and may differ substantially. Large absolute sensitivity identifies a case to
inspect, rather than a reason to remove it.

## Independently check the sign and scale

Perturb a group's likelihood weight by a small amount and recompute the
normalized weighted draw mean. Subtract the maximum log weight before
exponentiating to keep the calculation stable.

```{r diag-derivative}
contribution <- rowSums(ll[, group == id, drop = FALSE])
weighted_mean <- function(delta) {
  log_weight <- delta * contribution
  weight <- exp(log_weight - max(log_weight))
  sum(weight * mu) / sum(weight)
}
h <- 1e-5
finite_difference <- (weighted_mean(h) - weighted_mean(-h)) /
                     (2 * h)
s <- nrow(draws)
adjusted_difference <- finite_difference * s / (s - 1)
c(Covariance = case_derivatives["mu", id],
  Adjusted_difference = adjusted_difference)
stopifnot(isTRUE(all.equal(
  unname(case_derivatives["mu", id]), adjusted_difference,
  tolerance = 1e-7)))
```

The normalization matters when checking exact finite-draw equality. IJSE uses
sample covariance with denominator $S-1$; differentiating a normalized empirical
weighted mean uses denominator $S$. The adjustment above reconciles them. The
reweighted mean is a numerical check on these draws, not a replacement for
refitting after a substantial data change.

## Reproduce common input failures

Missing cluster IDs must be repaired using the original sampling records. Do
not silently omit affected observations after fitting the posterior.

```{r diag-missing}
bad_ids <- group
bad_ids[3] <- NA_character_
show_error(ijse_draws(draws, ll, cluster_var = bad_ids))
factor_ids <- factor(bad_ids, exclude = NULL)
c(AnyNA_factor = anyNA(factor_ids),
  AnyNA_values = anyNA(as.character(factor_ids)))
show_error(ijse_draws(draws, ll, cluster_var = factor_ids))
```

A used explicit NA factor level is still a missing sampling ID, even when
`anyNA()` on the factor returns FALSE. IJSE checks this case explicitly.
The literal character value `"NA"` is a different, nonmissing ID.

A one-column callback must return a matrix. R's default subsetting can drop that
dimension, so preserve it explicitly.

```{r diag-dimensions}
show_error(ijse_draws(draws, function(ii) ll[, ii],
  n_obs = length(y), block_size = 1))
fixed <- ijse_draws(draws,
  function(ii) ll[, ii, drop = FALSE], n_obs = length(y),
  block_size = 1, cluster_var = group, diagnostics = "none")
stopifnot(isTRUE(all.equal(vcov(fixed), vcov(full))))
```

Mismatched supplied row names are rejected. Unnamed inputs require equal care;
software cannot establish their semantic correspondence from dimensions alone.

```{r diag-order}
named_draws <- draws
named_ll <- ll
rownames(named_draws) <- paste0("s", seq_len(nrow(draws)))
rownames(named_ll) <- rev(rownames(named_draws))
show_error(ijse_draws(named_draws, named_ll))
show_error(ijse_draws(draws, ll, variables = "mu_typo"))
show_error(influence(pooled))
```

The last error occurs because influence retention is opt-in. Recompute with
`save_influence = TRUE` before calling `influence()`.

When draw rows are unlabeled, labels supplied by different likelihood blocks
must still agree with each other. This can expose a provider that changes draw
order partway through streaming.

```{r diag-cross-block-order}
block_ll <- ll
rownames(block_ll) <- paste0("s", seq_len(nrow(draws)))
misordered_reader <- function(ii) {
  block <- block_ll[, ii, drop = FALSE]
  if (min(ii) > 2) block <- block[nrow(block):1, , drop = FALSE]
  block
}
show_error(ijse_draws(draws, misordered_reader,
  n_obs = ncol(ll), block_size = 2))
```

The first labeled block establishes the reference across blocks. Agreement is
necessary but does not prove correspondence to unlabeled original draws.

## Check the sampling units, not only the dimensions

```{r diag-units}
observation_result <- ijse_draws(draws, ll,
  diagnostics = "none")
c(Observation_units = observation_result$n_units,
  Group_units = full$n_units)
stopifnot(isTRUE(all.equal(coef(observation_result),
                         coef(full))), full$n_units == 20L)
```

Omitting cluster IDs is a valid independent-observation request to the API. It
cannot know that your scientific design required groups. IDs of the correct
length can also be in the wrong order. Validate IDs against fitting-row keys and
inspect `clustered`, `n_obs`, `n_units` and the original records explicitly.
Renaming groups does not alter the estimator, and correctly permuting likelihood
columns together with their group IDs must leave it unchanged.

```{r diag-permutation}
perm <- rev(seq_along(y))
reordered <- ijse_draws(draws, ll[, perm, drop = FALSE],
  cluster_var = group[perm], diagnostics = "none")
stopifnot(isTRUE(all.equal(vcov(reordered), vcov(full))))
```

For an unexpected result, reproduce the smallest case, inspect original extraction
errors, check draw order and units, and compare a dense calculation with a small
streamed calculation. Preserve the input-generating code and session information.
Only then investigate numerical precision or methodological calibration. The
`ijse-study` vignette gives examples where correct computation still leaves
undercoverage from few clusters or prior bias.
