Statistical methods¶
This page specifies the calculations implemented by PyMetaAnalysis. It is an implementation reference, not guidance for choosing an estimand or replacing a review protocol.
Notation¶
For study i:
y_iis the study effect on the model scale;v_iis its sampling variance;w_iis an analysis weight;kis the number of included studies;tau^2is the estimated between-study variance.
Treatment and control are ordered exactly as their argument names imply. A positive difference measure means treatment minus control is positive.
Inverse-variance pooling¶
Common-effect weights and the pooled estimate are:
w_i = 1 / v_i
y_hat = sum(w_i y_i) / sum(w_i)
Var(y_hat) = 1 / sum(w_i)
Random-effects weights replace v_i with v_i + tau^2:
w_i* = 1 / (v_i + tau^2)
mu_hat = sum(w_i* y_i) / sum(w_i*)
Var(mu_hat) = 1 / sum(w_i*)
Reported normalized_weight values equal the selected raw weights divided by
their sum. The pooled-estimate implementation rescales weights internally when
necessary to avoid overflow without changing their ratios.
Between-study variance¶
Random-effects inverse-variance models require at least two included studies. The default estimator is REML.
DerSimonian-Laird¶
With common-effect weights, Cochran's Q, df = k - 1, and:
C = sum(w_i) - sum(w_i^2) / sum(w_i)
tau^2_DL = max(0, (Q - df) / C)
DL is closed form and records zero iterations.
Paule-Mandel¶
PM solves:
Q(tau^2) = sum((y_i - mu_hat(tau^2))^2 / (v_i + tau^2)) = k - 1
If the equation is already non-positive at zero, the estimate is the zero boundary. Otherwise PyMetaAnalysis brackets the non-negative root and solves it with a bounded scalar root finder.
Restricted maximum likelihood¶
REML solves the restricted-likelihood score equation over non-negative
tau^2. As with PM, a non-positive score at zero produces a boundary estimate;
otherwise a bracketed root is found iteratively.
result.diagnostics records convergence, iteration count, and whether the
solution reached the zero boundary. Failure to bracket or converge raises
ConvergenceError; no fallback estimator is selected silently.
Confidence intervals¶
Normal interval¶
The default interval is:
estimate +/- z_(1 - alpha/2) * SE
This is available for common- and random-effects inverse-variance models and for common-effect Mantel-Haenszel pooling.
Hartung-Knapp intervals¶
For random-effects inverse-variance models:
q_star = sum(w_i* (y_i - mu_hat)^2) / (k - 1)
Var_HK(mu_hat) = q_star / sum(w_i*)
CI = mu_hat +/- t_(k - 1, 1 - alpha/2) * sqrt(Var_HK)
hartung_knapp uses this variance without modification. If it is below the
classic variance, the result records a warning. hartung_knapp_adhoc instead
uses:
max(Var_classic, Var_HK)
The variants are separate public choices. PyMetaAnalysis does not silently apply the safeguard to an unmodified Hartung-Knapp request.
Prediction interval¶
Random-effects inverse-variance models with at least three included studies use the Higgins-Thompson-Spiegelhalter form:
mu_hat +/- t_(k - 2, 1 - alpha/2)
* sqrt(tau^2 + Var_classic(mu_hat))
The classic pooled-mean variance is used even when the confidence interval for the mean uses Hartung-Knapp. No interval is returned below three studies. An interval calculated with three or four studies carries an explicit uncertainty warning.
Heterogeneity and inconsistency¶
Cochran's Q always uses common-effect inverse-variance weights:
Q = sum(w_i (y_i - y_hat_common)^2)
df = k - 1
p = P(chi-square_df >= Q)
For Mantel-Haenszel analyses, residuals are centered on the MH pooled estimate while the individual-effect inverse variances supply the Q weights.
Common-effect and MH results use Q-based inconsistency:
I^2 = max(0, (Q - df) / Q)
H^2 = Q / df
Random-effects results use the fitted tau^2 and a typical within-study
variance:
C = sum(w_i) - sum(w_i^2) / sum(w_i)
v_typical = (k - 1) / C
I^2 = tau^2 / (tau^2 + v_typical)
H^2 = 1 + tau^2 / v_typical
result.i2_method records q_based or tau2_typical_variance. I-squared is a
proportion internally and is formatted as a percentage in human-readable
output. With one study, Q is zero and its p-value, I-squared, and H-squared are
unavailable.
Meta-regression¶
For k study effects, let X be the full-rank k-by-p design matrix. It
contains an intercept by default plus numeric moderator columns and treatment-
coded categorical terms:
y = X beta + u + epsilon
epsilon ~ N(0, V), V = diag(v_i)
u ~ N(0, tau^2 I)
Given residual tau-squared:
W = diag(1 / (v_i + tau^2))
B = (X' W X)^(-1)
beta_hat = B X' W y
P = W - W X B X' W
residual df = k - p
The implementation uses stable linear solves rather than forming an explicit
matrix inverse. A rank-deficient design or k <= p is rejected; terms are not
silently removed and a pseudo-inverse is not used.
Residual tau-squared¶
For generalized DL, define W0 and P0 at tau-squared zero:
QE0 = y' P0 y
C = trace(P0)
tau^2_DL = max(0, (QE0 - (k-p)) / C)
PM solves:
y' P(tau^2) y = k - p
REML solves the restricted score:
y' P(tau^2)^2 y - trace(P(tau^2)) = 0
All estimators are constrained to non-negative tau-squared. PM and REML use a
bracketed scalar root; a non-positive equation at zero returns the boundary.
Failure raises ConvergenceError without estimator fallback.
Coefficient and moderator inference¶
Normal inference uses covariance B, z coefficient tests, and a chi-squared
Wald test for one or more moderator terms.
For both Hartung-Knapp choices:
q = y' P(tau^2) y / (k-p)
Cov_HK(beta_hat) = q B
Coefficient tests use a t distribution with k-p degrees of freedom. Joint
tests divide their Wald statistic by the number of tested terms and use an F
distribution with that numerator df and k-p denominator df.
hartung_knapp_adhoc replaces q with max(1, q); unmodified
hartung_knapp retains q and warns when it is below one.
The global moderator test covers every non-intercept term.
test_moderator(name) covers every encoded term for one original moderator,
so a multi-level category receives a joint test.
Residual heterogeneity and pseudo-R-squared¶
Residual heterogeneity uses:
QE = y' P0 y
QE ~ chi-square_(k-p) under the null
Common models derive residual I-squared and H-squared from QE. Mixed models use:
v_typical = (k-p) / trace(P0)
I^2_residual = tau^2 / (tau^2 + v_typical)
H^2_residual = 1 + tau^2 / v_typical
For mixed models with an intercept, the same rows are refitted with an intercept-only design and the same tau-squared estimator:
R^2_raw = 1 - tau^2_model / tau^2_null
R^2 = max(0, R^2_raw)
The raw value is retained. Pseudo-R-squared is unavailable when the null tau-squared is zero and is not equivalent to ordinary regression R-squared.
Meta-regression prediction¶
For a new design vector x0:
fitted = x0' beta_hat
Var_mean = x0' Cov(beta_hat) x0
Mean-effect intervals use the selected normal or t_(k-p) critical value. A
mixed model additionally reports a true-effect prediction interval:
fitted +/- critical * sqrt(tau^2 + Var_mean)
It does not include a sampling variance for a future observed effect.
Binary study effects¶
For a treatment/control 2-by-2 table:
| Event | No event | |
|---|---|---|
| Treatment | a |
b |
| Control | c |
d |
Odds ratio¶
y_i = log(a d / (b c))
v_i = 1/a + 1/b + 1/c + 1/d
Risk ratio¶
y_i = log((a / (a + b)) / (c / (c + d)))
v_i = 1/a - 1/(a + b) + 1/c - 1/(c + d)
OR and RR results remain on the log model scale. Display properties exponentiate them. Continuity-correction and double-zero/double-all behavior is specified in zero-event studies.
Risk difference¶
With p_t = a / (a + b) and p_c = c / (c + d):
y_i = p_t - p_c
v_i = p_t (1 - p_t) / (a + b) + p_c (1 - p_c) / (c + d)
Under the default zero-variance policy, the effect uses raw counts while the variance uses corrected counts only for boundary tables.
Mantel-Haenszel pooling¶
MH is implemented only for common-effect OR and RR. It uses raw tables by default and has a continuity-correction option separate from the study-effect correction.
For OR, with n_i = a_i + b_i + c_i + d_i:
OR_MH = sum(a_i d_i / n_i) / sum(b_i c_i / n_i)
For RR, with treatment total n1_i = a_i + b_i and control total
n0_i = c_i + d_i:
RR_MH = sum(a_i n0_i / n_i) / sum(c_i n1_i / n_i)
The model-scale estimate is the logarithm of the pooled ratio. Standard errors
use the Greenland-Robins equations implemented in the estimator. An undefined
raw pooled ratio raises InvalidStudyDataError and recommends an explicit MH
continuity correction.
Continuous study effects¶
Mean difference¶
y_i = mean_treat - mean_control
v_i = sd_treat^2 / n_treat + sd_control^2 / n_control
The variance is unpooled and does not require equal group variances.
Standardized mean difference¶
The pooled within-study variance and Cohen's d are:
df = n_treat + n_control - 2
s_pooled^2 = ((n_treat - 1) sd_treat^2
+ (n_control - 1) sd_control^2) / df
d = (mean_treat - mean_control) / s_pooled
PyMetaAnalysis applies the exact gamma-function correction:
J(df) = Gamma(df / 2) / (sqrt(df / 2) Gamma((df - 1) / 2))
g = J(df) d
The implemented LS sampling variance is:
v_i = 1/n_treat + 1/n_control + g^2 / (2 (n_treat + n_control))
Subgroup differences¶
Subgroup fits use the same selected model independently within each group. For random effects, tau-squared is estimated separately for every subgroup and the overall analysis.
The formal subgroup-differences test weights subgroup pooled estimates by the inverse square of their pooled standard errors:
W_g = 1 / SE_g^2
theta_bar = sum(W_g theta_g) / sum(W_g)
Q_between = sum(W_g (theta_g - theta_bar)^2)
df_between = number_of_groups - 1
Q_between is compared with a chi-squared distribution. This is not a test
obtained by comparing separate subgroup p-values.
Numerical and software references¶
The implementation is independent Python code. Regression fixtures generated
by R metafor, hand calculations, invariants, and numerical edge cases are
described in validation. Exact supported combinations and
defaults are listed in the API reference.