---
title: "Custom families"
author: "metaGLMM authors"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Custom families}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>",
                      fig.width = 6, fig.height = 4)
set.seed(20260821)
library(metaGLMM)
```

## The family contract

`metaGLMM_family()` defines an R-level conditional likelihood.  Supply a
name, a link accepted by `stats::make.link()`, and a vectorized callback
with arguments `(y, eta, vi, ni)`.  The callback returns one finite
contribution for every value of `eta`.  Optional `validate` and `initialize`
hooks can add family-specific checks or deterministic starting values.

The example below defines a negative-binomial likelihood for overdispersed
study counts.  Each response is an observed rate `y`, `ni` is its exposure,
and `y * ni` recovers the count.  The conditional count mean is
`exp(eta) * ni`.  A fixed negative-binomial size is captured by the callback;
the between-study log-rate variance remains the `tau2` estimated by
`metaGLMM()`.

```{r family}
nb_size <- 8

negative_binomial_rate <- metaGLMM_family(
  name = "negative-binomial-rate",
  link = "log",
  loglik = function(y, eta, vi, ni) {
    count <- round(y * ni)
    dnbinom(
      x = count,
      size = nb_size,
      mu = exp(eta) * ni,
      log = TRUE
    )
  },
  validate = function(y, vi, ni) {
    count <- y * ni
    if (any(y < 0) || any(abs(count - round(count)) > 1e-7)) {
      stop("y * ni must define non-negative integer counts.")
    }
    if (any(!is.finite(vi)) || any(vi <= 0)) {
      stop("vi must contain finite positive display variances.")
    }
    TRUE
  },
  initialize = function(y, X, vi, ni) {
    value <- rep(0, ncol(X))
    intercept <- match("(Intercept)", colnames(X))
    if (!is.na(intercept)) {
      value[intercept] <- log(sum(y * ni) / sum(ni))
    }
    value
  }
)
negative_binomial_rate
```

The `loglik` callback is vectorized in `eta`: during QMC integration a single
study count is evaluated at all random-effect draws.  The validation function
checks the aggregate count contract before optimization, and the initialization
function supplies a deterministic pooled log-rate start.

## Fit an overdispersed rate model with QMC

```{r data-fit}
nb_dat <- data.frame(
  study = paste0("Study ", 1:8),
  events = c(8, 30, 5, 55, 18, 90, 9, 70),
  exposure = c(20, 35, 18, 42, 30, 50, 25, 45)
)
nb_dat$rate <- nb_dat$events / nb_dat$exposure
nb_dat$vi <- 1 / pmax(0.5, nb_dat$events) + 1 / nb_size
nb_dat

qmc_points <- qnorm((seq_len(512) - 0.5) / 512)

nb_fit <- metaGLMM(
  rate ~ 1,
  data = nb_dat,
  vi = nb_dat$vi,
  ni = nb_dat$exposure,
  tau2 = NA,
  tau2_var = TRUE,
  start_tau2 = 0.3,
  family = negative_binomial_rate,
  rstdnorm = qmc_points,
  fast = FALSE
)

summary(nb_fit)
exp(coef(nb_fit))

nb_ci <- list(
  Wald = confint(nb_fit, method = "wald"),
  Profile = confint(nb_fit, method = "profile"),
  SBC = confint(nb_fit, method = "SBC")
)
do.call(rbind, nb_ci)
stopifnot(is.finite(nb_fit$tau), nb_fit$tau > 0)
```

The exponentiated intercept is the pooled conditional rate.  The fitted
`tau2` describes additional between-study variation on the log-rate scale,
while `nb_size` controls negative-binomial variation within the conditional
count model.  In this interface `nb_size` is prespecified rather than estimated;
record its choice and consider sensitivity analyses when it is not externally
known.

Custom families support Wald, profile, and SBC intervals.  The simplified
Bartlett correction uses `vi` and the heterogeneity estimate under each profile
constraint; for a custom family, supply `vi` as an appropriate study-level
sampling variance on the link scale.

```{r forest}
forest(
  nb_fit,
  labels = nb_dat$study,
  type = "exp",
  xlab = "Negative-binomial rate",
  ci_methods = c("Wald", "profile", "SBC")
)
```

## Validation and limitations

The callback must return a finite numeric vector of the same length as `eta`.
Check response domains and variance conventions before fitting; family
validation errors are reported before numerical optimization.

Custom families are supported through `fast = FALSE` in this release.  A
request for `fast = TRUE` with a custom family is rejected because compiled
quadrature for arbitrary R callbacks is not part of the interface.

```{r expected-error, eval=FALSE}
try(
  metaGLMM(
    rate ~ 1,
    data = nb_dat,
    vi = nb_dat$vi,
    ni = nb_dat$exposure,
    tau2 = NA,
    tau2_var = TRUE,
    family = negative_binomial_rate,
    fast = TRUE
  )
)
```

For production analyses, keep the callback definition, the QMC sequence, and
the fitted object's integration settings with the analysis record.
