Skip to content

Fitting models

rma_uni (alias rma) fits equal-effects, fixed-effects, random-effects and mixed-effects (meta-regression) models. It mirrors metafor's rma.uni.

import numpy

from metafopy import datasets, escalc, rma

dat = escalc(measure='RR', ai='tpos', bi='tneg', ci='cpos', di='cneg', data=datasets.load('dat.bcg'))
res = rma('yi', 'vi', data=dat)
print(res)
Random-Effects Model (k = 13; tau^2 estimator: REML)

tau^2 (estimated amount of total heterogeneity): 0.3132 (SE = 0.1664)
tau (square root of estimated tau^2 value):      0.5597
I^2 (total heterogeneity / total variability):   92.22%
H^2 (total variability / sampling variability):  12.86

Test for Heterogeneity:
Q(df = 12) = 152.2330, p-val < .0001

Model Results:

estimate      se     zval    pval    ci.lb    ci.ub
 -0.7145  0.1798  -3.9744  <.0001  -1.0669  -0.3622  ***

---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Ways to give the data

All of these fit the same model:

a = rma('yi', 'vi', data=dat)
b = rma(yi=dat['yi'], vi=dat['vi'])
c = rma(yi=dat['yi'], sei=numpy.sqrt(dat['vi']))
d = rma(measure='RR', ai='tpos', bi='tneg', ci='cpos', di='cneg', data=datasets.load('dat.bcg'))
print(a.beta, b.beta, c.beta, d.beta)
[-0.71453235] [-0.71453235] [-0.71453235] [-0.71453235]

The last form lets rma call escalc for you. It takes the same add, to, drop00 and vtype arguments.

The model and the estimator

The model is chosen by method. 'EE' (aliases 'FE' and 'CE') is the equal-effects model. Every other value is a random-effects model with that estimator of the amount of heterogeneity tau^2:

method Estimator
'REML' (default) Restricted maximum likelihood
'ML' Maximum likelihood
'DL', 'DLIT' DerSimonian-Laird, and its iterative version
'HE' Hedges
'HS', 'HSk' Hunter-Schmidt, and its small-sample variant
'SJ', 'SJIT' Sidik-Jonkman, and its iterative version
'EB' Empirical Bayes
'PM' (alias 'MP'), 'PMM' Paule-Mandel, and its median-unbiased version
'GENQ', 'GENQM' Generalized Q-statistic estimators (Jackson, 2013)

Viechtbauer (2005), https://doi.org/10.3102/10769986030003261, and Veroniki et al. (2016), https://doi.org/10.1002/jrsm.1164, compare them. metafopy.models.tau2.ESTIMATORS lists all of them. A list such as method=['REML', 'DL'] tries each in turn until one converges. Optimiser settings go in control=, a Control instance or a dict with metafor's names (tau2_init, tau2_min, tau2_max, threshold, maxiter, stepadj...).

Tests and intervals

By default the pooled estimate has a z-test and Wald-type confidence interval. Use test='knha' for the Knapp-Hartung adjustment (a t-test with a rescaled variance; Knapp and Hartung, 2003, https://doi.org/10.1002/sim.1482), which has better coverage with few studies:

print(rma('yi', 'vi', data=dat, test='knha'))
Random-Effects Model (k = 13; tau^2 estimator: REML)

tau^2 (estimated amount of total heterogeneity): 0.3132 (SE = 0.1664)
tau (square root of estimated tau^2 value):      0.5597
I^2 (total heterogeneity / total variability):   92.22%
H^2 (total variability / sampling variability):  12.86

Test for Heterogeneity:
Q(df = 12) = 152.2330, p-val < .0001

Model Results:

estimate      se     tval  df    pval    ci.lb    ci.ub
 -0.7145  0.1808  -3.9522  12  0.0019  -1.1084  -0.3206  **

---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

level is the confidence level in percent (default 95), and digits controls how much is printed.

Meta-regression

Moderators are given as a formula in mods, evaluated on data with formulaic (R-style syntax: '~ ablat + year', '~ C(alloc)', '~ ablat * year'). As in R, factor(), relevel(x, ref) and scale() (which skips missing values) can be used in the formula, as can functions defined in the calling code (they are looked up where rma is called). The same goes for the scale formula of location-scale models.

res = rma('yi', 'vi', mods='~ ablat + year', data=dat)
print(res)
Mixed-Effects Model (k = 13; tau^2 estimator: REML)

tau^2 (estimated amount of residual heterogeneity):     0.1108 (SE = 0.0845)
tau (square root of estimated tau^2 value):             0.3328
I^2 (residual heterogeneity / unaccounted variability): 71.98%
H^2 (unaccounted variability / sampling variability):   3.57
R^2 (amount of heterogeneity accounted for):            64.63%

Test for Residual Heterogeneity:
QE(df = 10) = 28.3251, p-val = 0.0016

Test of Moderators (coefficients 2:3):
QM(df = 2) = 12.2043, p-val = 0.0022

Model Results:

         estimate       se     zval    pval     ci.lb    ci.ub
intrcpt   -3.5455  29.0959  -0.1219  0.9030  -60.5724  53.4814
ablat     -0.0280   0.0102  -2.7371  0.0062   -0.0481  -0.0080  **
year       0.0019   0.0147   0.1299  0.8966   -0.0269   0.0307

---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Categorical moderators are coded with treatment contrasts, so the intercept is the first level. Use '~ alloc - 1' for one coefficient per level. rma also accepts yi='yi ~ ablat', and a model matrix as mods.

The coefficients are res.beta, in the order of res.coef_names. btt selects the coefficients that the omnibus test tests. It takes 0-based positions (or a regular expression matched against the names):

res = rma('yi', 'vi', mods='~ alloc + ablat', data=dat)
print(res.coef_names)
print(rma('yi', 'vi', mods='~ alloc + ablat', data=dat, btt=[1, 2]))
('intrcpt', 'allocrandom', 'allocsystematic', 'ablat')

Mixed-Effects Model (k = 13; tau^2 estimator: REML)

tau^2 (estimated amount of residual heterogeneity):     0.1446 (SE = 0.1124)
tau (square root of estimated tau^2 value):             0.3803
I^2 (residual heterogeneity / unaccounted variability): 70.11%
H^2 (unaccounted variability / sampling variability):   3.35
R^2 (amount of heterogeneity accounted for):            53.84%

Test for Residual Heterogeneity:
QE(df = 9) = 26.2034, p-val = 0.0019

Test of Moderators (coefficients 2:3):
QM(df = 2) = 1.2850, p-val = 0.5260

Model Results:

                 estimate      se     zval    pval    ci.lb    ci.ub
intrcpt            0.2932  0.4050   0.7239  0.4691  -0.5006   1.0870
allocrandom       -0.2675  0.3504  -0.7633  0.4453  -0.9543   0.4193
allocsystematic    0.0585  0.3795   0.1540  0.8776  -0.6854   0.8023
ablat             -0.0273  0.0092  -2.9650  0.0030  -0.0453  -0.0092  **

---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Methods you call on a fit

These work on a fitted model the same way as in metafor. The result object is passed as the first argument.

res = rma('yi', 'vi', mods='~ ablat', data=dat)

predict

from metafopy import predict

print(predict(res, newmods=[10, 30, 50]))
print(predict(rma('yi', 'vi', data=dat), transf=numpy.exp))
     pred      se   ci.lb   ci.ub   pi.lb   pi.ub
0 -0.0396  0.1866 -0.4053  0.3262 -0.6931  0.6140
1 -0.6216  0.1069 -0.8312 -0.4120 -1.2023 -0.0409
2 -1.2036  0.1716 -1.5400 -0.8672 -1.8412 -0.5660
     pred   ci.lb   ci.ub   pi.lb  pi.ub
0  0.4894  0.3441  0.6962  0.1546  1.549

transf= applies a transformation to the predictions and their bounds. Prediction intervals (pi.lb, pi.ub) include the heterogeneity. predtype= (pi_type= is its old spelling) chooses how the interval is built: 'default', 'simple' (no uncertainty of the predicted value, normal distribution), 'riley' (a t distribution with k - parms degrees of freedom, Riley et al., 2011, https://doi.org/10.1111/j.1467-985X.2010.00696.x) or 't'; newvi= adds the sampling variances of new studies. Both work for every fit class. na_action= ('omit', 'exclude', 'pass' or 'fail', standing for R's options(na.action=)) decides whether the fitted values of fitted(), predict() and hatvalues() cover the studies used in fitting or one value for each study of the data.

newmods= may also be a pandas.Series, a dict or a one- or multi-row pandas.DataFrame with names. With more than one moderator the values are then matched to the moderators by name, in any order and also by partial names (a name is accepted when its letters occur in order in the moderator's name and only one moderator fits, so {'yr': 1960, 'abl': 30} matches year and ablat), as in R. prob='>log(0.8)' evaluates the cutoff (arithmetic and common functions such as log, exp and sqrt), and addx=True adds the model matrix as columns X.intrcpt, X.ablat, ... to the printed result and to to_frame().

confint

For a random-effects model, confint gives intervals for tau^2, tau, I^2 and H^2. The default is the Q-profile method of Viechtbauer (2007), https://doi.org/10.1002/sim.2514. type='pl' gives profile-likelihood intervals.

from metafopy import confint

print(confint(res))
print(confint(res, type='pl'))
        estimate    ci.lb    ci.ub
tau^2     0.0764   0.0167   0.7849
tau       0.2763   0.1292   0.8859
I^2(%)   68.3931  32.1080  95.6976
H^2       3.1639   1.4729  23.2428
        estimate    ci.lb    ci.ub
tau^2     0.0764   0.0058   0.4432
tau       0.2763   0.0763   0.6657
I^2(%)   68.3931  14.1564  92.6258
H^2       3.1639   1.1649  13.5607

anova

anova runs Wald-type tests of coefficients and likelihood ratio tests that compare two nested models. Fit both with method='ML' for the likelihood ratio test of fixed effects.

from metafopy import anova

full = rma('yi', 'vi', mods='~ ablat', data=dat, method='ML')
reduced = rma('yi', 'vi', data=dat, method='ML')
print(anova(full, reduced))
print(anova(res, btt=[1]))
         df      AIC      BIC     AICc   logLik     LRT    pval        QE   tau^2      R^2
Full      3  21.3713  23.0662  24.0380  -7.6857     NaN     NaN   30.7331  0.0344      NaN
Reduced   2  29.3302  30.4601  30.5302 -12.6651  9.9588  0.0016  152.2330  0.2800  87.7302
Test of Moderators (coefficient 2):
QM(df = 1) = 16.3571, p-val < .0001

For a fit made robust with robust(res, cluster, club_sandwich=True), anova runs the cluster-robust Wald test with the small-sample approximation of the fit's wald_test (HTZ by default), which gives an F test with an estimated denominator degrees of freedom (F(df1 = 1, df2 = 1.52) = 41.6109, p-val = 0.0440 for btt on the bcg data with cluster='alloc'); anova(res, X=...) uses the degrees of freedom of the fit's coef_test for each linear combination. L is R's old spelling of X, fixed=True matches a character btt literally, and anova(full, reduced, test='Wald') tests the extra coefficients of the full model with its Wald test.

Permutation test

permutest gives permutation-based p-values for the coefficients, with an exact test for small numbers of studies. Set the seed with set_seed first: metafopy reproduces R's random number generator, so seeds give the same permutations as set.seed in R.

from metafopy import permutest, set_seed

set_seed(1234)
print(permutest(res, iter=500))
Test of Moderators (coefficient 2):¹
QM(df = 1) = 16.3571, p-val = 0.0100

Model Results:

         estimate      se     zval    pval¹    ci.lb    ci.ub
intrcpt    0.2515  0.2491   1.0095  0.6980   -0.2368   0.7397
ablat     -0.0291  0.0072  -4.0444  0.0100   -0.0432  -0.0150  **

---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

1) p-values based on permutation testing

profile

profile evaluates the likelihood over a grid of tau^2 values, which is useful to see whether the likelihood is flat:

from metafopy import profile

print(profile(rma('yi', 'vi', data=dat)).tau2[:3])
[0.11969531 0.17189484 0.22409436]

profile(..., pred=True, newmods=...) adds the predicted values and their interval bounds at each grid value (pred, pred_ci_lb, pred_ci_ub, pred_pi_lb, pred_pi_ub) and blup=True the BLUPs (blup, blup_se, blup_pi_lb, blup_pi_ub) to the result.

Residuals and BLUPs

rstandard, rstudent, blup and ranef give residuals and the best linear unbiased predictions of the study-specific effects. See Diagnostics.

Equal-effects and fixed-effects models

method='EE' fits the equal-effects model: every study estimates the same true effect. metafor also accepts 'FE' and 'CE' as synonyms. The results are identical and only the interpretation differs. A random-effects model estimates the mean of a distribution of true effects instead. See Hedges and Vevea (1998), https://doi.org/10.1037/1082-989X.3.4.486, for the distinction.

Weights

weights replaces the inverse-variance weights with your own, and weighted=False fits by ordinary least squares. The standard errors of the coefficients then account for the weights not being inverse-variance weights. For inference that is robust to a misspecified model, see robust.