---
title: "Tutorial 2: brms, Clusters and Joint Contrasts"
output:
  rmarkdown::html_vignette:
    css: ijse.css
vignette: >
  %\VignetteIndexEntry{Tutorial 2: brms, Clusters and Joint Contrasts}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r brms-setup, include=FALSE}
library(IJSE)
stopifnot(requireNamespace("brms", quietly = TRUE))
options(width = 62)
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
se_table <- function(x) {
  round(data.frame(Estimate = coef(x),
    Posterior_SD = x$posterior_sd, IJ_SE = x$se), 3)
}
```

## What you will learn

This tutorial uses two installed, synthetic brms fits. You can run all evaluated
code without compiling Stan or downloading data. Install brms separately if
needed. The optional refit at the end requires cmdstanr, CmdStan and a compiler.

Both examples fit a working asymmetric-Laplace median regression,
$y\sim 1+x$, with identity link, estimated scale, and fixed quantile 0.5. Data
have generating conditional median $0.5+0.7x$. The independent example has
heteroscedastic normal errors. The clustered example has 30 groups of ten, with
within-group correlation 0.6 in the generated errors. The fitted likelihood uses
working independence and no random effects. These are worked examples, rather
than an additional coverage study.

## Load a fit together with its sampling IDs

```{r brms-load}
path <- system.file("extdata", "tutorial",
  "median-examples.rds", package = "IJSE")
stopifnot(nzchar(path))
examples <- readRDS(path)
ind <- examples$independent
cl <- examples$clustered
stopifnot(inherits(ind$fit, "brmsfit"),
          inherits(cl$fit, "brmsfit"))
```

The bundle keeps `fit`, original fitting `data`, and `cluster` separately.
brms can discard a variable unused by its model formula, so a missing
`fit$data$group` must not silently become your independent-observation default.
Use the saved IDs and verify the fitting rows.

```{r brms-check-rows}
fit <- cl$fit
ids <- cl$cluster
stopifnot(length(ids) == nrow(cl$data),
  identical(ids, cl$data$group),
  isTRUE(all.equal(fit$data$x, cl$data$x)),
  isTRUE(all.equal(fit$data$y, cl$data$y)),
  !anyNA(ids), length(unique(ids)) == 30L)
```

For your own univariate regression, select complete fitting rows explicitly
before fitting and save their original row IDs and cluster IDs. A length check
alone cannot establish substantive row order. The checks above apply to this
ordinary regression; likelihoods with special observation orderings require
separate care.

## Begin with independent observations

```{r brms-independent}
independent <- ijse(ind$fit, diagnostics = "posterior")
se_table(independent)
confint(independent)
stopifnot(!independent$clustered,
          independent$n_units == 200L)
stopifnot(isTRUE(all.equal(
  IJ_se(ind$fit), independent$se)))
```

By default, `ijse()` selects parameters whose names start with `b_`.
`coef()` returns posterior expectations. The original `IJ_se()` returns only
the IJ SE vector; its pooled inference is the same and it skips discarded
diagnostics. Select exact parameter names when your transformation needs a
particular subset or a distributional parameter.

## Use clusters as the sampling units

Compare two calculations on the *same* clustered-data fit. This changes the
resampling units, not the posterior draws or point estimates.

```{r brms-clustered}
observation_ij <- ijse(fit, diagnostics = "none")
cluster_ij <- ijse(fit, cluster_var = ids,
  save_influence = TRUE, diagnostics = "all")
se_table(cluster_ij)
data.frame(Observation_IJ = observation_ij$se,
           Cluster_IJ = cluster_ij$se)
stopifnot(cluster_ij$clustered,
  cluster_ij$n_obs == 300L, cluster_ij$n_units == 30L,
  isTRUE(all.equal(coef(observation_ij), coef(cluster_ij))))
```

Cluster aggregation sums likelihood sensitivities within each group before
forming sampling covariance. It treats groups as independent sampling units.
It does not change the working likelihood, fit random effects, or automatically
validate arbitrary hierarchical models. Its normal intervals also do not supply
a universal correction for few or highly unequal clusters.

## Compute predictions and a joint contrast

For this identity-link median model, the predictor at fixed $x=1$ is the
intercept plus the slope. At $x=-1$, it is the intercept minus the slope. Compute
both on every draw, with their difference, so all covariance terms are retained.
These are posterior expectations of fixed-reference predictors; estimating a
reference distribution would add another source of sampling uncertainty.

```{r brms-transform}
predictions <- ijse(fit, cluster_var = ids,
  variables = c("b_Intercept", "b_x"),
  transform = function(d) {
    cbind(at_minus1 = d[, "b_Intercept"] - d[, "b_x"],
          at_plus1 = d[, "b_Intercept"] + d[, "b_x"],
          difference = 2 * d[, "b_x"])
  }, diagnostics = "posterior")
se_table(predictions)
confint(predictions, parm = "difference", level = 0.90)
```

The contrast has coefficient vector $(0,2)$ on the original intercept and slope.
Verify its SE using the original full covariance matrix.

```{r brms-contrast-check}
a <- c(b_Intercept = 0, b_x = 2)
v <- vcov(cluster_ij)[names(a), names(a), drop = FALSE]
contrast_se <- sqrt(drop(t(a) %*% v %*% a))
stopifnot(isTRUE(all.equal(
  unname(predictions$se["difference"]), contrast_se)))
```

Do not add marginal SEs to obtain a contrast SE. Use the covariance matrix or
transform the joint draws. A contrast between predictors is an associational
quantity here; the simulation and model do not make it a causal effect.

## Select draws without losing alignment

`draw_ids` uses post-warmup draw indices, ordered by iteration within chain,
then chain. IJSE applies the same indices to parameter and likelihood extraction.
For this example there are 1,000 retained draws in each of two chains. The
following selects half of each chain for a numerical stability comparison.

```{r brms-subset}
arr <- posterior::as_draws_array(fit,
  variable = c("b_Intercept", "b_x"), regex = FALSE)
n_iter <- posterior::niterations(arr)
n_chain <- posterior::nchains(arr)
keep <- unlist(lapply(seq_len(n_chain), function(j) {
  (j - 1L) * n_iter + seq.int(1L, n_iter, by = 2L)
}))
subset_result <- ijse(fit, cluster_var = ids,
  draw_ids = keep, diagnostics = "posterior")
stopifnot(subset_result$n_draws == length(keep))
data.frame(All_draws = cluster_ij$se,
           Selected_draws = subset_result$se)
```

The estimates can differ because fewer posterior draws estimate the covariance
less precisely. This comparison does not imply thinning is a precision remedy.
Inspect convergence and collect sufficient effective posterior information.

## Verify streamed likelihood extraction

```{r brms-stream}
streamed <- ijse(fit, cluster_var = ids,
  block_size = 40, diagnostics = "none")
stopifnot(isTRUE(all.equal(vcov(streamed),
                         vcov(cluster_ij))))
```

This univariate likelihood supports streaming. The tested brms implementation
does not support IJSE streaming for regression ARMA models, and multivariate
streaming is rejected explicitly. Use `block_size = NULL` in those cases.
Covariance ARMA models require explicit joint series likelihoods through
`ijse_draws()`: their native brms output contains LOO conditional densities.
Discrete truncated models also require explicit likelihood matrices because
backend lower-bound normalization is not validated. Backend prediction objects
can still consume memory when likelihoods are blocked.

For measurement-error predictors, fit with
`save_pars = brms::save_pars(latent = TRUE)` to retain matching latent draws.
Without them, IJSE rejects prediction-time resampling. The native likelihood
still conditions on those latents; an observed-data analysis needs a likelihood
that integrates them. Missing-response models can return NA likelihood columns;
these require a separately derived observed-data likelihood, rather than
dropping incomplete cases or substituting zero for NA entries indiscriminately.

## Review diagnostics and save the analysis

```{r brms-diagnostics}
data.frame(Rhat = cluster_ij$diagnostics$rhat,
  Bulk_ESS = cluster_ij$diagnostics$ess_bulk)
cluster_ij$diagnostics$chain_relative_range
```

These R-hat and ESS values refer to the selected quantities. Check the original
fit's diagnostics too, including varying nuisance parameters and divergent
transitions. Chain-specific IJ ranges are stability checks, not calibrated
Monte Carlo SEs of IJ covariance. The two tutorial fits have their generation
seeds, fit seeds, fitting settings and diagnostic record in the installed data
bundle's companion files.

```{r brms-save, eval=FALSE, purl=FALSE}
saveRDS(list(result = cluster_ij, cluster = ids,
  data = cl$data, transform = predictions$call,
  session = sessionInfo()), "my-ijse-analysis.rds")
```

For reporting, identify the estimand, working likelihood, 30 independent clusters,
normal interval reference, posterior settings and diagnostic limitations.
A single example's interval width does not demonstrate repeated-sampling
coverage. The separate `ijse-study` vignette evaluates that question.

## Optional: fit the model yourself

The following code is displayed but not run while building the tutorial. It
refits this example's data and preserves the IDs separately. Using another
backend, seed or toolchain can change finite-draw results.

```{r brms-refit, eval=FALSE, purl=FALSE}
model_data <- cl$data
model_ids <- model_data$group
refit <- brms::brm(
  brms::bf(y ~ x, quantile = 0.5), data = model_data,
  family = brms::asym_laplace(link_quantile = "identity"),
  prior = c(
    brms::set_prior("normal(0, 2.5)", class = "b"),
    brms::set_prior("normal(0, 2.5)", class = "Intercept"),
    brms::set_prior("exponential(1)", class = "sigma")),
  backend = "cmdstanr", chains = 2, cores = 2,
  iter = 2000, warmup = 1000, seed = 41302,
  control = list(adapt_delta = 0.99, max_treedepth = 12))
refit_ij <- ijse(refit, cluster_var = model_ids)
stopifnot(refit_ij$clustered, refit_ij$n_units == 30L)
```

To regenerate both installed examples, run
`tools/generate-tutorial-data.R` from the source release with an output directory
and model-cache directory. Refit execution is separate from package checks and
from the coverage simulation in the study.
