Detailed notes on the differences¶
The full list of known differences between metafopy and metafor, one note per item, grouped by topic. The page
Differences from metafor shows the most important ones with the code and output of both. The notes below are
the reference: they give the numbers, the data sets and the reason of each. Names are as in the code (. is _ in Python).
Names, results and printing¶
What the result objects are and how they print.
-
Names:
.becomes_; indices are 0-based. -
simulate(na_action=)stands for R'soptions(na.action=)('omit'is R's default).formulais the methodres.formula()(a top-levelmetafopy.formulawould hide the module);se()is a function only, because results have ansefield.setmfopt(theme=..., check=...)is stored and validated but not used. -
summary()of every fit class includes the fit-statistics table (logLik,deviance,AIC,BIC,AICc) like R'ssummary();str(res)andprint(res)do not.fitstats(),AIC()andBIC()take further fits and give the table of several models (AIC(k=)is accepted but not used, as in R).AIC(correct=True)gives the AICc column. These are methods of the fits (res1.AIC(res2)); metafor's functions areAIC(res1, res2). -
rma_uni(yi, vi, data=escalc_frame)storesnifromescalc's attribute (R'sattr(yi, "ni")) so thatfunnel(yaxis='ni'),regtest(predictor='ni')andselmodel(prec='ninv')work onescalcframes, as in R;res.ni,res.k_all,res.k_effandres.p_effgive these data summaries.res.formula_yiandres.formula_modscorrespond to R'sformula.yiandformula.mods(strings, as written by the caller). A cluster-robust fit keepsrobust.meat, the block-diagonal matrix of residual cross-products (Noneforclub_sandwich=True, where metafor stores NA). -
vcov(res, type=, na_action=)(alsores.vcov(...)) followsvcov.rma:'fixed'(default),'beta','alpha','delta','obs'/'marginal','fitted'and'resid'with R's refusals ('obs'needsM, so it is refused forrma_mh,rma_petoandrma_glmm;'resid'is refused for robust and selection-model fits and for classes withouthatvalues()). The matrices are data frames labelled with the study labels,na_actionstands for R'soptions(na.action=), and the default'fixed'of location-scale and selection models is a dict{'beta': ..., 'alpha'|'delta': ...}like R's list.type='resid'withna_action='fail'raisesInputError: R stops there with the internal error "object 'out' not found". A selection model fitted to data with missing values labels its studies 1 to k, as R does (R refits without the studies with missing values).res.Mis the dense matrixdiag(vi + tau2)forrma_uni, location-scale, selection, robust andtrimfillfits (a sparse matrix is kept forrma_mvfits withsparse=True).res.bis the old alias ofbeta;I2,H2,vt,QEandQEpof a selection model arenan, as R storesNA.rma_mvfits havek_effbut noni.rma_glmmis checked only againstX vb X'because the R used for the fixtures has no lme4. -
coef_summary(res, type=None)(alsores.coef_summary()) iscoef(summary(res)): a data frame with the columnsestimate,se,zval(tvalanddffor the t-based tests, with the Satterthwaite or residual degrees of freedom of robust fits),pval,ci.lbandci.ub; location-scale and selection models give a dict of two frames. The column names keep R's dots. -
cbind_escalc()andrbind_escalc()keep theescalcattributes (yi_names, ...,digits,ni,slab) of the frames they combine. R keepsmeasure,slabandniperyicolumn, a frame here has one set:cbind_escalctakes them from the first argument that has them,rbind_escalcthemeasureof the first argument, as R does. -
Escalc attributes: pandas copies
DataFrame.attrsunchanged on row selection, so metafopy stores the row labels inattrs['rows']and matchesniandslabon them. A frame whose index was reset to 0..n-1 (also a plaind.iloc[:n]) cannot be matched, so itsniis dropped andslabreplaced by 1..k instead of being misassigned.rma()makes duplicated study labels unique asa,a.1;escalcasa.1,a.2. Coefficient names of terms such asrelevel(x, ref='a')use the text as written (R deparses the call). -
res.ddfof arobust(club_sandwich=True)fit staysNone(a single number elsewhere); the per-coefficient degrees of freedom are inres.dfs(a tuple) andres.robust.dfs.anova(full, reduced)ofrma_mvfits compareskandyibut notV(R compares a checksum ofV).influence()returns a different type for eachmeasure=.profile(pred=True, newmods=)rows followpredict()labels.predict(addx=True)printsXrounded todigits(R leaves it unformatted). A misspeltinfluence(measure=)gives the full result, as in R. -
progbar=defaults toFalseeverywhere (R:TRUEforpermutestandtes) and writes to stderr; thetesbar counts studies in the recursion, not R's combinations.digits=is a single number (theestelement); R's per-element vector is accepted but onlyestis used.signif_starsis the mfopt option standing forgetOption('show.signif.stars'). P-values print with the usual formatting, not R's 2-digit minimum;predictprintouts withoutslabuse 0-based row labels. Theanova(X=)hypotheses string reproduces R's1*quirk.ConfintResult.to_frame()stacks the fixed and random tables.anova(btt=)andconfint()of multi-componentrma_mvfits return list subclasses (ListAnovaResult,ConfintList);LSWaldResultlists are plain lists. -
Audit 8, fields of results: the bookkeeping fields of metafor fits that users read (
yi_f,vi_f,ni_f,weights_f,k_f,ids,not_na,subset,slab_null,outdat_f,outdat,intercept,coef_na,X_f,tau2_f,add,to,drop00,dfs,model,fit_stats,ci_null, and forrma_mvalsoyi_f,vi_f,V_f,W_f,weighted,random,formulas,withS/withG/withH/withR,Z_G1, ...) are attributes of the result classes.idscounts from 0.res.fit_statsis the 5 x 2 data frame of R'sfit.stats; the method that computed it for a givenREMLis nowfit_stats_for(REML).dfsequalsddffor fits that are not robust (R'sdfs).res.datais theMetaDataof the fit (the vectors and matrices used) and not the data frame the caller passed;slab,idsanddata.yiof the fit keep only the studies used (R keeps the studies with missing values inslab), the_fvariants hold all studies aftersubset.digitsis the single number of theestelement, not R's vector of nine;coef_nais labelled with the coefficient names (R names itX);precisof a selection model is a dict. R's model frames (mf.g,mf.s,mf.r, ...),opt.resandclass.fhave no counterpart;influence()hasinfanddfbsas data frames like R (marksis the tuple of asterisks,dfbs_arraythe raw array). -
Audit 8, more fields:
cumul()andleave1out()havezval/tval,test('t'for every t-based test) andidsbut nodata; the result ofvif()is a table, not R's list per coefficient;confint()keepstau2_min;res.controlkeeps the entries R reads elsewhere (Control.ignored);logLik()returns a float that prints like R's'log Lik.';formula()returns R's deparse (~ablat + year);res.controlofrma_glmmandrma_mvis the settings dict as passed. Tables wider than 80 characters wrap like R's matrices (metafopy.printing.SETTINGS['width']isgetOption('width')); the data frames ofescalc(and ofsummary_escalc,conv_fivenum,bind) print asprint.escalcdoes, with the digits of each column kind, rows numbered from 1 and p-values as<.0001. The audit harness lays out plain data frames, matrices and named vectors in R's format (1-based row names, seven significant digits per column) before it compares printouts; numpy arrays have no dimnames,summary()returns text (R returns a fit object), and thecoef.na.Z(Zintrcpt) andXnames of R are not reproduced. -
Audit 8, round 2 fields:
predict()hasddfandpi_ddfonly for t-based tests (R has none for z tests);anova()of a test of moderators hasclass_(R'sclass, the class vector of the model) and the likelihood-ratio testclass_f;vif()keeps its entries in a tuple that carries the names (R's listvif$ablat; useres.vifsfor the named values);res.controlofrma_mvandrma_glmmkeeps the entries R reads elsewhere (ignored) likerma_uni;g_levels_k/h_levels_kare series named by the levels;selmodel()of a trimfill fit keepsfill,se_k0,side,k0_estandp_k0(its ownk0is that of the selection model, as in R);regtest()refitsrma_unifits from the studies used, sofithask_all,ids,slab_null=Falseand the measure of R. Not reproduced:fitofregtest(model='lm')is a lm object in R (qr,effects,terms, ...) andNonehere (the coefficients are inest,zvalandpval); thechksum*,time,verboseandversionfields of refitted models; R'scoef.naof the refit is namedX(andcoef.na.ZZ...).ddfofrobust(club_sandwich=True)staysNone(see above) and that of a defaultrma_mvrobust fit is the vector of coefficient-wise degrees of freedom (R: one number);predict()of a model with missing moderators drops the rows inpi_ddf/prob(R keeps vectors of length k there).class(robust fit)in R isc('robust.rma', 'rma', 'rma.uni'), reproduced inclass_; theZ.fcolumn names of a fit withabbrev=Trueare the abbreviated ones (R keeps them in full). -
Results are immutable dataclasses:
res$field <- valueisdataclasses.replace(res, field=value), and an element that the result does not have (res$zz <- 1) cannot be added. Fits keep R's list-like access through attributes only. -
res[i]ofpredict(),rstandard(),rstudent(),blup(),ranef(),leave1out()andcumul()results is metafor's[.list.rmawith NumPy indices (0-based, slices, boolean masks), a result of the same class with the rowsi; unlike R it does not returnNULLfor an index that selects nothing. -
pairmat(btt=...)without the model returns aPairmatSpecthatanova(X=)andpredict(newmods=)complete with the model of the call, instead of metafor's lookup of the model in the enclosing call;pairmat(res, btt=...)still gives the matrix. -
The robust (
club_sandwich) denominator degrees of freedomddfare a vector in R and live in thedfsfield of metafopy. -
influence()underna_action='exclude'or'pass'returns 13 rows as R does, butis_inflhas no NA entries (R: NA). -
predict()of a model fitted to data with missing moderator values (na_action='omit'): R keepspi.seandpi.ddfat one value per study of the subset; metafopy has them for the returned rows. -
robust()of anrma_mvfit withoutclub_sandwich: R'sddfis one number, metafopy has one value per coefficient inddf(all equal) and the number indfs.predict()of a robust fit returnsddfwith one value per prediction, as R does. -
deltamethod()keeps thedigitsof the model (nine entries) and, for a vector of estimates, five (esttoci). -
Audit 8, round 3 (printing and names):
escalc()returns apandas.DataFramesubclass (metafopy.core.labelled.EscalcFrame) that prints asprint.escalc(rows from 1, thedigitsattribute for the effect sizes and variances); every selection of rows or columns, and pandas operations such asmerge, give a plainDataFramethat keepsattrs.vcalc()returns anumpy.ndarraysubclass (VCovMat) that prints asprint.vcovmat(zeros as., rows and columns from 1),blsplit()andranef()of anrma_mvfit return a dict (NamedList) that prints as an R list ($nameabove each element). The matrices metafor gives dimnames (X,X_f,Z,Z_fwith the row numbers of the data and the coefficient names,G,H,hessian,vvc, theVofrcalc(), the Hessian ofselmodel()) arenumpy.ndarraysubclasses (LabelledMatrix) withrownames,colnamesanddimnames; they keep the names only through operations that keep the shape (a transpose swaps them) and reductions or other shapes give plain arrays and numbers. A sparsevcalc(sparse=True)stays ascipy.sparse.csr_arrayand prints as SciPy does, where R prints adgCMatrix. -
Audit 8, round 3 (what
summary()andpredict()return):summary(res)is the printed text, as astrsubclass with the fields of the fit as attributes (summary(res).beta), where R'ssummary(res)is the fit itself with another print method.predict(res, vcov=True)is the predictions with the matrix invcov, where R returnslist(pred = <predictions>, vcov = <matrix>).setmfopt()returns the options as they were before,{'metafor': options}.permutest(res).coef()is the table of the coefficients with the permutation p-values (R:coef.permutest.rma.uni), not only the estimates.rma_mv,rma_uni(scale=)andrma_glmmkeep thecontrolentries the model does not use (vc.min,stepadj,hessianCtrl,optCtrl,tol, ...) inres.control, as R does. -
Audit 9 (
print(digits=<vector>)):digitsas a named vector (or mapping) ofpredict(),cumul(),leave1out()andinfluence()results is applied per column kind as metafor's.get.digitsdoes (names matched likepmatch; an unnamed vector gives its first element to every kind); useres.with_digits(...)where R callsprint(res, digits=...). Thevif()table of a location-scale fit ignoresdigitsin R (it passesdigits=NULL, so the p-values have one digit); metafopy prints it with the digits it was given. -
Audit 9 (
print(num=TRUE)):summary(res, num=True)numbers the rows of the coefficient tables of location-scale,rma_mv,rma_glmmand selection-model fits and ofvif(), as R does. -
Audit 13, printing and harness.
vcov()frames print asprint.vcovmat(four decimals,.for zeros);predict()results wrap at the console width; the selmodel and other tables with a stars column wrap onto a second block when R's do (the blank stars column counts for width 2). Reproduced in R and left as it is in metafopy:emmprep()ofrma_mhandrma_petofits gives prediction 0 with SE 0;summary(signif.stars=FALSE)is ignored (metafopy honours it);profile()draws when called, andplot()of a selmodel profile writes the textFALSEto the device. The harness now also runs 39 emmeans cases and compares the text drawn by plots (axis labels, legends) in the help examples and tests.
Optimisers and flat likelihoods¶
Where metafopy and R stop at different points of the same likelihood.
-
Optimisers: metafor's
optimBFGS and Nelder-Mead are ported forrma_glmm(CM.EL) andselmodelonly;rma_mvand location-scale fits use SciPy's BFGS, Nelder-Mead and others, as do the other optimisers, and agree to optimiser tolerance (about 1e-4 relative). -
verbose: progress messages go to stderr and traces to stdout as R prints them. The iteration traces ofrma_mvandrma_glmmcome from SciPy's optimisers, not nlminb, glm or glmer, so only their progress messages and tables match R. -
rma_mvwithstruct='UN'and a fixedtau2component can stop at a worse local maximum than R's nlminb (seen on berkey1998, logLik 1.799 against 1.936);cholesky=Falseor starting values reach R's optimum. For this reason the refits ofprofile()and of the profile-likelihoodconfint()ofrma_mvfits are also started at the estimates of the full fit, and the higher of the two log-likelihoods is kept. -
rma_mv(restart=)takes a previous fit (anRMAMVResult) as well asTrue, which starts from the last parameters the optimiser tried in the session as metafor'srestart=TRUEdoes.RMAMVResult.pis the number of columns ofX; metafor'sp(the number of estimated coefficients whenbetais given) isp_eff. Onlyhesspack='numDeriv'is available for the Hessians ofoptbetaandcvvc.lambda1(lasso) is non-smooth, so the optimiser accepts the last iterate. -
selmodel(type='stepcon')uses SLSQP, not Rsolnp. ItsLRT.tau2can be larger than R's, because R refits at tau2 = 0 without the delta ordering constraint and solnp stops at a worse point. -
Location-scale models: the Hessian of the scale part uses the Richardson depth
hessian_r=4(LSControl), where metafor's numDeriv call usesr=8. The eight steps amplify round-off in the log-likelihood, so R's own standard errors of the scale coefficients carry noise of about 1e-5; metafopy agrees with R's to about 2e-6 relative on the fixtures.control={'hessian_r': 8}gives the noisy variant. -
rma_mvwithstruct='GEN'on a likelihood ridge: on konstantopoulos2011 withrandom=~year|districtandmods=~year, R's defaultnlminbrun stops at logLik -31.47369 (tau2 0.2056 and 1.3e-8, rho -0.8045) on a plateau that continues to rho = -1, where the log-likelihood is -30.98466 (tau2 3801.59 and 9.6e-4); R started there stays there. SciPy's BFGS follows the plateau to its end and now finishes there (it used to stop with 'precision loss'): a fit whose line search fails is accepted when the decrease its inverse Hessian still predicts is below 1e-10 relative (nlminb'srel.tol). The estimates therefore differ from R's default run (logLik higher by 0.49, tau2 3801.59 against 0.2056) although R's own optimum is the same; the fits withoutmods(R -32.2046, here -31.8685) and withmethod='ML'behave alike. -
permutest()of location-scale fits: the permuted fits use the Richardson depthr=8ofnumDeriv::hessianwhateverhessian_rthe fit has, and the optimiser's Nelder-Mead polish only runs when BFGS has not converged. On bcg (mods=~ablat,scale=~alloc,iter=50,seed=1) this gives R's p-values (scale: 0.1064, 0.4894, 0.2553; QS 0.6383) and the same 3 undefined standard errors of 50 permutations at the same permutations (withr=4there were 13 and the p-values 0.4054 and 0.8108). Where a permuted scale fit runs to the boundary (a log-variance heading to minus infinity) the stopping point differs from R's by 1 to 3 units (R -18 to -24; here -15 to -22 for 23 of the 32 boundary permutations and -30 to -36 for the rest), so the z values of the boundary coefficient are near zero in both (below 0.01, in the compared fits) but not equal; the other coefficients of such fits agree to about 1e-3. -
Location-scale fits with
link='identity': the 5e-4 difference of the scale standard errors seen on bcg (REML,scale=~ablat, no moderators:se_alpha[0]0.539892 in R against 0.539621 here) is not caused by the link. The slope of the scale model is -3.8e-5, so numDeriv's steps (0.1 times the coefficient, halved at every Richardson step) shrink to 3e-8 at R's depthr=8, and the second differences of the log-likelihood there are rounding error; R'sr=8andr=4already differ by 5e-4 (0.539892 against 0.539623). The defaulthessian_r=4here matches R'sr=4value to 3e-6; athessian_r=8the rounding errors of the two implementations differ (0.539570 here) and cannot be reproduced without R's long-double sums and BLAS. The log link agrees better only because its coefficients are of order one. -
rma_mv(control=):parallel,ncpusandclare accepted and ignored (serial fit);nearpd=Trueuses a finite-difference gradient instead of the analytic score;optimizer='nlminb'is BFGS, so iteration paths differ from PORT; the uncertainty-basedUNRHessian step is disabled. Location-scalesolnp/nloptrfollowrma.uni's source (valid for the identity link, otherwise reset toconstrOptimwith a warning) and constrained methods are stood in by SLSQP. -
selmodel(type='beta', steps=c(0.025, 0.975), optimizer='nlminb')on baskerville2012: R's PORT stops onrel.tol1e-10 on a flat likelihood, so estimates differ from R's by up to 9e-4 relative at the same log-likelihood; the dataset is bundled now but R's stopping point is not reproduced. -
Audit 8 (wrong values, first half):
rma(yi, vi, data = dat.konstantopoulos2011)and the other bundled data sets that are escalc objects in R carrymeasure,ni,digitsand theyi/vinames inDataFrame.attrs, as in R. Thepi.se/pi.ddfquirk ofpredict()is listed above. Relative differences of 1e-4 to 4e-3 inpval,ci,se,QM,QSandvbof location-scale,rma_mvandrma_glmm(model='CM.EL')fits come from the optimiser stopping on a flat likelihood or from R's noisy objective (see the entries for those fits); they were not traced one by one. -
Audit 8, traced against R (the log-likelihoods at the optima were compared): location-scale fits with a scale intercept near -10 to -150 (
ls_year,ls_na: tau2 is 0 to machine precision) have the same log-likelihood in R and Python to 1e-9 (-7.602255955 and -4.194072548), R's own optimisers agree to 1e-6 andnlminbwith a tightrel.toldoes not converge; scale coefficients agree to 1e-6 to 1e-5 and the Wald statistics of the scale part (QS,se.alpha,va) to 2e-4.profile()of these fits, andpermutest()(which refits for each permutation), end on different points of the flat surface. TheSPEXPrangerhoofrma_mv(random=~year|district)ondat.konstantopoulos2011is 0.05592 (Rnlminb), 0.05596 (metafopy) and 0.03527 (R Nelder-Mead) at the same REML log-likelihood -31.91350753.rma_glmm(model='UM.FS')and'UM.RS': the objective is the same as lme4's (at R's coefficients metafopy's deviance is 4e-6 above its own optimum); R's two optimisers differ in the intercept by 4e-4 (0.61510 bobyqa, 0.61554 Nelder-Mead) and metafopy's 0.61456 is 5e-4 from bobyqa; forUM.RSmetafopy's log-likelihood (-126.2962773) is higher than R's (-126.2962883). -
Audit 8 (rarely hit wrong values):
fmtx(drop0ifint=True)always drops trailing zeros, where R does so only for an integerdigits(2L; the defaultdigitsand2do not).plot()of agoshresult returns the axes drawn, where R returns the value of its last graphics call.cooks_distance()of anrma_mvfit gives a value where R's leave-one-out refit does not converge and givesNA(mv_widefixture, study 18).rma_glmm(model='UM.RS')andmodel='CM.EL'differ from R by the amounts given above (the fixturesglmm_rsandglmm_cmof the harness). A location-scale fit whose scale intercept runs to the boundary (ls_year,optbeta=True) has a singular numerical Hessian in metafopy and an invertible one in R, sovcovmatis NaN here; the scale model~ allocof thels_bcgfixture is degenerate and the simulated and re-estimated VIFs of the scale part cannot be compared. -
Audit 8 (second pass):
anova(x, X=)of linear combinations that are not of full rank givesQMdf=(m, ddf)where R has the singleNA; the escalc attributes (measure,ni,slab) are stored once inDataFrame.attrs, soattr(dat$y1, "measure")of a data frame with two escalc() results (differentvar_names) is the last measure.selmodel()with a likelihood that is flat or at the boundary (type='beta'on baskerville2012:tau2of 2.4e-6;type='truncest'withoptimizer='mads'on hackshaw1998) ends at a slightly higher log-likelihood than R (by 4e-6 and 2.5e-4), so the estimates differ by 3e-2 (tau2near 0) and 5e-3 relative. -
Location-scale fits (
rma_uni(scale=)) use a central-difference gradient for BFGS and cap the Nelder-Mead polish, sopermutest()andvif(sim=True)of a fit that ends on a boundary take about 0.05 to 0.3 s per refit like R'snlminb(before: 10 to 40 s per refit). -
Flat or boundary fits that R's own optimisers do not agree on (location-scale fits with scale moderators on
dat.bcg,rma_mvwithstruct='UN',selmodelwithtype='truncest') are insrc/audit/data/known.tomlwith the log-likelihoods that agree. -
Audit 9 (flat likelihoods, not matched): the QM, QS and F statistics of location-scale fits whose scale part is on a flat ridge differ in the fourth digit from R (the coefficients differ by 1e-5 and the optimisers stop at different points); the
confint()step of a flat profile can beNAin one and a bound in the other;rma_mv(struct='GDIAG'/'SP*')withrhoon a flat likelihood ends at another point than R'snlminb.rma_glmm(model='UM.FS')of an intercept-only model differs from R because lme4'sbobyqastops earlier than the tighter optimiser of metafopy. -
Audit 10: the standard errors of the scale part of
rma(yi, vi = 0.1, mods = ~ ablat + year, scale = ~ ablat + year)on dat.bcg differ from R by up to 40%; metafopy agrees with a central-difference Hessian and with R's owncontrol = list(hesspack = 'pracma'), R's default numDeriv step is too small for a scale coefficient of 3e-4.deltamethod(coef, vcov, digits = <vector>)stores the digits per key where R keeps the vector (whoseprint()then fails in R). Therma.glmm()refit in metafor'supdate()test differs from R in the fourth digit, like the otherrma.glmm()fits by lme4. -
Audit 11: the standard errors of the scale part of
rma(yi, vi, mods = ~ ablat + I(ablat^2) + year, scale = ~ ablat + year)on dat.bcg, and of the same fit withsubset = 1:8, differ from R by 3e-4 to 5e-3 at the same log-likelihood. The scale intercept and the uncentred year slope are almost collinear, so the Hessian is ill-conditioned: R's numDeriv andhesspack = 'pracma'Hessians differ from each other by as much, and central differences of R's own log-likelihood move by 1% to 10% with the step; metafopy's values lie in that spread.tes(alternative = 'greater')andpermutest(exact = TRUE)of the trim-and-fill fit of dat.bcg take 419 s and 545 s in R and agree with metafopy. -
rma_mv(struct='GDIAG'/'GEN')ondat.konstantopoulos2011(~ year | district) has more than one local maximum of the REML log-likelihood. R'snlminbstops at tau2 0.0826 (REML log-likelihood -32.21647, GDIAG) and tau2 3.487, rho -0.9997 (-32.20462, GEN); metafopy stops at tau2 0 (-32.21355) and tau2 3131.9, rho -1 (-31.86847), which are higher.control={'tau2_init': [0.01, 0.01]}gives R's GDIAG point. -
Not matched, because metafopy's optimiser stops elsewhere on a flat surface (R's
nlminbis PORT, which metafopy does not port):rma_mvGDIAG and GEN (metafopy reaches a higher log-likelihood than R), berkey1998UNwith fixedtau2, the fourth digit of location-scaleQM,QSandF,se.alphaand the flat-profileconfintstep ofrma_ls, selmodel fits withnlminb, and therma_glmmcases listed above. An opt-in port of PORT is a possible way to remove most of these.
rma_glmm¶
Mixed-model and conditional fits.
-
rma_glmmsupports all eight measures. The experimentalRR,RD(unconditional models only),PRandPLNgive metafor's "experimental" warning. lme4 often stops early or fails for the log and identity links (R'sRDwithUM.RSand methodML, andRDon collins1985a, end in "Cannot fit ML model"); metafopy returns the maximum-likelihood fit where R errors, does not reproduce lme4's convergence warnings, and agrees with R's non-converged estimates only to 2e-3 to 1e-1 relative. With adaptive quadrature and an identity link the quadrature likelihood can be undefined; then, like lme4, the first-stage estimates are returned (NA log-likelihood). -
rma_glmm(control={'package': ...})accepts'lme4'(default),'GLMMadaptive'and'glmmTMB', tested against R on 24 to 29 fits each (src/tests/test_glmm_packages.py). GLMMadaptive is a step-by-step port ofmixed_fit(EM stage, thenoptimBFGS with R's parscale and re-centred nodes), so it reproduces R's iterates, including where R stops short of the maximum: results agree to 1e-4 relative or better, usually 1e-8. Exceptions: the BFGS path ofUM.RSwithmodson bcg is chaotic (a 1e-12 change of the start moves the coefficients by about 1e-2), so it agrees to 25 % and its confidence bounds are not compared; the Wald and LRT heterogeneity statistics ofUM.RSon collins1985a agree to 3 %. glmmTMB uses the Laplace approximation (nAGQis ignored, as in R) and agrees to 5e-3 relative or better;I2is the worst where tau2 is near zero. Fits that fail in R (GLMMadaptive:RD,PRandRRwithUM.RS; glmmTMB:RR,RD,PR,PLNandUM.RSwithRR) are not reproduced as errors: metafopy returns its fit where one is found. Not ported: GLMMadaptive'smixed_fitoptions beyond the defaults metafor passes. -
rma_glmm(control={'optimizer': 'clogit'})and'clogistic'(conditional modelCM.EL,method='FE') are native ports ofsurvival::clogit(exact, Efron and Breslow ties) andEpi::clogistic; they agree with R to about 1e-11. R'sclogitfails on the full bcg and li2007 data, so the fixtures use smaller data sets.rma_glmm(link=, family=)accept the links logit, probit, cauchit, cloglog, log, identity and sqrt, or a family object, for the unconditional models and one-group measures; mixed fits agree with R to 3e-5 up to 1e-2, and the cauchit link is only checked with fixed effects (lme4 reports a convergence warning and a huge random-effect variance there). Thepredictprint for the new links is not compared with R. -
rma_glmm(measure='OR')(UM.FS) ondat.bcg:zvalof the pooled log odds ratio is -4.2436 against R's -4.2434; the estimate, standard error and the rest of the print agree. The cause has not been traced (the log-likelihood and estimates agree, so the difference is in the standard error); it is within the 2e-3 to 1e-1 tolerance of the other mixed-model cases. -
rma_glmm(model='CM.EL')withcontrol={'optimizer': ...}:optimwithmethodBFGS or Nelder-Mead (also writtenoptimizer='BFGS'or'Nelder-Mead',optmethod=,optCtrl=) are ports of R's routines and reproduce R's estimates, log-likelihood and its failure to converge on the saturated model (bcg,method='FE':QE_LRTis NA for Nelder-Mead in both); the other optimisers (CG,L-BFGS-B,nlm,uobyqa,newuoa,bobyqa,nloptr,hjk, ... as inselmodel) are replaced by the closest SciPy method, so their paths differ and they reach the estimates to about 1e-6 relative.QE_Wldof this model on bcg is ill-conditioned in R itself (R: 269 withnlminb, 199 withoptim, 68 withbobyqa, NA withCG, 0 withnlm); metafopy gives other values (150 to 200) with each optimiser andQE_Wldis not comparable. The defaultnlminbis a BFGS stand-in for R's PORT routine, as before. Fixtures formethod='ML'need lme4 in R and are not included; the ML fit with another optimiser is only checked against the default optimiser.CM.ELstandard errors with the optimisers differ from R's by up to 5e-4 relative (finite-difference Hessian of an integral). -
rma_glmmhas no R reference forpredtype,newviandna_actionin the tests (the R librarylme4is not installed where the fixtures were made); the code path is that ofrma_uni, which is compared with R. -
rma_glmm(model='CM.EL'): standard errors can differ from R's by 0.2 to 0.5 % (collins1985a, EE: 0.098986, R 0.099461). R's Hessian is a finite-difference one of an objective that is noisy (integrate()withrel.tol1.2e-4 and BiasedUrn's truncation), so the value moves by about 1e-3 under 1e-9 shifts of the evaluation point and cannot be matched. The defaultdnchgcalcis the smooth density;dnchgcalc='dFNCHypergeo'ordnchgprecuses a port of the truncating routine for the Hessian (slower, andQE_Wldbecomes unstable). The derivative-free optimisers (hjk,BBoptim,nmk,mads,subplex) agree with R to about 1e-5 relative, not 1e-6 as stated above. -
rma_glmmmaximises the exact likelihood; R fits the mixed models with lme4 (adaptive quadrature or Laplace, its own optimiser), so R's estimates depend oncontrolin the third or fourth digit (the expected values in metafor's tests differ between the variants) and metafopy agrees with them to about 1e-3.control['optimizer']of R keeps the name given ('BFGS'); metafopy stores'optim'andoptmethod.
Robust and cluster-robust fits¶
robust() and club_sandwich=True.
-
robust(club_sandwich=True, coef_test='saddlepoint')solves the saddlepoint equation with a port of R'suniroot(tolerance eps^0.25), so p-values match R to about 1e-7. -
robust()fits (plain andclub_sandwich=True) are refused byrstandard,rstudent,dfbetas,influence,leave1out,cumul,permutest,funnel(with moderators),qqnormandplot_permutestexactly where metafor refusesrobust.rmaobjects; before this version metafopy ran them on the underlying model and ignored the robust variance.simulate(),cooks_distance()andhatvalues()still work, as in R. Forrma_mvfits R has noinfluence,leave1out,cumulorpermutestmethod at all, and metafopy raises the correspondingNotSupportedErrororArgumentError. -
predict(),forest(),regplot()andaddpoly()of aclub_sandwich=Truefit use the Satterthwaite degrees of freedom of the linear contrasts (clubSandwich::linear_contrast) forcoef_test='Satterthwaite'; they agree with R to 1e-6. Withcoef_test='saddlepoint'R refusespredict()and so does metafopy. -
blup()andranef()of arobust(..., club_sandwich=True)fit reproduce R's prediction intervals, which come fromqt(level/2, df = x$ddf)with one Satterthwaite degree of freedom per coefficient; R multiplies that vector with the vector of the standard errors of the studies, so the quantiles are recycled over the studies (study 1 gets the quantile of the intercept, study 2 that of the slope, ...) and R warns "longer object length is not a multiple of shorter object length" (twice) when the number of studies is not a multiple of the number of coefficients. This is probably a bug in R; metafopy follows it on purpose and gives the same warning (bcg withmods='~ ablat',cluster='alloc': study 1 interval -2.4384 to 0.4335, where the normal quantile would give -1.5480 to -0.4570). Missing studies take part in the recycling, as in R. -
anova()of aclub_sandwich=Truefit:rhscannot be combined withX(a warning, and zero is used, as in R). The Xb results keep thehyptable out of the printed output, as for other fits. -
Audit 8, leftovers:
sandwichesof arobust(club_sandwich=True)fit has thecoef_testandconf_inttables ofclubSandwichand, with moderators, aWald_testtable with the test, F statistic,delta, degrees of freedom and p-value; R'sWald_testisNAwithout moderators and so is metafopy's.rma_mvkeeps the matrices given asRin a mapping with an entry (None) for every~ 1 | factorterm, as R's list.dfbetas()ofrma_mvfits has syntactic column names (log.year...1900...1000.), as R's data frame. Not reproduced: R's graphics state read back withpar()in the scripts (mar,mfrow,cexof the device; matplotlib keeps the grid in the figure), theZ.G1/Z.G2column names of fits with dropped rows,yi.names/vi.namesafter the columns of anescalcframe are dropped (the attributes stay inDataFrame.attrs), the list form ofdat.moura2021(metafopy.datasets.load()returns the data frame,metafopy.datasets.trees.load_tree('moura2021')the tree), and the fields R lets a test assign into a result (res$time <- NULL,sav$inf <- ...; the results are frozen). Apermutest(exact=TRUE)of~ factor(alloc)with a missing value at a further permutation in one audit run is an R artefact: a separate run of R 4.3.3 gives the two missing values at 981 and 14361 only.
Plots¶
Drawing with matplotlib instead of base graphics.
-
forest()/addpoly()support all fivepredstyles andfonts(families map to matplotlib:sanstosans-serif,serif,monotomonospace).preddistis a tuple(x, y)or a dict;addpolytakespi_se,pi_dist,pi_ddfandpi_levelkeywords instead of attributes onpi.lb;shade/distrecognise the supported transformations by function identity (numpy.exp,transf_ilogit,plogis, ...). R'spar(family)leak between plots does not exist. -
plot_profile()of the listprofile()returns draws one panel per component and honoursreflinein every panel (metafor's loop does not pass it on).profile()of location-scale fits takesplot=andax=. -
Plot
...arguments are explicit keywords: unknown keywords raiseTypeErrorwhere R silently ignores them.layout=ofplot_infl,plot_permutestandplot_vifis ignored with R's deprecation warning.legendpositions are strings or{'x': ...}, not numeric coordinates.plot_profile(log=)raisesInputErrorfor non-positive limits.baujat(las=)is applied only when given.forest(clim=)is not forwarded for cumulative results (as in R). -
forest()lowerscexfor many studies as R does (height / (1.25 * k * strheight('O'))), butstrheightis that of the axes of the caller's matplotlib figure, not of R's 7 x 7 inch pdf device (0.6399 against 0.5309 for the 56 studies ofdat.konstantopoulos2011).x[i]with one index on avcalc()matrix selects rows (R: elements). -
R's base-graphics parameters that metafor passes through
...toplot()(bty,las,xaxt,yaxt,cex,lwd,lty,col,pch,bg,main,xlab,ylab,xlim,ylim,layout,legend,cline,addgrid) are arguments only of the metafopy functions that interpret them themselves; the others are styled on the matplotlibax=the caller passes. R warns"x" is not a graphical parameterfor an unknown one; metafopy does not.
Individual functions¶
Notes on single functions and their ports.
-
conv_fivenum(method=)'qe','bc','mln'(R's estmeansd 1.0.1) and'blue'(metaBLUE 1.0.0) are native ports (metafopy.effects.fivenum_estimators), checked against R on fixtures (src/tests/r/generate_fivenum_methods.R).'blue','bc'and'mln'agree to 1e-9 relative or better:'bc'and'mln'simulate with R's generator, soconv_fivenum(seed=)reproduces R'sset.seed(seed)results exactly (withoutseedthey draw fromset_seed's stream).'qe'minimises with SciPy's L-BFGS-B (R's finite-difference gradient,ndeps = 1e-3,lmm = 5,factr = 1e7) instead of R's version 2.3; the selected distribution is the same and the mean and SD agree to about 1e-6 relative on the fixtures, up to 1e-3 for a candidate that is not selected. R drops a candidate fit that raises a warning (qbeta's, for example) where SciPy returns a poor fit that is never the best; a case where that fit would win is not reproduced. The weibull starting value comes from SciPy's L-BFGS-B, notnlminb. The quirks of R'sconv.fivenumare kept: withmethod='qe'andtest=True, a study with negative values switches the skewness test on and a skipped study leaves it on for the following studies; thedistattribute holds the distribution'qe'selected (Nonewhere its fit failed).verbose=Truelets the warnings of these methods through. -
emmeans:
emmprep,emmeans,contrast,pairsandregridare a native port, checked against emmeans 2.0.4 with metafor 5.2-1. Not ported:emm_listresults and formula left-hand sides incontrast. Non-conformable matrixweightsraiseInputError(R warns and returns garbage), and with nested factors themvtandscheffeadjustments use the rows shown, where R indexes the full grid. R'semmprepreturnsNAforrma_mhandrma_petofits and fails forselmodelfits; metafopy computes them.PASis not back-transformed, as in R (passtran='asin.sqrt'toemmprepto get it). Themvtadjustment uses SciPy's quasi-Monte Carlo integration and agrees with R to about 1e-3.rma_glmmmixed-model cases agree to 2e-3 where R'sglmerconverges to a slightly different optimum. -
vif(sim=)for location-scale models follows R'sseed+lpermutations exactly for matrix inputs; for formula models the matrix columns are permuted rather than the data variables. Scale-part simulated VIFs can differ from R in a few iterations at boundary fits. -
selmodel
profile()hasparallel,ncpus,clandprogbar(process pools or anyconcurrent.futures.Executor);confint()has no such arguments in metafor either. -
hettest()(metafor 5.2-1):progbaris off by default (R:TRUE),testraises an error if any entry is unknown (R only checks that some entry is valid), andseed=reproducesset.seed(seed)before the bootstrap draws. Results ofrma_mv,rma_glmm, location-scale and selection models are refused, as in R. -
residuals(type=): R returnsNULL(or fails) forrstandardandrstudentresiduals underna.failwithout missing studies; metafopy returns the values. The'pass'residuals ofrma_mvfits areNaNfor unused studies.weights_(type='matrix')ofrma_uniand location-scale fits is the diagonal weight matrix. -
gosh(seed=)draws the subset sizes with a port of R'sdbinom(Loader's algorithm as in R 4.5.1,metafopy.core.dbinom, identical to R in every bit on the checked sizes), because R'ssample(prob=)orders tied probabilities by their last bit; with the same seed the subsets are those of R. Reference fixtures were generated with R 4.5.1, whosedbinomdiffers from R 4.3.3's in the last bit for some sizes, so the subsets of R 4.3.3 can differ. The last bit also depends on the C library'slog,expandpow(R itself differs between platforms for the same reason), so the seeded subsets are checked against R on Linux only; the tests that compare them bit for bit are skipped on macOS and Windows. -
vif()ofrma_mvfits agrees with R to 8e-9 (default), 2e-9 (sim) and 2e-6 (reestimate=True, where each refit is a new optimisation) ondat.konstantopoulos2011.vif()takestime,LB,joinbandjoinalike R (joinafor location-scale fits only);regplot()takesat_labandbox_lty. Its per-study arguments (pch,col,bg,psize,slab) have one entry per study used in the fit, not per row of the original data, andpoints_regplot()takes onlyax(R passes...on topoints()andtext()). Theregplot()of a location-scale model draws no prediction interval, as in R (itspredict()gives none). -
predict()withna_action(R:options(na.action=)) followspredict.rma, with three details of R not copied. R keepspi.seandpi.ddfat one value per study of the subset even whenna.omitdrops studies frompredand the intervals; metafopy returns them for the rows it returns. Forpredict()of aclub_sandwich=Truefit made from data with missing values R stops under everyna.action("Could not obtain the linear contrast(s)"); metafopy returns the predictions (NaN rows for studies without moderator values). Forrma_mvfits with severaltau2/gamma2values the prediction interval of a study left out of the fit (visible underna_action='pass') is NaN, because the inner level of a dropped study is not kept. -
labbe()of anrma_unifit from counts usesadd = 1/2,to = 'only0'(or the values passed tolabbe), not those used when the model was fitted; repeatadd=/to=if the fit used other values.rma_mh()with an outcome that never occurs: whether the variance is exactly 0 or a negative rounding error (se NaN) depends on the summation order, in R as well.dfround()leaves columns withNAdigitsuntouched (the audit's reading of R; the 5.2-1 source has no explicit branch). -
Audit 8, round 3:
update(formula_=)is metafor'sformula.(ignored forrma_mhandrma_peto, one-sided formods, as in R); selecting rows of a result withres[i]raisesIndexErrorfor rows that do not exist where R's[.list.rmareturnsNULL;cumul(na_action=)keeps the rows that were used (R keepsNArows for the studies that were dropped);baujat(na_action='fail')raises when a study has missing values (R stops with "need finite 'ylim' values");fmtx()andradial()refuse adigitsof length greater than one, as R does;rma(cutoff=)has no effect (R 5.2-1 stops with "object 'addtxt' not found" whenever anabs(yi)exceeds it, a bug in.chkyisize). -
Audit 8 fixed in the code:
simulate()takesna_action(R:options(na.action=)) and pads to the studies of the subset under'exclude','pass'and'fail';k_allof a fit from counts withsubset=is the number of rows before subsetting (internallyMetaData.k_allis still the length ofkeep; the public value isMetaData.supplied). -
hettest(model)does not forwardcontrolto the refits, as in R;controlonly applies to the data-vector form. -
selmodel()refits the model on the studies used only when the method is not FE/EE/CE/ML, and keeps a givenslabwhen no study was dropped; otherwise labels are numbered 1 to k, as in R. -
anova(X=)with a rank-deficientX: R gives a single NA forQMdf; metafopy keeps the pair (m, ddf) and gives NaN for QM and QMp.
Arguments, defaults and errors¶
Arguments, defaults and errors that differ from R.
-
profile()ofrma_mvfits defaults toplot=Falseandprogbar=Falselike the other Pythonprofilemethods (R:TRUE).rstandard()ofrma_mvfits has notypeargument, as in metafor 5.2-1. -
conv_2x2: R's assignmentci[!is.na(ci)]in the complete-data path is a bug; metafopy assignscias intended. Data frames carry onemeasureandniattribute, where R tracks them per column. -
escalc(measure='HR')and'HD'are reserved names in metafor 5.2-1 with no computation: they passyiandvi/seithrough, otherwise metafopy raisesNotSupportedError(R fails with an internal error). -
forest()of anrma_mvfit withna_action='pass'does not show the rows with missing values.fsn()ofrma_mh/rma_petofits raises "Method not available" (R fails with an internal error). -
anova(object2=)androbust(clubSandwich=)are accepted as aliases ofmodel2andclub_sandwich. -
confint(control=)is accepted next to the keyword argumentstau2_min,tau2_max,tolandmaxiter(vc_min,vc_max,tol,maxiterandeptriesforrma_mv), spelt with.or_; an unknown entry raisesInputError. Printedconfint(),anova()of two models (blank cells,%after R^2, wrapped at 80 columns) andsummary()follow R's layout.predict()results havecr_lbandcr_ubas aliases ofpi_lbandpi_ub, and an explicitrows=-1inaddpoly(predict_result, rows=-1)is honoured (the defaultrows=Noneusesrow). -
controlentries that another function of metafor reads (threshold,stepadj,extendInt,vc.min,optimizer, ...) are ignored by the fitting functions,permutest,confint,hettestandselmodelas R ignores them (pmatchagainst the defaults); an entry no function reads still raises, so that a misspelt setting is not lost.robust(adjust=)with a string other than'Stata','Stata1'or'Stata2'(for example'holm') is refused, where R silently applies no adjustment; theselmethod ofanova()accepts a fit of the plain class where R stops with 'Class of ...'.emmprep(transf=)with'+.sqrt'is not supported; an unknown name gives the identity link as in emmeans. The unusually large SMD warning is given once per distinctyivector, like R'syichecks. -
Audit 7 (first run of
bin/metafor-audit.sh):control=entries a function does not use (optimizerforconfint,hettest,permutest) raise, where R ignores them; plot keywords are explicit and unknown ones raise (see above). The data sets of metadat that are lists of data frames rather than tables are not bundled; the others are (datasets.list_datasets()). Results ofanova()of location-scale fits takeadjustanddigitslikeanova.rma.time=(R's...) is implemented on the fitting functions and on every method that takes it in R;na_actionon the functions that readgetOption('na.action'); the graphical parameters of the plot functions that R passes through...are explicit keywords (see above). -
Audit 8 (arguments R reads from
...):code1tocode4(R's plotting-code hooks,evaled text) are not implemented;cooks_distance,dfbetas,rstudentandinfluenceof location-scale and selection fits are refused in R too (.chkclassinsideinfluence.rma.uni), so theirprogbaranddigitsthere are unreachable.funnel(transf=)only warns, as in R;LB(R's load-balanced scheduling) is accepted bygosh,profileandvifand changes no result;joinais accepted by everyvifand used by location-scale fits only.forest(addpred=, predstyle=),addpoly(rows=),robust(clubSandwich=),hettest(method=)andrma_glmm(model=)default toNonewhere R hasFALSE,'line',NULL,NULLand'UM.FS', with the same effect. Not implemented because they are undocumented or experimental R internals:outlist(an arbitrary R list evaluated withstr2lang),retopt(R's optimiser output),randhetandomega2(a random-heterogeneity location-scale model R'srma.unireads from...),pleasedonotreportI2thankyouverymuch, and forrma_glmmretdat,retfit,skiphet.tdistis not an argument ofrma_mvandrma_glmmin R's documentation andcutoffofrma_mvandvtypeofrma_glmmhave no counterpart.metafor.news()opens R's NEWS file and is not ported.vif()ofrma_mh,rma_petoandrma_glmmfits raisesArgumentErrorwhere R reports that VIFs are not applicable to intercept-only models. New:rma(knha=, dfs=, i2def=, r2def=, skipr2=, abbrev=),anova(att=, Z=)and theL,fixed,test,df,verbosearguments of the location-scaleanova,confint(extint=, parm=, digits=),permutest(verbose=, fixed=)(permciwarns for location-scale fits, as in R),profile(sub1=, LB=),addpoly(cr_lb=, cr_ub=),rcalc(rowid=, vnames=, noid=),pairmat(fixed=), all compared with R insrc/tests/test_audit8_args.py.
Not ported¶
What is deliberately left out.
-
Not ported:
reporter()(writes an R Markdown report; Jupyter notebooks are the Python equivalent);ranef/blupforrma_glmmdo not exist in metafor either. -
Undocumented switches that R reads from
...(not in the Rd files):code1tocode4(R evaluates the code it is given),outlist,retopt,tdist,abbrev,skiphet,retdat,retfit,omega2,randhet, and the spellingnearPDofnearpd, are not ported; the harness does not generate them.print(x, num=, legend=)ofrma.unifits is not ported either. -
Not implemented: the model frames
mf.g,mf.s, ... ofrma.mv(seemf.*above) and the column names metafor givesZ.S,Z.G1andZ.G2(mf.s[[j]]11, ...; internal to R's code).
The audit¶
How the known differences are tracked.
-
The audit's known list has narrow entries for flat-likelihood fits (ls, selmodel finite-difference Hessian, glmm UM.FS/UM.RS/CM.EL, mv_sp rho), device-dependent
par(cex), gosh ties ofdbinom, and attributes that R drops indata.frame()andc()in scripts (measure,ni). -
Audit 9 (environment only): the
gosh()socket cluster and the timeout of R'spermutest()in the harness depend on the sandbox, not on metafopy.