---
title: "**Per-group hierarchical anchors: Operational Guide**"
subtitle: "API, pre-flight (C7), coefficient summaries, and cross-group prediction (Block 6.5)"
author: "**José Mauricio Gómez Julián**"
date: "`r Sys.Date()`"
output:
  rmarkdown::html_vignette:
    toc: true
    toc_depth: 3
vignette: >
  %\VignetteIndexEntry{Per-group hierarchical anchors: Operational Guide}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(
  echo = TRUE, message = FALSE, warning = FALSE,
  collapse = TRUE, comment = "#>"
)
```

---

# **1. What this vignette covers**

Block 6.5 promotes the population-level reference $\theta_{\text{ref}}$ of the AMM canonical decomposition

$$\theta_i = \theta_{\text{ref}} + a(x_i) + b(x_i) \odot \theta_{\text{ref}} + W(\theta_{\text{ref}})\,x_i$$

from a single global scalar (Block 6 default) to a per-group vector $\theta_{\text{ref}}[g]$, $g = 1, \ldots, J_{\text{groups}}$, sampled hierarchically from $\mathrm{Normal}(\mu_{\theta_{\text{ref}}}, \sigma_{\theta_{\text{ref}}})$ with both hyperparameters estimated. The model under grouping is

$$\theta_i = \theta_{\text{ref}}[g_i] + a(x_i) + b(x_i) \odot \theta_{\text{ref}}[g_i] + W(\theta_{\text{ref}}[g_i])\,x_i, \qquad \theta_{\text{ref}}[g] \sim \mathrm{Normal}(\mu_{\theta_{\text{ref}}}, \sigma_{\theta_{\text{ref}}}),$$

where $\odot$ denotes the Hadamard (elementwise) product, coherent with the canonical notation of `vignette("v00_framework_overview", package = "gdpar")` §8.2 and `vignette("v01_amm_identifiability", package = "gdpar")` §3.3.

The user activates the grouping via the one-sided formula argument `group` of `gdpar()`. Default `group = NULL` reduces bit-exactly to the Block 6 scalar regime, so existing fits and tests do not change.

This vignette documents:

1. The `group` API and what changes inside the model.
2. The pre-flight condition (C7) that prevents structural aliasing of the per-group anchor with `a` or `b`.
3. The `coef()` schema under grouping: per-group anchors plus the population-level hyperparameters.
4. The `predict()` semantics for newdata that contains levels unseen at fit time.
5. The relationship with the parametrization toggle of `vop01_parametrization_toggle`.

---

# **2. API**

## **2.1. The `group` argument**

`group` is a one-sided formula identifying a single variable in `data`:

```{r api_group, eval=FALSE}
fit <- gdpar(
  formula = y ~ x1 + x2,
  family  = gdpar_family("gaussian"),
  amm     = amm_spec(a = ~ x1 + x2),
  data    = df,
  group   = ~ school    # promotes theta_ref to per-school anchor
)
```

Constraints accepted by the resolver `.resolve_group_argument()`:

- One-sided formula with **exactly one variable name**. Examples: `~ school`, `~ region`. Expressions such as `~ school + region` are rejected; if you need an interaction, materialize it as a column beforehand: `df$cell <- interaction(df$school, df$region); gdpar(..., group = ~ cell)`.
- The variable must exist in `data`, contain no NA values, and have at least one level. Internally it is coerced to a factor; the factor levels are stored in `fit$group_info$levels` and used later by `predict()` to map newdata.
- The resolver emits an informational warning of class `gdpar_grouping_warning` when at least one level has fewer than 5 observations: the hierarchical anchor is then dominated by shrinkage and per-group summaries should be interpreted with caution.

## **2.2. What changes inside the model**

- Stan-side declaration. `vector[J_groups] theta_ref` (univariate) or `array[J_groups] vector[p] theta_ref` (multivariate), with `mu_theta_ref` and `sigma_theta_ref` allocated only when `use_groups = 1`. The model branch is the standard hierarchical prior `theta_ref[g] ~ Normal(mu_theta_ref, sigma_theta_ref)` with weakly informative hyperpriors (default `normal(0, 2.5)` on `mu_theta_ref` and `student_t(3, 0, 1)` on `sigma_theta_ref`).
- Derived quantities. The `b_coef = c_b / theta_ref` derived quantity is **not reported** when `use_groups = 1`, because it would be ambiguous per group (different denominator per row). The structurally identified `c_b` is sampled and reported instead; users can derive `b_coef[j, g] = c_b[j] / theta_ref[g, k]` post-hoc per group if they need to.
- Prior schema. `gdpar_prior(sigma_theta_ref = "student_t(3, 0, 1)")` is the new hyperprior knob; the existing `theta_ref` prior now applies to `mu_theta_ref` under grouping (and stays applied to the scalar `theta_ref` when `group = NULL`).

---

# **3. Pre-flight condition (C7)**

When `group` is supplied, the pre-flight check `.check_group_aliasing_c7()` runs in addition to the standard Block 1 identifiability check. It enforces that neither `a` nor `b` contains columns that are linearly dependent with the group indicator $G$. The check has two layers:

1. **Constant-per-group detection.** Any column of $Z_a$ or $Z_b$ that is constant within every level of `group` is rank-deficient with $G$ and creates a perfect alias with $\theta_{\text{ref}}[g]$. This catches the most frequent mistake: including `factor(group)` (or any deterministic function of `group`) in `a` or `b`.
2. **Joint QR rank check.** For the centered design block $Z$, form $[G \mid Z]$, normalize column-wise, and verify $\mathrm{rank}([G \mid Z]) = \mathrm{ncol}([G \mid Z])$. This catches indirect aliases that the per-column check misses (a non-trivial linear combination of $Z$ columns matching a column of $G$).

On violation, `gdpar()` aborts with `gdpar_input_error` naming the offending columns. The user either removes them from the AMM spec or drops the `group` argument.

Defense in depth: the standard Block 1 identifiability check (`gdpar_check_identifiability()`) frequently catches the direct `factor(group)` case before C7 even runs, because $Z_a$ centered is rank-deficient with the implicit intercept that the model carries via $\theta_{\text{ref}}$. C7 covers the residual subtler aliases.

See `vignette("v01_amm_identifiability", package = "gdpar")` Section 6.6.2 for the formal statement and proofs.

---

# **4. Coefficient summary under grouping**

`coef(fit)` returns an object of class `gdpar_coef` with the following slots when `use_groups = 1`:

- `theta_ref`: data.frame with columns `(g, k, mean, q05, q50, q95)`. One row per `(group, coordinate)`. In the univariate case (`p = 1`), `k` is constant at `1`. The factor levels are stored in `fit$group_info$levels` for the mapping `g <-> name`.
- `mu_theta_ref`: data.frame with columns `(k, mean, q05, q50, q95)`. One row per coordinate. The population-level mean of the per-group anchors.
- `sigma_theta_ref`: data.frame with columns `(k, mean, q05, q50, q95)`. Same shape. The population-level scale.
- `a`, `b`, `W`: unchanged from Block 6.

Under `use_groups = 0` (default), the `theta_ref` data.frame keeps the Block 6 columns `(k, mean, q05, q50, q95)` without `g`, preserving backward compatibility bit-exactly.

`as.data.frame(coef(fit))` returns a long-tidy table that always includes the `g` column (NA when grouping is inactive) plus rows tagged `component = "mu_theta_ref"` and `component = "sigma_theta_ref"` when present.

Example (untested):

```{r coef_example, eval=FALSE}
cf <- coef(fit)
cf$theta_ref          # one row per (g, k)
cf$mu_theta_ref       # population-level mu
cf$sigma_theta_ref    # population-level sigma
as.data.frame(cf)     # long-tidy: component, g, k, identifier, x_name, mean, q05, q50, q95
```

The print method shows the population-level hyperparameter blocks first, followed by the per-group anchors and the per-component summaries.

---

# **5. Cross-group prediction**

`predict(fit, newdata = new_df, ...)` under a grouped fit resolves `new_df[[var_name]]` against `fit$group_info$levels` to obtain the per-row group index:

- **Known levels.** The corresponding posterior column `theta_ref[, g]` (or `theta_ref[, g, k]` in multivariate) is read draw by draw and used as $\theta_{\text{ref}}$.
- **Unseen levels.** The function emits a `gdpar_predict_unseen_group_warning` naming the offending levels and, for each affected row, draws $\theta_{\text{ref}}$ from the **marginal prior predictive** $\mathrm{Normal}(\mu_{\theta_{\text{ref}}}[s], \sigma_{\theta_{\text{ref}}}[s])$ per posterior draw $s$ (and per coordinate $k$ in multivariate). The prediction is therefore well-defined but its uncertainty includes the population-level variance, not just the within-group variance.

The newdata data.frame must contain the same grouping variable. If absent, `predict()` aborts with `gdpar_input_error`.

The b-block of the predictor uses `c_b` directly under grouping (consistent with the Stan model that does not report `b_coef` per group). Mathematically the formula is the same as in Block 6 with `Z_b %*% c_b` replacing `Z_b %*% b_coef * theta_ref`, since `c_b = theta_ref * b_coef` by construction.

Example (untested):

```{r predict_example, eval=FALSE}
new_df <- df[1:10, ]
new_df$school <- factor(c(rep("school_A", 8), rep("new_school_X", 2)),
                        levels = c(levels(df$school), "new_school_X"))
# predict() emits a single warning naming "new_school_X" and uses the
# prior predictive marginal for those two rows:
pred <- predict(fit, newdata = new_df, summary = "draws")
```

---

# **6. Interaction with the parametrization toggle**

The parametrization toggle (`parametrization`, `parametrization_a`, `parametrization_W`) discussed in `vignette("vop01_parametrization_toggle", package = "gdpar")` is **orthogonal** to grouping: it controls the CP/NCP geometry of the hierarchical scales `sigma_a` and `sigma_W` on the basis coefficients, not the hierarchical scale `sigma_theta_ref` on the per-group anchors. The per-group anchor is always sampled from a standard random-intercept hierarchy without an additional CP/NCP toggle.

In practice, when a fit shows divergences under grouping, the diagnostic order is:

1. Confirm pre-flight (C7) is satisfied (otherwise the divergences are structural, not geometric).
2. Inspect `fit$parametrization$meta` for the per-component CP/NCP decision; switch to the alternative parametrization with the standard toggle if `sigma_a` or `sigma_W` is the culprit.
3. If the divergences persist and concentrate on `theta_ref[g]`, consider tightening `gdpar_prior(sigma_theta_ref = ...)`, since with very few observations per group the random-intercept funnel can be severe regardless of the per-component parametrization.

---

# **7. Reproducibility checklist**

- The `group` argument is part of the call recorded in `fit$call`. Re-running the call with the same seed and the same `data` (including the same factor levels for the grouping variable) reproduces the fit bit-exactly.
- `fit$group_info` stores the resolved metadata: `var_name`, `levels`, and `group_id` (the integer vector of length `n`). The mapping `g <-> name` is therefore portable across sessions through the saved `gdpar_fit` object.
- `coef(fit)$group_levels` returns the same character vector, useful for plotting per-group anchors with the original labels.

---

# **8. See also**

- `vignette("v01_amm_identifiability", package = "gdpar")` Section 6.6.2 for the formal statement of (C7).
- `vignette("v03_special_cases", package = "gdpar")` Section 4.1 for the random-intercept canonization.
- `vignette("vop01_parametrization_toggle", package = "gdpar")` for the CP/NCP toggle on the hierarchical scales of `a` and `W`.
