---
title: "Custom Estimating Equations"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Custom Estimating Equations}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r knitr-opts, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

```{r setup}
library(deli)
```

## Overview

The deli package lets you define custom estimating equations, stack them with
built-in ones, and get valid sandwich variance estimates for the full system.
This vignette covers three topics:

1. Writing a custom estimating equation from scratch
2. Stacking custom and built-in estimating equations
3. Using the delta method for post-hoc transformations

## Writing a custom estimating equation

The core requirement is simple: your `psi` function must take a parameter
vector `theta` and return a **p-by-n matrix**, where p is the number of
parameters and n is the number of observations. Each row corresponds to one
parameter's estimating equation, and each column corresponds to one
observation's contribution.

### Example: estimating a ratio of means

Suppose we observe two variables `y1` and `y2` and want to estimate the ratio
of their means, `mu1 / mu2`. We need three parameters:

- `theta[1]`: the mean of `y1` (i.e., `mu1`)
- `theta[2]`: the mean of `y2` (i.e., `mu2`)
- `theta[3]`: the ratio `mu1 / mu2`

The estimating equations are:

1. `y1_i - theta[1]` (solves for the mean of `y1`)
2. `y2_i - theta[2]` (solves for the mean of `y2`)
3. `theta[1] / theta[2] - theta[3]` (solves for the ratio)

The third equation has no variation across observations, so we repeat the same
value n times.

```{r ratio-of-means}
set.seed(42)
n <- 200
y1 <- rnorm(n, mean = 4, sd = 1)
y2 <- rnorm(n, mean = 2, sd = 1)

psi <- function(theta) {
  # Row 1: estimating equation for the mean of y1
  mu1 <- y1 - theta[1]
  # Row 2: estimating equation for the mean of y2
  mu2 <- y2 - theta[2]
  # Row 3: estimating equation for the ratio (repeated n times)
  ratio <- rep(theta[1] / theta[2] - theta[3], n)
  # Stack into a 3-by-n matrix. `rbind()` labels each row with the name of the
  # variable it came from, and those labels become the parameter names.
  rbind(mu1, mu2, ratio)
}

m <- m_estimate(stacked_equations = psi, init = c(1, 1, 1))
m@theta
```

The third element of `theta` is the estimated ratio. Because the ratio is
estimated jointly with the means, the sandwich variance accounts for the
uncertainty in all three parameters:

```{r ratio-summary}
summary(m)
```

Compare the point estimate to the naive ratio:

```{r naive-ratio}
mean(y1) / mean(y2)
```

### Key points for custom equations

- **Always return a matrix.** Even for a single parameter, return a 1-by-n
  matrix (e.g., `matrix(..., nrow = 1)`).
- **Rows = parameters, columns = observations.** The number of rows must match
  the length of `theta`, and the number of columns must equal n.
- **Deterministic equations** (like the ratio above) should repeat the same
  value across all n columns. This is necessary so the matrix dimensions are
  consistent with the other rows.
- **Row names name the parameters.** The row names of the returned matrix
  become the labels on `coef()`, `vcov()`, `confint()`, and `summary()`. Names
  on `init` take precedence when it has any. Row names are read only when every
  parameter is labeled and no two labels are alike, so a stack that names some
  rows and not others, or that repeats a name, is numbered `theta_1` through
  `theta_p` instead. `rbind()` supplies a label for each plain vector it is
  given, taken from the variable name, as in the example above; a matrix
  argument contributes whatever row names it already carries. Many built-in
  `ee_*()` functions name their rows and many do not. The ones that do say so
  under **Value** on their help pages, along with what the names are, so a stack
  that mixes in one of the others is numbered unless you name it yourself. Most
  of the regression estimating equations leave their rows unnamed, because there
  is nothing to name: a regression coefficient has no name apart from the design
  column it multiplies, and deli drops a design's column headings, so the labels
  left would say no more than `theta_1` already does. Four returns are
  different. `ee_glm()` under `distribution = "gamma"` and
  `distribution = "negative_binomial"`, along with `ee_tobit()` and
  `ee_beta_regression()`, each hold one row more than the design has columns,
  and that last row is a parameter of the outcome distribution: a log shape, a
  log dispersion, a log scale, or a log precision. It is the row you are most
  likely to read as a coefficient, so each of the four names it `log_shape`,
  `log_dispersion`, `log_sigma`, or `log_phi`, with the design rows labeled
  `X_1` through `X_p` beside it. Set the names yourself with `rownames()` where
  the ones you want are not the ones you get. The assignment works under
  `deriv_method = "exact"` as well, unlike the reshaping helpers described in
  `vignette("getting-started")`.

## Stacking custom EEs with built-in ones

A major strength of M-estimation is stacking: you can combine built-in
estimating equations with custom ones using `rbind()`. The sandwich variance
estimator then correctly propagates uncertainty through the entire system.

### Example: log odds-ratio from logistic regression

Suppose we fit a logistic regression and want to estimate the odds ratio for a
coefficient, along with a proper confidence interval. We can stack the
regression estimating equations with a custom equation that exponentiates the
log-odds coefficient.

```{r stacked-odds-ratio}
set.seed(42)
n <- 500
x <- rnorm(n)
pr <- plogis(-0.5 + 0.8 * x)
y <- rbinom(n, 1, pr)
X <- cbind(1, x)

psi <- function(theta) {
  # theta[1:2]: logistic regression coefficients (intercept, slope)
  # theta[3]: odds ratio = exp(theta[2])
  beta <- theta[1:2]
  or <- theta[3]

  # Built-in logistic regression EE (returns a 2-by-n matrix)
  ee_reg <- ee_regression(beta, X = X, y = y, model = "logistic")

  # Custom EE for the odds ratio (deterministic, repeated n times)
  ee_or <- matrix(rep(exp(theta[2]) - or, n), nrow = 1)

  # Stack: 3-by-n matrix
  rbind(ee_reg, ee_or)
}

m <- m_estimate(stacked_equations = psi, init = c(0, 0, 1))
summary(m)
```

The third row of the summary gives the odds ratio with a sandwich-based
confidence interval that correctly accounts for the estimation uncertainty in
the regression coefficients. Compare with a manual calculation:

```{r odds-ratio-manual}
exp(m@theta[2])
```

### How stacking works

When you `rbind()` a 2-by-n matrix from `ee_regression()` with a 1-by-n matrix
from a custom equation, you get a 3-by-n matrix. `m_estimate()` solves the
full 3-parameter system simultaneously and computes the sandwich variance for
all parameters at once. This is what makes the variance estimates valid: the
covariance between the regression coefficients and the odds ratio is captured
automatically.

## Using the delta method

The delta method is an alternative to stacking for obtaining variance estimates
of transformed parameters. Instead of adding extra equations to the system,
you apply a transformation after estimation and use a first-order
approximation to compute the variance.

### Example: odds ratio via the delta method

Using the same logistic regression as above, we can get the odds ratio variance
without stacking. Nothing custom is stacked onto the regression here, so the
formula interface applies: give `m_estimate()` a formula and a data frame and it
builds the design matrix and response for you.

```{r logistic-fit}
# Fit the logistic regression only
d <- data.frame(x, y)

m_reg <- m_estimate(y ~ x, data = d, .ee = ee_regression, model = "logistic")
m_reg@theta
```

Now apply the delta method. The `transform` function takes the full `theta`
vector and returns the transformed quantity:

```{r delta-method}
# Transform: exponentiate the second coefficient to get the odds ratio
dm_var <- delta_method(m_reg, transform = function(theta) exp(theta[2]))
dm_var
```

The result is the variance of the odds ratio. We can compute a confidence
interval:

```{r delta-method-ci}
or_est <- exp(m_reg@theta[2])
or_se <- sqrt(dm_var[1, 1])
ci <- or_est + c(-1, 1) * qnorm(0.975) * or_se
round(c(or = unname(or_est), lower = ci[1], upper = ci[2]), 3)
```

### When to use the delta method vs. stacking

- **Stacking** is preferred when the transformation involves additional data or
  when you want the transformed parameter to be part of the root-finding
  system. Stacking also makes it easy to add further equations that depend on
  the transformed parameter.
- **The delta method** is convenient for simple post-hoc transformations of
  already-estimated parameters. It avoids modifying the estimating equation
  system and does not require specifying initial values for the extra
  parameters.

Both approaches give the same asymptotic variance. Use whichever is more
natural for your problem.

### Using the delta method with raw estimates

You can also call `delta_method()` directly on a numeric vector of estimates
and a covariance matrix, without a fitted estimator object:

```{r delta-method-raw}
delta_method(
  m_reg@theta,
  transform = function(theta) exp(theta[2]),
  covariance = m_reg@variance
)
```

This is useful when you have estimates and covariances from another source
(e.g., from `glm()` or another package) and want to apply the delta method.
