#' Perform a \emph{p}-curve analysis #' #' This function performs a \eqn{p}-curve analysis using a \code{meta} object or calculated effect size data. #' #' @usage pcurve(x, effect.estimation = FALSE, N, dmin = 0, dmax = 1) #' #' @param x Either an object of class \code{meta}, generated by the \code{metagen}, \code{metacont}, #' \code{metacor}, \code{metainc}, or \code{metabin} function, or a dataframe containing the calculated effect size #' (named \code{TE}, log-transformed if based on a ratio), standard error (named \code{seTE}) and study label (named \code{studlab}) #' for each study. #' @param effect.estimation Logical. Should the true effect size underlying the \emph{p}-curve be estimated? #' If set to \code{TRUE}, a vector containing the total sample size for each study must be provided for #' \code{N}. \code{FALSE} by default. #' @param N A numeric vector of same length as the number of effect sizes included in \code{x} specifying the #' total sample size \eqn{N} corresponding to each effect. Only needed if \code{effect.estimation = TRUE}. #' @param dmin If \code{effect.estimation = TRUE}: lower limit for the effect size (\eqn{d}) space in which #' the true effect size should be searched. Must be greater or equal to 0. Default is 0. #' @param dmax If \code{effect.estimation = TRUE}: upper limit for the effect size (\eqn{d}) space in which #' the true effect size should be searched. Must be greater than 0. Default is 1. #' #' @details #' \strong{P-curve Analysis} #' #' \eqn{P}-curve analysis (Simonsohn, Simmons & Nelson, 2014, 2015) has been proposed as a method #' to detect \eqn{p}-hacking and publication bias in meta-analyses. #' #' \eqn{P}-Curve assumes that publication bias #' is not only generated because researchers do not publish non-significant results, #' but also because analysts “play” around with their data ("\eqn{p}-hacking"; e.g., selectively removing outliers, #' choosing different outcomes, controlling for different variables) until a non-significant #' finding becomes significant (i.e., \eqn{p<0.05}). #' #' The method assumes that for a specific research #' question, \eqn{p}-values smaller 0.05 of included studies should follow a right-skewed distribution #' if a true effect exists, even when the power in single studies was (relatively) low. Conversely, #' a left-skewed \eqn{p}-value distribution indicates the presence of \eqn{p}-hacking and absence of #' a true underlying effect. To control for "ambitious" \eqn{p}-hacking, \eqn{P}-curve also incorporates a #' "half-curve" test (Simonsohn, Simmons & Nelson, 2014, 2015). #' #' Simonsohn et al. (2014) #' stress that \eqn{p}-curve analysis should only be used for test statistics which were actually of interest #' in the context of the included study, and that a detailed table documenting the reported results #' used in for the \eqn{p}-curve analysis should be created before communicating #' results (\href{http://www.p-curve.com/Supplement/}{link}). #' #' \strong{Implementation in the function} #' #' To generate the \eqn{p}-curve and conduct the analysis, this function reuses parts of the \emph{R} code underlying #' the \href{http://p-curve.com/app4/pcurve_app4.052.r}{P-curve App 4.052} (Simonsohn, 2017). The effect sizes #' included in the \code{meta} object or \code{data.frame} provided for \code{x} are transformed #' into \eqn{z}-values internally, which are then used to calculate {p}-values and conduct the #' Stouffer and Binomial test used for the \eqn{p}-curve analysis. Interpretations of the function #' concerning the presence or absence/inadequateness of evidential value are made according to the #' guidelines described by Simonsohn, Simmons and Nelson (2015): #' #' \itemize{ #' \item \strong{Evidential value present}: The right-skewness test is significant for the half curve with #' \eqn{p<0.05} \strong{or} the \eqn{p}-value of the right-skewness test is \eqn{<0.1} for both the half and full curve. #' \item \strong{Evidential value absent or inadequate}: The flatness test is \eqn{p<0.05} for the full curve #' \strong{or} the flatness test for the half curve and the binomial test are \eqn{p<0.1}. #'} #' #' For effect size estimation, the \code{pcurve} function implements parts of the loss function #' presented in Simonsohn, Simmons and Nelson (2014b). #' The function generates a loss function for candidate effect sizes \eqn{\hat{d}}, using \eqn{D}-values in #' a Kolmogorov-Smirnov test as the metric of fit, and the value of \eqn{\hat{d}} which minimizes \eqn{D} #' as the estimated true effect. #' #' It is of note that a lack of robustness of \eqn{p}-curve analysis results #' has been noted for meta-analyses with substantial heterogeneity (van Aert, Wicherts, & van Assen, 2016). #' Following van Aert et al., adjusted effect size estimates should only be #' reported and interpreted for analyses with \eqn{I^2} values below 50 percent. #' A warning message is therefore printed by #' the \code{pcurve} function when \code{x} is of class \code{meta} and the between-study heterogeneity #' of the meta-analysis is substantial (i.e., \eqn{I^2} greater than 50 percent). #' #' #' @references Harrer, M., Cuijpers, P., Furukawa, T.A, & Ebert, D. D. (2019). #' \emph{Doing Meta-Analysis in R: A Hands-on Guide}. DOI: 10.5281/zenodo.2551803. #' \href{https://bookdown.org/MathiasHarrer/Doing_Meta_Analysis_in_R/pcurve.html}{Chapter 9.2}. #' #' Simonsohn, U., Nelson, L. D., & Simmons, J. P. (2014a). P-curve: a Key to the File-drawer. #' \emph{Journal of Experimental Psychology, 143}(2), 534. #' #' Simonsohn, U., Nelson, L. D. & Simmons, J. P. (2014b). P-Curve and Effect Size: #' Correcting for Publication Bias Using Only Significant Results. #' \emph{Perspectives on Psychological Science 9}(6), 666–81. #' #' Simonsohn, U., Nelson, L. D. & Simmons, J. P. (2015). Better P-Curves: Making P-Curve #' Analysis More Robust to Errors, Fraud, and Ambitious P-Hacking, a Reply to Ulrich and Miller (2015). #' \emph{Journal of Experimental Psychology, 144}(6), 1146-1152. #' #' Simonsohn, U. (2017). R code for the P-Curve App 4.052. http://p-curve.com/app4/pcurve_app4.052.r (Accessed 2019-08-16). #' #' Van Aert, R. C., Wicherts, J. M., & van Assen, M. A. (2016). #' Conducting meta-analyses based on p values: Reservations and recommendations for applying #' \emph{p}-uniform and \emph{p}-curve. \emph{Perspectives on Psychological Science, 11}(5), 713-729. #' #' @author Mathias Harrer & David Daniel Ebert #' #' @return Returns a plot and main results of the pcurve analysis: #' \itemize{ #' \item \strong{P-curve plot}: A plot displaying the observed \eqn{p}-curve and significance results #' for the right-skewness and flatness test. #' \item \strong{Number of studies}: The number of studies provided for the analysis, the number #' of significant \eqn{p}-values included in the analysis, and the number of studies with \eqn{p<0.025} #' used for the half-curve tests. #' \item \strong{Test results}: The results for the right-skewness and flatness test, including the #' \eqn{p_{binomial}} value, as well as the \eqn{z} and \eqn{p} value for the full and half-curve test. #' \item \strong{Power Estimate}: The power estimate and 95\% confidence interval. #' \item \strong{Evidential value}: Two lines displaying if evidential value is present and/or absent/inadequate based #' on the results (using the guidelines by Simonsohn et al., 2015, see Details). #' \item \strong{True effect estimate}: If \code{effect.estimation} is set to \code{TRUE}, the estimated true effect #' \eqn{\hat{d}} is returned additionally. #'} #' #' #' If results are saved to a variable, a list of class \code{pcurve} containing the following objects is returned: #' \itemize{ #' \item \code{pcurveResults}: A data frame containing the results for the right-skewness and flatness test, including the #' \eqn{p_{binomial}} value, as well as the \eqn{z} and \eqn{p} value for the full and half-curve test. #' \item \code{Power}: The power estimate and 95\% confidence interval. #' \item \code{PlotData}: A data frame with the data used in the \eqn{p}-curve plot. #' \item \code{Input}: A data frame containing the provided effect sizes, calculated \eqn{p}-values and individual results for each included (significant) effect. #' \item \code{EvidencePresent}, \code{EvidenceAbsent}, \code{kInput}, \code{kAnalyzed}, \code{kp0.25}: Further results of the \eqn{p}-curve analysis, including the presence/absence of evidence interpretation, #' and number of provided/significant/\eqn{p<0.025} studies. #' \item \code{I2}: \eqn{I^2}-Heterogeneity of the studies provided as input (only when \code{x} is of class \code{meta}). #' \item \code{class.meta.object}: \code{class} of the original object provided in \code{x}. #'} #' #' #' @import stringr poibin #' @importFrom graphics abline axis lines mtext par plot points rect segments text #' @importFrom stats as.formula hat influence ks.test optimize pbinom pchisq pf pnorm pt punif qchisq qf qnorm qt reformulate reorder setNames uniroot #' #' @export pcurve #' #' @seealso #' \code{\link{eggers.test}} #' #' @examples #' # Example 1: Use metagen object, do not estimate d #' suppressPackageStartupMessages(library(meta)) #' data("ThirdWave") #' meta1 = metagen(TE,seTE, studlab=ThirdWave$Author, data=ThirdWave) #' pcurve(meta1) #' #' # Example 2: Provide Ns, calculate d estimate #' N = c(105, 161, 60, 37, 141, 82, 97, 61, 200, 79, 124, 25, 166, 59, 201, 95, 166, 144) #' pcurve(meta1, effect.estimation = TRUE, N = N) #' #' # Example 3: Use metacont object, calculate d estimate #' data("amlodipine") #' meta2 <- metacont(n.amlo, mean.amlo, sqrt(var.amlo), #' n.plac, mean.plac, sqrt(var.plac), #' data=amlodipine, studlab=study, sm="SMD") #' N = amlodipine$n.amlo + amlodipine$n.plac #' pcurve(meta2, effect.estimation = TRUE, N = N, dmin = 0, dmax = 1) #' #' # Example 4: Construct x object from scratch #' sim = data.frame("studlab" = c(paste("Study_", 1:18, sep = "")), #' "TE" = c(0.561, 0.296, 0.648, 0.362, 0.770, 0.214, 0.476, #' 0.459, 0.343, 0.804, 0.357, 0.476, 0.638, 0.396, 0.497, #' 0.384, 0.568, 0.415), #' "seTE" = c(0.338, 0.297, 0.264, 0.258, 0.279, 0.347, 0.271, 0.319, #' 0.232, 0.237, 0.385, 0.398, 0.342, 0.351, 0.296, 0.325, #' 0.322, 0.225)) #' pcurve(sim) pcurve = function(x, effect.estimation = FALSE, N, dmin = 0, dmax = 1){ # Rename x to metaobject, remove x metaobject = x rm(x) # Stop if metaobject is not meta or does not contain TE or seTE column if (!(class(metaobject)[1] %in% c("metagen", "metabin", "metacont", "metacor", "metainc", "meta", "metaprop"))){ for (i in 1:length(colnames(metaobject))){ te.exists = FALSE if (colnames(metaobject)[i]=="TE"){ te.exists = TRUE break } else { } } for (i in 1:length(colnames(metaobject))){ sete.exists = FALSE if (colnames(metaobject)[i]=="seTE"){ sete.exists = TRUE break } else { } } for (i in 1:length(colnames(metaobject))){ studlab.exists = FALSE if (colnames(metaobject)[i]=="studlab"){ studlab.exists = TRUE break } else { } } if(te.exists == FALSE | sete.exists ==FALSE | studlab.exists ==FALSE){ stop("x must be a meta-analysis object generated by meta functions or a data.frame with columns labeled studlab, TE, and seTE.") } } #Disable scientific notation options(scipen=999) # Calculate Z zvalues.input = abs(metaobject$TE/metaobject$seTE) ############################################## # 1. Functions ############################### ############################################## getncp.f =function(df1,df2, power) { error = function(ncp_est, power, x, df1,df2) pf(x, df1 = df1, df2=df2, ncp = ncp_est) - (1-power) xc=qf(p=.95, df1=df1,df2=df2) return(uniroot(error, c(0, 1000), x = xc, df1 = df1,df2=df2, power=power)$root) } getncp.c =function(df, power) { xc=qchisq(p=.95, df=df) error = function(ncp_est, power, x, df) pchisq(x, df = df, ncp = ncp_est) - (1-power) return(uniroot(error, c(0, 1000), x = xc, df = df, power=power)$root) } getncp=function(family,df1,df2,power) { if (family=="f") ncp=getncp.f(df1=df1,df2=df2,power=power) if (family=="c") ncp=getncp.c(df=df1,power=power) return(ncp) } percent <- function(x, digits = 0, format = "f", ...) { paste(formatC(100 * x, format = format, digits = digits, ...), "%", sep = "") } pbound=function(p) pmin(pmax(p,2.2e-16),1-2.2e-16) prop33=function(pc) { prop=ifelse(family=="f" & p<.05,1-pf(qf(1-pc,df1=df1, df2=df2),df1=df1, df2=df2, ncp=ncp33),NA) prop=ifelse(family=="c" & p<.05,1-pchisq(qchisq(1-pc,df=df1), df=df1, ncp=ncp33),prop) prop } stouffer=function(pp) sum(qnorm(pp),na.rm=TRUE)/sqrt(sum(!is.na(pp))) ############################################################################### # 2. Process data ############################################################ ############################################################################### # Note: due to reliance on the pcurve-app function, z-scores are pasted into characters first # and then screened to generate variables necessary for further computation zvalues.input = paste("z=", zvalues.input, sep="") filek = "input" raw = zvalues.input raw=tolower(raw) ktot=length(raw) k=seq(from=1,to=length(raw)) stat=substring(raw,1,1) test=ifelse(stat=="r","t",stat) # Create family family=test family=ifelse(test=="t","f",family) family=ifelse(test=="z","c",family) #family: f,c converting t-->f and z-->c # Find comma,parentheses,equal sign par1 =str_locate(raw,"\\(")[,1] par2 =str_locate(raw,"\\)")[,1] comma=str_locate(raw,",")[,1] eq =str_locate(raw,"=")[,1] # DF for t-tests df=as.numeric(ifelse(test=="t",substring(raw,par1+1,par2 -1),NA)) # DF1 df1=as.numeric(ifelse(test=="f",substring(raw,par1+1,comma-1),NA)) df1=as.numeric(ifelse(test=="z",1,df1)) df1=as.numeric(ifelse(test=="t",1,df1)) df1=as.numeric(ifelse(test=="c",substring(raw,par1+1,par2 -1),df1)) # DF2 df2=as.numeric(ifelse(test=="f",substring(raw,comma+1,par2-1),NA)) df2=as.numeric(ifelse(test=="t",df,df2)) equal=abs(as.numeric(substring(raw,eq+1))) value=ifelse((stat=="f" | stat=="c"),equal,NA) value=ifelse(stat=="r", (equal/(sqrt((1-equal**2)/df2)))**2,value) value=ifelse(stat=="t", equal**2 ,value) value=ifelse(stat=="z", equal**2 ,value) p=ifelse(family=="f",1-pf(value,df1=df1,df2=df2),NA) p=ifelse(family=="c",1-pchisq(value,df=df1),p) p=pbound(p) #Bound it to level of precision, see function 3 above ksig= sum(p<.05,na.rm=TRUE) #significant studies khalf=sum(p<.025,na.rm=TRUE) #half p-curve studies if (ksig <= 2){ stop("Two or less significant (p<0.05) effect sizes were detected, so p-curve analysis cannot be conducted.") } ############################################################################## # 3. PP-values ############################################################### ############################################################################## # Right Skew, Full p-curve ppr=as.numeric(ifelse(p<.05,20*p,NA)) ppr=pbound(ppr) # Right Skew, half p-curve ppr.half=as.numeric(ifelse(p<.025,40*p,NA)) ppr.half=pbound(ppr.half) # Power of 33% ncp33=mapply(getncp,df1=df1,df2=df2,power=1/3,family=family) # Full-p-curve pp33=ifelse(family=="f" & p<.05,3*(pf(value, df1=df1, df2=df2, ncp=ncp33)-2/3),NA) pp33=ifelse(family=="c" & p<.05,3*(pchisq(value, df=df1, ncp=ncp33)-2/3),pp33) pp33=pbound(pp33) # half p-curve prop25=3*prop33(.025) prop25.sig=prop25[p<.05] #Compute pp-values for the half pp33.half=ifelse(family=="f" & p<.025, (1/prop25)*(pf(value,df1=df1,df2=df2,ncp=ncp33)-(1-prop25)),NA) pp33.half=ifelse(family=="c" & p<.025, (1/prop25)*(pchisq(value,df=df1, ncp=ncp33)-(1-prop25)),pp33.half) pp33.half=pbound(pp33.half) ############################################################################## # 4. Stouffer & Binomial test ################################################ ############################################################################## # Convert pp-values to Z scores, using Stouffer function above Zppr = stouffer(ppr) Zpp33 = stouffer(pp33) Zppr.half = stouffer(ppr.half) Zpp33.half = stouffer(pp33.half) # Overall p-values from Stouffer test p.Zppr = pnorm(Zppr) p.Zpp33 = pnorm(Zpp33) p.Zppr.half = pnorm(Zppr.half) p.Zpp33.half = pnorm(Zpp33.half) # Save results to file main.results=as.numeric(c(ktot, ksig, khalf, Zppr, p.Zppr, Zpp33, p.Zpp33, Zppr.half, p.Zppr.half, Zpp33.half, p.Zpp33.half)) # BINOMIAL # Observed share of p<.025 prop25.obs=sum(p<.025)/sum(p<.05) # Flat null binom.r=1-pbinom(q=prop25.obs*ksig- 1, prob=.5, size=ksig) # Power of 33% null binom.33=ppoibin(kk=prop25.obs*ksig,pp=prop25[p<.05]) # Save binomial results binomial=c(mean(prop25.sig), prop25.obs, binom.r, binom.33) # Beautifyier Function cleanp=function(p) { p.clean=round(p,4) #Round it p.clean=substr(p.clean,2,6) #Drop the 0 p.clean=paste0("= ",p.clean) if (p < .0001) p.clean= " < .0001" if (p > .9999) p.clean= " > .9999" return(p.clean) } #If there are zero p<.025, change Stouffer values for half-p-curve tests for "N/A" messages if (khalf==0) { Zppr.half ="N/A" p.Zppr.half ="=N/A" Zpp33.half ="N/A" p.Zpp33.half ="=N/A" } #If there are more than 1 p<.025, round the Z and beutify the p-values if (khalf>0) { Zppr.half =round(Zppr.half,2) Zpp33.half =round(Zpp33.half,2) p.Zppr.half=cleanp(p.Zppr.half) p.Zpp33.half=cleanp(p.Zpp33.half) } #Clean results for full test Zppr=round(Zppr,2) Zpp33=round(Zpp33,2) p.Zppr=cleanp(p.Zppr) p.Zpp33=cleanp(p.Zpp33) binom.r=cleanp(binom.r) binom.33=cleanp(binom.33) ################################################ # 5. Power #################################### ################################################ powerfit=function(power_est) { ncp_est=mapply(getncp,df1=df1,df2=df2,power=power_est,family=family) pp_est=ifelse(family=="f" & p<.05,(pf(value,df1=df1,df2=df2,ncp=ncp_est)-(1-power_est))/power_est,NA) pp_est=ifelse(family=="c" & p<.05,(pchisq(value,df=df1,ncp=ncp_est)-(1-power_est))/power_est,pp_est) pp_est=pbound(pp_est) return(stouffer(pp_est)) } fit=c() fit=abs(powerfit(.051)) for (i in 6:99) fit=c(fit,abs(powerfit(i/100))) mini=match(min(fit,na.rm=TRUE),fit) hat=(mini+4)/100 x.power=seq(from=5,to=99)/100 get.power_pct =function(pct) { #Function that finds power that gives p-value=pct for the Stouffer test #for example, get.power_pct(.5) returns the level of power that leads to p=.5 for the stouffer test. #half the time we would see p-curves more right skewed than the one we see, and half the time #less right-skewed, if the true power were that get.power_pct(.5). So it is the median estimate of power #similarliy, get.power_pct(.1) gives the 10th percentile estimate of power... #Obtain the normalized equivalent of pct, e.g., for 5% it is -1.64, for 95% it is 1.64 z=qnorm(pct) #convert to z because powerfit() outputs a z-score. #Quantify gap between computed p-value and desired pct error = function(power_est, z) powerfit(power_est) - z #Find the value of power that makes that gap zero, (root) return(uniroot(error, c(.0501, .99),z)$root) } # Boundary conditions p.power.05=pnorm(powerfit(.051)) #Proability p-curve would be at least at right-skewed if power=.051 p.power.99=pnorm(powerfit(.99)) #Proability p-curve would be at least at right-skewed if power=.99 # Lower end of ci if (p.power.05<=.95) power.ci.lb=.05 if (p.power.99>=.95) power.ci.lb=.99 if (p.power.05>.95 && p.power.99<.95) power.ci.lb=get.power_pct(.95) # Higher end of CI if (p.power.05<=.05) power.ci.ub=.05 if (p.power.99>=.05) power.ci.ub=.99 if (p.power.05>.05 && p.power.99<.05) power.ci.ub=get.power_pct(.05) # Save power fit power_results=c(power.ci.lb,hat,power.ci.ub) ############################################################################## # 6. Plot ################################################################### ############################################################################## # Green line (Expected p-curve for 33% power) gcdf1=prop33(.01) gcdf2=prop33(.02) gcdf3=prop33(.03) gcdf4=prop33(.04) green1=mean(gcdf1,na.rm=TRUE)*3 green2=mean(gcdf2-gcdf1,na.rm=TRUE)*3 green3=mean(gcdf3-gcdf2,na.rm=TRUE)*3 green4=mean(gcdf4-gcdf3,na.rm=TRUE)*3 green5=mean(1/3-gcdf4,na.rm=TRUE)*3 green=100*c(green1,green2,green3,green4,green5) # Blue line (observed p-curve) ps=ceiling(p[p<.05]*100)/100 blue=c() for (i in c(.01,.02,.03,.04,.05)) blue=c(blue,sum(ps==i,na.rm=TRUE)/ksig*100) # Red line red=c(20,20,20,20,20) # Make the graph x = c(.01,.02,.03,.04,.05) par(mar=c(6,5.5,1.5,3)) moveup=max(max(blue[2:5])-66,0) ylim=c(0,105+moveup) legend.top=100+moveup plot(x,blue, type='l', col='dodgerblue2', main="", lwd=2, xlab="", ylab="", xaxt="n",yaxt="n", xlim=c(0.01,0.051), ylim=ylim, bty='L', las=1,axes=F) x_=c(".01",".02",".03",".04",".05") axis(1,at=x,labels=x_) y_=c("0%","25%","50%","75%","100%") y=c(0,25,50,75,100) axis(2,at=y,labels=y_,las=1,cex.axis=1.2) mtext("Percentage of test results",font=2,side=2,line=3.85,cex=1.25) mtext("p ",font=4,side=1,line=2.3,cex=1.25) mtext(" -value", font=2,side=1,line=2.3,cex=1.25) points(x,blue,type="p",pch=20,bg="dodgerblue2",col="dodgerblue2") text(x+.00075,blue+3.5,percent(round(blue)/100),col='black', cex=.75) lines(x,red, type='l', col='firebrick2', lwd=1.5, lty=3) lines(x,green, type='l', col='springgreen4', lwd=1.5, lty=5) tab1=.017 #Labels for line at p=.023 in x-axis tab2=tab1+.0015 #Test results and power esimates at tab1+.0015 gap1=9 #between labels gap2=4 #between lable and respective test (e.g., "OBserved p-curve" and "power estimate") font.col='gray44' text.blue=paste0("Power estimate: ",percent(hat),", CI(", percent(power.ci.lb),",", percent(power.ci.ub),")") text(tab1,legend.top, adj=0,cex=.85,bquote("Observed "*italic(p)*"-curve")) text(tab2,legend.top-gap2,adj=0,cex=.68,text.blue,col=font.col) text.red=bquote("Tests for right-skewness: "*italic(p)*""[Full]~.(p.Zppr)*", "*italic(p)*""[Half]~.(p.Zppr.half)) #note: .() within bquote prints the value rather than the variable name text(tab1,legend.top-gap1, adj=0,cex=.85, "Null of no effect" ) text(tab2,legend.top-gap1-gap2, adj=0,cex=.68, text.red, col=font.col ) text.green=bquote("Tests for flatness: "*italic(p)*""[Full]~.(p.Zpp33)*", "*italic(p)*""[half]~.(p.Zpp33.half)*", "*italic(p)*""[Binomial]~.(binom.33)) text(tab1,legend.top-2*gap1, adj=0,cex=.85,"Null of 33% power") text(tab2,legend.top-2*gap1-gap2, adj=0,cex=.68,text.green,col=font.col) segments(x0=tab1-.005,x1=tab1-.001,y0=legend.top,y1=legend.top, col='dodgerblue2',lty=1,lwd=1.5) segments(x0=tab1-.005,x1=tab1-.001,y0=legend.top-gap1, y1=legend.top-gap1,col='firebrick2',lty=3,lwd=1.5) segments(x0=tab1-.005,x1=tab1-.001,y0=legend.top-2*gap1,y1=legend.top-2*gap1,col='springgreen4',lty=2,lwd=1.5) rect(tab1-.0065,legend.top-2*gap1-gap2-3,tab1+.032,legend.top+3,border='gray85') msgx=bquote("Note: The observed "*italic(p)*"-curve includes "*.(ksig)* " statistically significant ("*italic(p)*" < .05) results, of which "*.(khalf)* " are "*italic(p)*" < .025.") mtext(msgx,side=1,line=4,cex=.65,adj=0) kns=ktot-ksig if (kns==0) ns_msg="There were no non-significant results entered." if (kns==1) ns_msg=bquote("There was one additional result entered but excluded from "*italic(p)*"-curve because it was "*italic(p)*" > .05.") if (kns>1) ns_msg=bquote("There were "*.(kns)*" additional results entered but excluded from "*italic(p)*"-curve because they were "*italic(p)*" > .05.") mtext(ns_msg,side=1,line=4.75,cex=.65,adj=0) ############################################################################## # 7 Save Calculations ####################################################### ############################################################################## # table_calc table_calc=data.frame(raw, p, ppr, ppr.half, pp33, pp33.half, qnorm(ppr), qnorm(ppr.half), qnorm(pp33), qnorm(pp33.half)) headers1=c("Entered statistic","p-value", "ppr", "ppr half", "pp33%","pp33 half", "Z-R","Z-R half","Z-33","z-33 half") table_calc=setNames(table_calc,headers1) # table_figure headers2=c("p-value","Observed (blue)","Power 33% (Green)", "Flat (Red)") table_figure=setNames(data.frame(x,blue,green,red),headers2) ################################################ # 8. Cumulative p-curves (Deprecated) ########## ################################################ #7.1 FUNCTION THAT RECOMPUTES OVERALL STOUFFER TEST WITHOUT (K) MOST EXTREME VALUES, ADJUSTING THE UNIFORM TO ONLY INCLUDE RANGE THAT REMAINS dropk=function(pp,k,droplow) { #Syntax: #pp: set of pp-values to analyze sensitivity to most extremes #k: # of most extreme values to exclude #dropsmall: 1 to drop smallest, 0 to drop largest pp=pp[!is.na(pp)] #Drop missing values n=length(pp) #See how many studies are left pp=sort(pp) #Sort the pp-value from small to large if (k==0) ppk=pp #If k=0 do nothing for nothing is being dropped #If we are dropping low values if (droplow==1 & k>0) { #Eliminate lowest k from the vector of pp-values ppk=(pp[(1+k):n]) ppmin=min(pp[k],k/(n+1)) #Boundary used to define possible range of values after exclusion ppk=(ppk-ppmin)/(1-ppmin) #Take the k+1 smallest pp-value up to the highest, subtract from each the boundary value, divide by the range, ~U(0,1) under the null #This is explained in Supplement 1 of Simonsohn, Simmons Nelson, JEPG 2016 "Better p-curves" paper. See https://osf.io/mbw5g/ } #If we are dropping high values if (droplow==0 & k>0) { #Eliminate lowest k from the vector of pp-values ppk=pp[1:(n-k)] ppmax=max(pp[n-k+1],(n-k)/(n+1)) #Find new boundary of range ppk=ppk/ppmax #Redefine range to make U(0,1) } #In case of a tie with two identical values we would have the ppk be 0 or 1, let's replace that with almost 0 and almost 1 ppk=pmax(ppk,.00001) #Adds small constant to the smallest redefined p-value, avoids problem if dropped p-value is "equal" to next highest, then that pp-value becomes 0 ppk=pmin(ppk,.99999) #Subtract small constant to the largest redefined pp-value, same reason Z=sum(qnorm(ppk))/sqrt(n-k) return(pnorm(Z)) } #End function dropk #7.2 Apply function, in loop with increasing number of exclusions, to full p-curve #Empty vectors for results droplow.r=droplow.33=drophigh.r=drophigh.33=c() #Loop over full p-curves for (i in 0:(round(ksig/2)-1)) { #Drop the lowest k studies in terms of respective overall test #Right skew droplow.r= c(droplow.r, dropk(pp=ppr,k=i,droplow=1)) drophigh.r=c(drophigh.r, dropk(pp=ppr,k=i,droplow=0)) #Power of 33% droplow.33=c(droplow.33, dropk(pp=pp33,k=i,droplow=1)) drophigh.33=c(drophigh.33, dropk(pp=pp33,k=i,droplow=0)) } #Half p-curves if (khalf>0) { droplow.halfr=drophigh.halfr=c() for (i in 0:(round(khalf/2)-1)) { #Drop the lowest k studies in terms of respective overall test droplow.halfr= c(droplow.halfr, dropk(pp=ppr.half,k=i,droplow=1)) drophigh.halfr=c(drophigh.halfr, dropk(pp=ppr.half,k=i,droplow=0)) } #End loop }#End if that runs calculations only if khalf>0 #7.3 FUNCTION THAT DOES THE PLOT OF RESULTS plotdrop=function(var,col) { k=length(var) #Plot the dots plot(0:(k-1),var,xlab="",ylab="",type="b",yaxt="n",xaxt="n",main="", cex.main=1.15,ylim=c(0,1),col=col) #Add marker in results with 0 drops points(0,var[1],pch=19,cex=1.6) #Red line at p=.05 abline(h=.05,col="red") #Y-axis value labels axis(2,c(.05,2:9/10),labels=c('.05','.2','.3','.4','.5','6','7','.8','.9'),las=1,cex.axis=1.5) axis(1,c(0:(k-1)),las=1,cex.axis=1.4) } ###################################################################################### # 9. Effect Estimation ############################################################### ###################################################################################### if (effect.estimation == TRUE){ # Define ci.to.t function ci.to.t = function(TE, lower, upper, n){ z.to.d = function(z, n){ d = (2*z)/sqrt(n) return(abs(d)) } ci.to.p = function(est, lower, upper){ SE = (upper-lower)/(2*1.96) z = abs(est/SE) p = exp(-0.717*z - 0.416*z^2) return(p) } d.to.t = function(d, n){ df = n-2 t = (d*sqrt(df))/2 return(t) } p = ci.to.p(TE, lower, upper) z = abs(qnorm(p/2)) d = z.to.d(z, n) t = d.to.t(d, n) return(t) } #Function 13 - loss function loss=function(t_obs,df_obs,d_est) { #1.Convert all ts to the same sign (for justification see Supplement 5) t_obs=abs(t_obs) #2 Compute p-values p_obs=2*(1-pt(t_obs,df=df_obs)) #3 Keep significant t-values and corresponding df. t.sig=subset(t_obs,p_obs<.05) df.sig=subset(df_obs,p_obs<.05) #4.Compute non-centrality parameter implied by d_est and df_obs #df+2 is total N. #Becuase the noncentrality parameter for the student distribution is ncp=sqrt(n/2)*d, #we add 2 to d.f. to get N, divide by 2 to get n, and by 2 again for ncp, so -->df+2/4 ncp_est=sqrt((df.sig+2)/4)*d_est #5.Find critical t-value for p=.05 (two-sided) #this is used below to compute power, it is a vector as different tests have different dfs #and hence different critical values tc=qt(.975,df.sig) #4.Find power for ncp given tc, again, this is a vector of implied power, for ncp_est, for each test power_est=1-pt(tc,df.sig,ncp_est) #5.Compute pp-values #5.1 First get the overall probability of a t>tobs, given ncp p_larger=pt(t.sig,df=df.sig,ncp=ncp_est) #5.2 Now, condition on p<.05 ppr=(p_larger-(1-power_est))/power_est #this is the pp-value for right-skew #6. Compute the gap between the distribution of observed pp-values and a uniform distribution 0,1 KSD=ks.test(ppr,punif)$statistic #this is the D statistic outputted by the KS test against uniform return(KSD) } if(missing(N)){ stop("If 'effect.estimation=TRUE', argument 'N' must be provided.") } if (length(N) != length(metaobject$TE)){ stop("N must be of same length as the number of studies contained in x.") } lower = metaobject$TE - (metaobject$seTE*1.96) upper = metaobject$TE + (metaobject$seTE*1.96) t_obs = ci.to.t(metaobject$TE, lower, upper, N) df_obs = N-2 #Results will be stored in these vectors, create them first loss.all=c() di=c() #Compute loss for effect sizes between d=c(dmin,dmax) in steps of .01 for (i in 0:((dmax-dmin)*100)) { d=dmin+i/100 #effect size being considered di=c(di,d) #add it to the vector (kind of silly, but kept for symmetry) options(warn=-1) #turn off warning becuase R does not like its own pt() function! loss.all=c(loss.all,loss(df_obs=df_obs,t_obs=t_obs,d_est=d)) #apply loss function so that effect size, store result options(warn=0) #turn warnings back on } #find the effect leading to smallest loss in that set, that becomes the starting point in the optimize command imin=match(min(loss.all),loss.all) #which i tested effect size lead to the overall minimum? dstart=dmin+imin/100 #convert that i into a d. #optimize around the global minimum dhat=optimize(loss,c(dstart-.1,dstart+.1), df_obs=df_obs,t_obs=t_obs) options(warn=-0) #Plot results plot(di,loss.all,xlab="Effect size\nCohen-d", ylab="Loss (D stat in KS test)",ylim=c(0,1), main="How well does each effect size fit? (lower is better)") points(dhat$minimum,dhat$objective,pch=19,col="red",cex=2) text(dhat$minimum,dhat$objective-.08,paste0("p-curve's estimate of effect size:\nd=",round(dhat$minimum,3)),col="red") } ###################################################################################### # 10. Prepare Results for Return ##################################################### ###################################################################################### # Get results main.results = round(main.results, 3) ktotal = round(main.results[1]) # Get the total number of inserted TEs k.sign = round(main.results[2]) # Get the total number of significant TEs k.025 = round(main.results[3]) # Get the number of p<0.25 TEs skew.full.z = main.results[4] # Get the Z-score for the full curve skewness test skew.full.p = main.results[5] # Get the p-value for the full curve skewness test flat.full.z = main.results[6] # Get the Z-score for the full curve flatness test flat.full.p = main.results[7] # Get the p-value for the full curve flatness test skew.half.z = main.results[8] # Get the Z-score for the half curve skewness test skew.half.p = main.results[9] # Get the p-value for the half curve skewness test flat.half.z = main.results[10] # Get the Z-score for the half curve flatness test flat.half.p = main.results[11] # Get the p-value for the half curve flatness test skew.binomial.p = round(binomial[3], 3) # Get the skewness binomial p-value flat.binomial.p = round(binomial[4], 3) # Get the flatness binomial p-value # Make data.frame skewness = c(skew.binomial.p, skew.full.z, skew.full.p, skew.half.z, skew.half.p) flatness = c(flat.binomial.p, flat.full.z, flat.full.p, flat.half.z, flat.half.p) colnames.df = c("pBinomial", "zFull", "pFull", "zHalf", "pHalf") rownames.df = c("Right-skewness test", "Flatness test") pcurveResults = rbind(skewness, flatness) colnames(pcurveResults) = colnames.df rownames(pcurveResults) = rownames.df # Power results power_results = round(power_results, 3) powerEstimate = power_results[2] powerLower = power_results[1] powerUpper = power_results[3] Power = as.data.frame(cbind(powerEstimate, powerLower, powerUpper)) rownames(Power) = "" # Presence and absence of evidential value # - If the half p-curve test is right-skewed with p<.05 or both the half and full test # are right-skewed with p<.1, then p-curve analysis indicates the presence of evidential value # - Evidential value is inadequate or absent if the 33% power test is p<.05 for the full p-curve # or both the half p-curve and binomial 33% power test are p<.1 if (skew.half.p < 0.05 | (skew.half.p < 0.1 & skew.full.p < 0.1)){ presence.ev = "yes" } else { presence.ev = "no" } if (flat.full.p < 0.05 | (flat.half.p < 0.1 & flat.binomial.p < 0.1)){ absence.ev = "yes" } else { absence.ev = "no" } # Plot Data PlotData = round(table_figure, 3) # Input Data table_calc[,1] = NULL colnames(table_calc) = c("p", "ppSkewFull", "ppSkewHalf", "ppFlatFull", "ppFlatHalf", "zSkewFull", "zSkewHalf", "zFlatFull", "zFlatHalf") Input = cbind(metaobject$TE, round(table_calc,3)) rownames(Input) = paste(1:length(metaobject$TE), metaobject$studlab) colnames(Input)[1] = "TE" if (effect.estimation==TRUE){ dEstimate = round(dhat$minimum, 3) return.list = list("pcurveResults" = pcurveResults, "Power" = Power, "PlotData" = PlotData, "Input" = Input, "EvidencePresent" = presence.ev, "EvidenceAbsent" = absence.ev, "kInput" = ktot, "kAnalyzed" = k.sign, "kp0.25" = k.025, "dEstimate" = dEstimate, "I2" = metaobject$I2, "class.meta.object" = class(metaobject)[1]) class(return.list) = c("pcurve", "effect.estimation") } else { return.list = list("pcurveResults" = pcurveResults, "Power" = Power, "PlotData" = PlotData, "Input" = Input, "EvidencePresent" = presence.ev, "EvidenceAbsent" = absence.ev, "kInput" = ktot, "kAnalyzed" = k.sign, "kp0.25" = k.025, "I2" = metaobject$I2, "class.meta.object" = class(metaobject)[1]) class(return.list) = c("pcurve", "no.effect.estimation") } cat(" ", "\n") invisible(return.list) return.list }