An introduction to MetaHunt

library(MetaHunt)
set.seed(1)

The problem: function-valued meta-analysis

You have several studies — trial centres, cohorts, sites — and each one has produced a fitted model for some function of patient-level covariates. That function might be a regression curve, a CATE, or a dose-response curve. You also have study-level metadata for each study: region, sample composition, year, and so on. You want to predict that function for a new study, characterised only by its metadata, together with a prediction band, and you want to do it without ever pooling patient-level data across studies.

MetaHunt is designed for exactly that workflow. It assumes a small number of latent basis functions drive cross-study heterogeneity, recovers them from the per-study estimates, and learns how the mixing weights depend on study-level metadata. The result is a single fitted object that you can predict() for new metadata and combine with conformal prediction for uncertainty quantification. If you would rather see one short example before reading the full pipeline, start with vignette("get-started", package = "MetaHunt").

Key assumptions

The MetaHunt pipeline rests on four assumptions. We state them in applied-reader voice; the manuscript contains the formal statements and proofs.

A1. Low-rank cross-study heterogeneity

We assume the true study-level functions all live inside the convex hull of a small number \(K\) of shared latent basis functions. Each study is a convex combination of the bases, with non-negative weights that sum to one. Geometrically, every study is a point inside a \((K-1)\)-simplex whose vertices are the basis functions; the bases are identifiable as the vertices of that hull.

Formally, there exist basis functions \(g_1, \ldots, g_K\) with \(K < m\) such that, for every study \(i \in \{0, 1, \ldots, m\}\), \[f^{(i)}(\boldsymbol{x}) \;=\; \sum_{k=1}^{K} \pi_{ik}\, g_k(\boldsymbol{x}), \qquad \boldsymbol{x} \in \mathcal{X},\] with weight vector \(\boldsymbol{\pi}_i = (\pi_{i1}, \ldots, \pi_{iK})^\top \in \Delta_{K-1}\), the \((K-1)\)-simplex. The bases are non-degenerate, i.e. \(g_2 - g_1, \ldots, g_K - g_1\) are linearly independent in \(L^2(\mu)\).

What this buys you: the entire \(m\)-by-grid table of study functions collapses to \(K\) shared shapes plus an \(m\)-by-\(K\) table of weights, so heterogeneity becomes low-dimensional and shareable.

A2. Weight model

The mixing weight \(\boldsymbol{\pi}_i\) for study \(i\) is drawn from some conditional distribution given that study’s covariates \(\boldsymbol{W}_i\). The distribution can be anything you can estimate — a Dirichlet regression, an RKHS-Dirichlet, a multinomial logit, a nearest-neighbour smoother. MetaHunt does not commit you to a specific functional form.

Formally, for \(i = 0, 1, \ldots, m\), the weight vectors are independent draws \[\boldsymbol{\pi}_i \mid \boldsymbol{W}_i \;\stackrel{\text{ind.}}{\sim}\; \mathcal{P}_{\boldsymbol{\pi} \mid \boldsymbol{W}}(\,\cdot \mid \boldsymbol{W}_i),\] where \(\mathcal{P}_{\boldsymbol{\pi}\mid\boldsymbol{W}}\) is an arbitrary distributional map from the covariate space \(\mathcal{W}\) to the simplex \(\Delta_{K-1}\).

What this buys you: once you can map metadata to weights, predicting the function for a brand-new target population reduces to predicting its weights and re-mixing the recovered bases.

A3. Exchangeability

The studies (including the target) are exchangeable units drawn from a common data-generating process: their joint distribution is invariant under reordering. This is much weaker than i.i.d. and is the standard condition under which conformal prediction is valid.

Formally, the site-level covariates \(\boldsymbol{W}_0, \boldsymbol{W}_1, \ldots, \boldsymbol{W}_m\) are exchangeable. Combined with A1–A2, this implies that the triples \[\bigl(\boldsymbol{W}_i,\, \boldsymbol{\pi}_i,\, f^{(i)}\bigr), \qquad i = 0, 1, \ldots, m,\] are jointly exchangeable across \(i\).

What this buys you: the calibration step in conformal prediction inherits a finite-sample, distribution-free coverage guarantee with no need for parametric error models.

A4. Estimation error control

Each study reports a noisy version of its true function, \(\hat f^{(i)} = f^{(i)} + \epsilon^{(i)}\). We assume the within-study estimation error vanishes uniformly in the within-study sample size, and that the number of studies \(m\) does not grow too fast relative to those sample sizes. In words: every study should be reasonably well-estimated, and you should not have a large number of studies whose individual sample sizes \(n_i\) are small.

Formally, write \(\hat f^{(i)}(\boldsymbol{x}) = f^{(i)}(\boldsymbol{x}) + \epsilon^{(i)}(\boldsymbol{x})\). We assume \[\sup_{\boldsymbol{x} \in \mathcal{X}} \mathbb{E}\!\left[\epsilon^{(i)}(\boldsymbol{x})^2\right] = O\!\left(n_i^{-r}\right) \quad \text{for some } r > 0,\] and that \(m = o\bigl(\inf_i n_i^{\,a}\bigr)\) for some \(0 < a < r\), where \(n_i\) is study \(i\)’s sample size.

What this buys you: the noise in \(\hat F\) does not accumulate through the pipeline, basis recovery is consistent, and conformal intervals retain their asymptotic coverage despite a multi-stage estimator.

The three-step pipeline

metahunt() is a thin wrapper around three exported steps. You can also call them individually if you want fine-grained control.

Step 1. Basis hunting via d-fSPA

dfspa() extends the Successive Projection Algorithm to functions. It iteratively picks the study whose current residual norm is largest, projects the rest onto the orthogonal complement, and repeats \(K\) times. A denoising step averages each study with its near neighbours before the search; this trades a small bias for substantially smaller variance when the per-study estimates are noisy.

Step 2. Fitting weight model

For each study, project_to_simplex() finds the convex weights that best reconstruct its observed function from the recovered bases. fit_weight_model() then regresses these weight vectors on the study-level covariates W using a method of your choice (default: Dirichlet regression).

Step 3. Target prediction

predict.metahunt() takes a new metadata row \(W_0\), predicts its weight vector through the fitted weight model, and returns the convex combination of the recovered bases. With wrapper = mean (or any other reduction) it returns a scalar summary — for example, an ATE under a uniform grid weighting.

A worked end-to-end example

For the rest of the vignette we work with a simulated (F_hat, W) so the truth is known. In your own data, replace this block with the data-prep onramp described in vignette("data-prep").

G <- 40; m <- 120; K_true <- 3
x <- seq(0, 1, length.out = G)
basis <- rbind(sin(pi * x), cos(pi * x), x)         # 3 true bases on the grid

W <- data.frame(w1 = rnorm(m), w2 = rnorm(m))       # study-level covariates
beta <- cbind(c(1, -0.8), c(-0.5, 1.2), c(0, 0))
pi_true <- exp(as.matrix(W) %*% beta)
pi_true <- pi_true / rowSums(pi_true)

F_hat <- pi_true %*% basis + matrix(rnorm(m * G, sd = 0.05), m, G)
dim(F_hat)
#> [1] 120  40

In real data, F_hat[i, ] would be predict(model_i, newdata = grid) and W[i, ] would be that centre’s metadata. The data-prep vignette describes a one-line lm-based onramp.

Fit the full pipeline and inspect the recovered bases:

fit <- metahunt(F_hat, W, K = 3)
fit
#> MetaHunt fit
#>   m (studies):    120 
#>   G (grid size):  40 
#>   K (bases):      3 
#>   weight method:  dirichlet 
#>   predictors:     w1, w2
plot(fit, x_axis = x,
     col = c("#0072B2", "#D55E00", "#009E73"))

Predict the target functions for three new metadata profiles and plot them:

W_new <- data.frame(w1 = c(0, 1, -1), w2 = c(0, -0.5, 1),
                    row.names = c("baseline", "high w1, low w2", "low w1, high w2"))
f_pred <- predict(fit, newdata = W_new)
dim(f_pred)
#> [1]  3 40
oldpar <- par(mar = c(4, 4.5, 3, 1))
matplot(x, t(f_pred), type = "l", lty = 1,
        col = c("#0072B2", "#D55E00", "#009E73"),
        xlab = "x", ylab = expression(tilde(f)(x)),
        main = "Predicted target functions")
legend("topright", legend = rownames(W_new),
       col = 1:3, lty = 1, bty = "n")

par(oldpar)

Pass a wrapper for a scalar summary per target (an ATE under uniform grid weights):

predict(fit, newdata = W_new, wrapper = mean)
#> [1] 0.3318159 0.5559631 0.1020037

Where to next

References

Shi, W., Imai, K., and Zhang, Y. (2024). Privacy-preserving meta-analysis through low-rank basis hunting.

Zhang, Y., Huang, M., and Imai, K. (2024). Minimax regret estimation for generalizing heterogeneous treatment effects with multisite data. arXiv:2412.11136.