Skip to content

[ENH] k h correction - #139

Merged
jdkent merged 5 commits into
neurostuff:masterfrom
jdkent:enh/k_h_correction
Aug 20, 2026
Merged

[ENH] k h correction#139
jdkent merged 5 commits into
neurostuff:masterfrom
jdkent:enh/k_h_correction

Conversation

@jdkent

@jdkent jdkent commented Aug 20, 2026

Copy link
Copy Markdown
Member

very useful for small meta-analyses with only one image per study (no groups). The analogous correction to ch2 + satterwaite dof when there are groups.

jdkent and others added 5 commits August 20, 2026 01:16
A random-effects meta-regression estimates tau^2 and then treats it as
known, so the model-based covariance (X'WX)^-1 understates how uncertain
the coefficients are and the Wald test built on it rejects too often at
small K. knapp_hartung_cov_and_dof scales that covariance by
q = e'We / (K - P) and reports K - P degrees of freedom, which is the
adjustment of Knapp & Hartung (2003) as metafor's rma.uni applies it.

Written to reproduce metafor: the scale factor is RSS.knha / (k - p)
under the inverse-variance weights, and conservative=True is metafor's
test="adhoc", the modification of Rover, Knapp & Friede (2015).

Nothing consumes it yet; the estimators are wired up separately.

Notable details, all pinned by tests:

- q is undefined when K - P < 1, since the residual sum of squares and
  its divisor are both zero. The inputs come back unadjusted with dof
  None, and a warning names the consequence.
- A weighted residual sum of squares at or below machine epsilon is
  floored to a scale factor of exactly zero rather than to rounding
  noise. q is dimensionless -- scaling y by c and v by c^2 leaves every
  w_i e_i^2 unchanged -- so an absolute floor is meaningful here. Zero
  reaches get_fe_stats as a non-positive standard error, which it
  already reports as undefined rather than as maximally significant.
- conservative=True floors q after that clamp, not before: the whole
  point of the modification is that the adjustment can only widen an
  interval, and a zeroed scale factor is the most extreme narrowing
  there is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three functions computed e'We under three spellings: np.square in
correlated_effects_tau2, ** 2 in q_gen, and a named intermediate in
knapp_hartung_cov_and_dof. They are the same quantity -- verified equal
to 1.8e-16 across 200 random designs -- so a change to the formulation
could reach one and leave the others behind.

_weighted_rss is now that definition, and the three call sites read it.
This also makes a claim the Knapp-Hartung docstring was only asserting
true in the code: the scale factor is literally q_gen over K - P, which
test_knapp_hartung_scale_factor_is_q_gen_over_its_expectation pins.
q_gen cannot simply be called from there, because it refits beta and the
residuals have to come from the fit that produced model_cov.

Byte-identical across 144 estimator x weight-scheme x dataset-count
configurations, including the rescale cases that exercise
correlated_effects_tau2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…efault

Every meta-regression estimator gains small_sample_correction, and the
four that estimate tau^2 default it to "knapp-hartung". This changes
every standard error, p-value and interval reported without group
labels; small_sample_correction="wald" returns the previous values, and
is byte-identical to them across 144 configurations.

WeightedLeastSquares defaults to "wald" instead. The adjustment accounts
for uncertainty in an estimated tau^2 and that estimator takes tau^2 as
given, which is why metafor warns when test="knha" is asked of a
fixed-effects model. It is still accepted there, for a caller who
plugged in a tau^2 obtained elsewhere.

_robust_cov_and_dof becomes _inference_cov_and_dof and now resolves the
whole choice in one place: dependent observations get the CR2 sandwich
with Satterthwaite degrees of freedom, independent ones get
Knapp-Hartung with K - P. They are alternatives rather than layers -- a
sandwich already stops assuming the fitted weights are right, which is
the entire content of the scale factor, so applying both would correct
for the same thing twice. Group labels therefore take precedence and
small_sample_correction is ignored, silently: the estimators do not
share one default for it, so a warning keyed on "not the default" would
fire on ordinary use of one estimator while staying quiet on the same
request to another.

Collapsing the decision into that one function let all five call sites
drop both their "model_cov if robust_cov is None else robust_cov"
expression and their "if dof is not None" guard, so estimators.py has
the same statement count and eight fewer branches than before.

The measurements behind the default are recorded separately, in
validation/knapp_hartung.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Agreement with an independent implementation is the strongest evidence
available for a formula, so validation/metafor regenerates 180 reference
cases from a Docker image with pinned R and metafor versions, mirroring
validation/robumeta.

The adjustment agrees to machine precision in all 180: 1.8e-15 on the
coefficients, 5.6e-16 on the standard errors, 3.0e-15 on the p-values,
and 5.9e-11 absolute on the interval bounds -- the last bounded not by
the adjustment but by scipy.stats.t.ppf and R's qt returning quantiles
that differ in their final bits.

The comparison is layered, because two questions are involved and
mixing them would let one answer hide the other. Supplying PyMARE with
metafor's own tau^2 isolates the adjustment from the tau^2 estimators
and is what covers every metafor `method`; the closed-form estimators
(FE, DL) are additionally compared end to end.

Whether PyMARE's ML, REML and Hedges tau^2 match metafor's is a separate
question that predates this work: ML and REML differ by up to the tau^2
search tolerance, one ML case lands on the opposite side of the tau^2 = 0
boundary, and Hedges differs by a definitional choice about whether the
mean sampling variance is taken over raw or weighted rows. All three are
visible with no correction applied, so none is caused by the adjustment.
The README records the measurements; comparing them here would fold an
optimizer's tolerance into a check on a closed-form scale factor.

The pinned file keeps metafor's own test= spellings, because it records
what metafor was asked. CORRECTIONS in the alignment module translates
them, and a test asserts that map is total in both directions -- an
unmapped metafor spelling would otherwise raise only for the cases using
it, and an unmapped PyMARE value would simply never be compared.

The four designs bracket the condition the literature says decides
whether the adjustment behaves: sampling variances spanning a factor of
1, 250, 10,000 and 30, at 5 to 20 observations. The 10,000-fold cell is
there because IntHout et al. (2014) and Rover et al. (2015) both report
the adjustment exceeding its nominal level for few observations of very
unequal precision.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Changing a default needs evidence, and validation/knapp_hartung is it:
144 cells at 10,000 replications each, crossing 5 to 40 observations,
1 to 3 predictors, weight ratios from 1 to 10,000, and no/moderate/large
heterogeneity, for two tau^2 estimators and all three corrections.

The result that decides it: "knapp-hartung" is at least as close to
nominal as "wald" in every one of the 144 cells, for both estimators, to
within Monte Carlo noise -- the worst excess measured is +0.0012 against
a standard error of 0.0022. There is no cell where turning the
correction on costs anything, including the cell where the Wald test is
provably right (variances known, tau^2 truly zero), where "wald"
measures 0.038 and "knapp-hartung" 0.049. Against that, the uncorrected
test rejects at 0.098 on median at K = 5 and 0.310 in the worst cell.
Near-zero cost when unnecessary, large gain when necessary.

It does not fix the worst corner. At five observations with three
predictors and a hundredfold spread of precisions -- two residual
degrees of freedom, one observation carrying most of the weight -- it
halves the error rather than removing it, and no reference distribution
rescues that. The conservative variant helps there and overcorrects
badly elsewhere (median 0.005 at K = 5, below 0.02 in a quarter of all
cells), which is why it is offered rather than defaulted to.

Self-contained on purpose: nothing in pymare imports from here and no
measurement is pinned into the test suite. These are Monte Carlo
estimates, so a pinned number would move on every run; --check enforces
the three claims instead, each the reason for one decision. Not wired
into CI either -- the grid takes about twenty minutes.

Replications go on the parallel-dataset axis, so one fit covers a whole
cell, which is what makes the grid affordable at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.80%. Comparing base (89bbad1) to head (62e5201).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #139      +/-   ##
==========================================
+ Coverage   94.76%   94.80%   +0.04%     
==========================================
  Files          13       13              
  Lines        1929     1944      +15     
==========================================
+ Hits         1828     1843      +15     
  Misses        101      101              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jdkent
jdkent merged commit cb49253 into neurostuff:master Aug 20, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant