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").
The MetaHunt pipeline rests on four assumptions. We state them in applied-reader voice; the manuscript contains the formal statements and proofs.
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.
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.
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.
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.
metahunt() is a thin wrapper around three exported
steps. You can also call them individually if you want fine-grained
control.
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.
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).
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.
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 40In 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, w2Predict 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")Pass a wrapper for a scalar summary per target (an ATE
under uniform grid weights):
vignette("data-prep") — turning per-centre fitted
models into (F_hat, W) via build_grid() and
f_hat_from_models(), including a self-contained
lm onramp.vignette("grid-weights") — choosing the
grid_weights argument and the underlying \(L^2(\mu)\) inner product.vignette("choosing-k-denoising") — picking the rank
\(K\) and the d-fSPA denoising knobs
(N, Delta) via elbow and CV diagnostics.vignette("conformal-prediction") — split, cross, and
from-fit conformal bands around the target function.vignette("wrapper-scalar") — using
wrapper = mean and other reductions to turn function-valued
predictions into ATEs and related scalar estimands.vignette("minmax-baseline") — when to prefer the
covariate-free worst-case-regret aggregator and how it compares to
MetaHunt.?metahunt,
?dfspa, ?fit_weight_model,
?predict_target.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.