wviechtb/metafor

Evaluate a function to transf and atransf that can alias with expected functions such that xlab is set to corresponding label.

ltrainstg opened this issue · 4 comments

Feature Request

Be able to pass a custom function to the transf and atransf variables and get the expected xlab.

Summary

I like to use a variable for the transf and atransf when automating results of forest plots but the way these are parsed in forest and .setlab only the explicit text arguments are recognized.

I did some digging and I think it is possible to resolve this with a few changes by taking advantage of this stack post.
https://stackoverflow.com/questions/32266433/in-r-how-do-i-test-that-two-functions-have-the-same-definition

Reproducible Example (if applicable)

The xlab is different for these two forest plots because the first will capture the character and the

get_atransf <- function(measure=""){
    if (is.null(measure)){
        measure <- ""
    }
    inv.function <- ""
    if(measure %in% c('MD', 'SMD', 'RD','MN', 'PR')) inv.function <- identity
    if(measure %in% c('OR', 'RR', 'PLN')) inv.function <- exp
    if(measure %in% c('PLO')) return(transf.ilogit)
    return(inv.function )
    
}

res <- rma(measure="PLO", xi=ci, ni=n2i, data=dat.nielweise2007)
forest(res, atransf = transf.ilogit)
forest(res, atransf =get_atransf('PLO'))

We can check that these two things should be the same, but the behavior differs because only the text argument of atransf is parsed and not what it evaluates.

identical(deparse(transf.ilogit), deparse(get_atransf('PLO')))

Suggested Edit 1

In the defaults plots we need to change how the argument is substituted then deparsed.
Because of substitute we just get the function name, but we want what the function evaluates since the test_atransf_as_a_function returns the 'default' back transform for the measure.

   #transf.char  <- deparse(substitute(transf))
   #atransf.char <- deparse(substitute(atransf))
   transf.char  <- deparse(transf)
   atransf.char <- deparse(atransf)

Suggested Edit 2

A corresponding edit in .setlabs is need.
Here we create a list of possible functions and deparse them.
So long as any are identical we can use the recommended label.
Here I just make a dummy .setlabs for a test case.

setlab_custom <- function(measure, transf.char, atransf.char, gentype, short=FALSE){
  
  if (measure == "PLO") {
    if (transf.char == "FALSE" && atransf.char == "FALSE") {
      lab <- ifelse(short, "Log[Odds]", "Log Odds")
    } else {
      lab <- ifelse(short, lab, "Transformed Log Odds")
      # Take all the possible expected matching functions and deparse them
      # This allows us to pass funtions as variables in metafor. 
      funct_list <- list(transf.ilogit.int, transf.ilogit, plogis)
      funct_list <- lapply(funct_list, deparse)
      if (any(sapply(funct_list, identical, atransf.char)))
        lab <- ifelse(short, "Proportion", "Proportion (logit scale)")
    }
  }
  return(lab)
  
}

# Current only way using explicit text argument  
# It works since this is the current design. 
atransf.char <- deparse(substitute(transf.ilogit))
metafor:::.setlab("PLO", "FALSE", atransf.char,gentype=1)

# using a function that takes a variable fails 
# it just takes the string of the exact argument

atransf.char <- deparse(substitute(get_atransf('PLO')))
metafor:::.setlab("PLO", "FALSE", atransf.char,gentype=1)

# New method saves what the argument evaluates to
# New evaluation checks all possible good evaluations. 
atransf.char <- deparse(get_atransf('PLO'))
setlab_custom("PLO", "FALSE", atransf.char,gentype=1)


sessionInfo()

R version 4.1.0 (2021-05-18)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Ubuntu 20.04.2 LTS

Matrix products: default
BLAS:   /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.9.0
LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.9.0

locale:
 [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C               LC_TIME=en_US.UTF-8       
 [4] LC_COLLATE=en_US.UTF-8     LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
 [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                  LC_ADDRESS=C              
[10] LC_TELEPHONE=C             LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] DREmetafor_1.0 metafor_2.4-0  Matrix_1.3-3  

loaded via a namespace (and not attached):
 [1] Rcpp_1.0.6         pillar_1.4.7       compiler_4.1.0     tools_4.1.0       
 [5] boot_1.3-28        digest_0.6.27      jsonlite_1.7.2     evaluate_0.14     
 [9] lifecycle_0.2.0    tibble_3.0.4       nlme_3.1-152       gtable_0.3.0      
[13] lattice_0.20-44    pkgconfig_2.0.3    rlang_0.4.10       personograph_0.1.3
[17] yaml_2.2.1         xfun_0.24          dplyr_1.0.2        stringr_1.4.0     
[21] knitr_1.33         generics_0.1.0     vctrs_0.3.8        grid_4.1.0        
[25] tidyselect_1.1.0   grImport_0.9-3     glue_1.4.2         R6_2.5.0          
[29] XML_3.99-0.5       rmarkdown_2.6      pander_0.6.4       ggplot2_3.3.3     
[33] purrr_0.3.4        magrittr_2.0.1     htmltools_0.5.0    DREutils_1.0      
[37] scales_1.1.1       ellipsis_0.3.2     colorspace_2.0-0   stringi_1.6.2     
[41] munsell_0.5.0      crayon_1.4.1       Cairo_1.5-12.2

Apologies for the slow response, but this came in just as I went on my summer break and afterwards I had some catching up to do on some other tasks. This is an interesting suggestion and I will see if I can fully implement this.

I have implemented this now in 36a3b13. Does this work for you now?

Yes. This works for us. I tried the most common cases we handle.
We've been using a switch statement to handle this, but it's much nicer when it's handled by the package.
Thanks for taking the time to look into this.

Test Cases

atransf Function

get_atransf <- function(measure=""){
if (is.null(measure)){
measure <- ""
}
inv.function <- FALSE
if(measure %in% c('MD', 'SMD', 'RD','MN', 'PR')) inv.function <- FALSE
if(measure %in% c('OR', 'RR', 'PLN')) inv.function <- exp
if(measure %in% c('PLO')) return(transf.ilogit)
return(inv.function )

}

Single Proportion

measure <- "PR"
res <- rma(measure=measure, xi=ci, ni=n2i, data=dat.nielweise2007)
forest(res, atransf =get_atransf(measure))
forest(res)
measure <- "PLO"
res <- rma(measure=measure, xi=ci, ni=n2i, data=dat.nielweise2007)
forest(res, atransf =get_atransf(measure))
forest(res )
measure <- "PLN"
res <- rma(measure=measure, xi=ci, ni=n2i, data=dat.nielweise2007)
forest(res, atransf =get_atransf(measure))
forest(res )

Two Proportions

measure <- "OR"
dat <- escalc(measure=measure, ai=tpos, bi=tneg, ci=cpos, di=cneg, data=dat.bcg)
res <- rma(yi, vi,data=dat)
forest(res, atransf =get_atransf(measure))
forest(res )
measure <- "RR"
dat <- escalc(measure=measure, ai=tpos, bi=tneg, ci=cpos, di=cneg, data=dat.bcg)
res <- rma(yi, vi,data=dat)
forest(res, atransf =get_atransf(measure))
forest(res )

measure <- "RD"
dat <- escalc(measure=measure, ai=tpos, bi=tneg, ci=cpos, di=cneg, data=dat.bcg)
res <- rma(yi, vi,data=dat)
forest(res, atransf =get_atransf(measure))
forest(res )

Two Means

measure <- 'MD'
dat1 <- escalc(measure=measure, m1i=m1i, sd1i=sd1i, n1i=n1i,
m2i=m2i, sd2i=sd2i, n2i=n2i, data=dat.normand1999)
res <- rma(yi, vi, data=dat1)
forest(res, atransf =get_atransf(measure))
forest(res)

measure <- 'SMD'
dat1 <- escalc(measure=measure, m1i=m1i, sd1i=sd1i, n1i=n1i,
m2i=m2i, sd2i=sd2i, n2i=n2i, data=dat.normand1999)
res <- rma(yi, vi, data=dat1)
forest(res, atransf =get_atransf(measure))
forest(res)

Happy to hear that and thanks for the suggestion!