Last updated: 2026-02-10

Checks: 7 0

Knit directory: exp_evol_metaanalysis/

This reproducible R Markdown analysis was created with workflowr (version 1.7.2). The Checks tab describes the reproducibility checks that were applied when the results were created. The Past versions tab lists the development history.


Great! Since the R Markdown file has been committed to the Git repository, you know the exact version of the code that produced these results.

Great job! The global environment was empty. Objects defined in the global environment can affect the analysis in your R Markdown file in unknown ways. For reproduciblity it’s best to always run the code in an empty environment.

The command set.seed(20260206) was run prior to running the code in the R Markdown file. Setting a seed ensures that any results that rely on randomness, e.g. subsampling or permutations, are reproducible.

Great job! Recording the operating system, R version, and package versions is critical for reproducibility.

Nice! There were no cached chunks for this analysis, so you can be confident that you successfully produced the results during this run.

Great job! Using relative paths to the files within your workflowr project makes it easier to run your code on other machines.

Great! You are using Git for version control. Tracking code development and connecting the code version to the results is critical for reproducibility.

The results in this page were generated with repository version 788847e. See the Past versions tab to see a history of the changes made to the R Markdown and HTML files.

Note that you need to be careful to ensure that all relevant files for the analysis have been committed to Git prior to generating the results (you can use wflow_publish or wflow_git_commit). workflowr only checks the R Markdown file, but you know if there are other scripts or data files that it depends on. Below is the status of the Git repository when the results were generated:


Ignored files:
    Ignored:    .DS_Store
    Ignored:    .Rproj.user/
    Ignored:    input_data/.DS_Store
    Ignored:    input_data/lundhansen_github_data_FLXevoexp-master/.DS_Store
    Ignored:    input_data/lundhansen_github_data_FLXevoexp-master/DevelopmentTimeGeneration/.Rapp.history
    Ignored:    input_data/lundhansen_github_data_FLXevoexp-master/LocomotionAssayGeneration123/.DS_Store
    Ignored:    input_data/lundhansen_github_data_FLXevoexp-master/LocomotionAssayGeneration123/.Rapp.history
    Ignored:    input_data/lundhansen_github_data_FLXevoexp-master/LocomotionAssayGeneration123/.Rhistory
    Ignored:    input_data/lundhansen_github_data_FLXevoexp-master/ReproductiveFitnessFM/.Rapp.history
    Ignored:    input_data/lundhansen_github_data_FLXevoexp-master/ReproductiveFitnessFM/.Rhistory
    Ignored:    input_data/lundhansen_github_data_FLXevoexp-master/StandardFitnessGeneration15-18/.Rapp.history
    Ignored:    input_data/lundhansen_github_data_FLXevoexp-master/StandardFitnessGeneration39-41 /.Rapp.history
    Ignored:    input_data/lundhansen_github_data_FLXevoexp-master/ThoraxSizeGeneration15-18/.Rapp.history
    Ignored:    input_data/lundhansen_github_data_FLXevoexp-master/ThoraxSizeGeneration72/.Rapp.history

Unstaged changes:
    Deleted:    data/README.md
    Deleted:    output/README.md

Note that any generated files, e.g. HTML, png, CSS, etc., are not included in this status report because it is ok for generated content to have uncommitted changes.


These are the previous versions of the repository in which changes were made to the R Markdown (analysis/meta_analysis.Rmd) and HTML (docs/meta_analysis.html) files. If you’ve configured a remote Git repository (see ?wflow_git_remote), click on the hyperlinks in the table below to view the files as they were in that past version.

File Version Author Date Message
Rmd 788847e luekholman 2026-02-10 Fixed a few bits
html f546a4c luekholman 2026-02-10 Build site.
Rmd 2e563e5 luekholman 2026-02-10 Fixed a few bits
html 68e88ca luekholman 2026-02-09 Build site.
Rmd 67d1c7a luekholman 2026-02-09 pm typo
html 67d1c7a luekholman 2026-02-09 pm typo
html 77d88f0 luekholman 2026-02-09 Build site.
Rmd d64f5ed luekholman 2026-02-09 figure caption
html 0b01f66 luekholman 2026-02-09 Build site.
html 27f0459 luekholman 2026-02-09 Build site.
html b0251e4 luekholman 2026-02-09 Many minor edits
html f4fb206 luekholman 2026-02-09 First commit
html f1a4d10 luekholman 2026-02-09 First commit
html 6029ddf luekholman 2026-02-09 Build site.
Rmd 7057ba1 luekholman 2026-02-09 First commit
html 7057ba1 luekholman 2026-02-09 First commit

Load packages, the meta-analysis data, and a helper function.

library(tidyverse)
library(metafor)
library(DT)
library(ggh4x)
library(kableExtra)


# function to make the searchable, exportable HTML table
my_data_table <- function(df){

  num_cols <- names(df)[sapply(df, is.numeric)]
  num_cols <- num_cols[num_cols != "Generations"]

  datatable(
    df, rownames = FALSE,
    autoHideNavigation = TRUE,
    extensions = c("Scroller", "Buttons"),
    options = list(
      autoWidth = TRUE,
      dom = 'Bfrtip',
      deferRender = TRUE,
      scrollX = TRUE,
      scrollY = 1000,
      scrollCollapse = TRUE,
      buttons = list(
        'pageLength', 'colvis', 'csv',
        list(
          extend = 'pdf',
          pageSize = 'A4',
          orientation = 'landscape',
          filename = 'Trait_data'
        )
      ),
      pageLength = 100
    )
  ) %>%
    formatRound(
      columns = num_cols,
      digits = 3
    )
}

# Load up the meta-analysis data:
meta_analysis_data <- list.files("effect_sizes/", full.names = T) %>% 
  lapply(read_csv, show_col_types = FALSE) %>% 
  bind_rows() %>% 
  left_join(read_csv("input_data/study_metadata.csv"), by = join_by(Study)) %>% 
  mutate(year = str_extract(Short_name, "[:digit:]+")) %>% 
  arrange(year) %>% 
  mutate(Sex = "Both",
         Sex = replace(Sex, str_detect(Trait, "Male"), "Male"),
         Sex = replace(Sex, str_detect(Trait, "Female"), "Female"))

Data collected for the meta-analysis

This table is searchable and can be exported as a file. You can also hide columns to help read it. There are 56 effect sizes from 11 independent experimental evolution projects, which were published across 16 journal articles.

meta_analysis_data %>% 
  select(-year, -Short_name) %>% 
  my_data_table()

Meta-analysis without moderators

This meta-analysis can be used to calculate the grand mean effect size. It’s a mixed effects meta-analysis with the random effects structure ~ 1 | Origin_of_lines/Study. This means we fit a random intercept for Origin_of_lines, a variable which gives the name of the study that first created the experimental evolution lines being measured. We also fit a random intercept for Study nested within Origin_of_lines, where Study is a variable giving the name of the study doing the measuring (which is often the same study, but not always, since some studies re-measured lines that were created in an earlier study).

meta_analysis <- rma.mv(
  yi = Estimate,
  V = Var,
  random = ~ 1 | Origin_of_lines/Study,
  data = meta_analysis_data,
  method = "REML"
)

summary(meta_analysis)

Multivariate Meta-Analysis Model (k = 56; method: REML)

   logLik   Deviance        AIC        BIC       AICc   
-479.4613   958.9226   964.9226   970.9446   965.3931   

Variance Components:

            estim    sqrt  nlvls  fixed                 factor 
sigma^2.1  0.0192  0.1387     11     no        Origin_of_lines 
sigma^2.2  0.1198  0.3462     16     no  Origin_of_lines/Study 

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

Model Results:

estimate      se    zval    pval   ci.lb   ci.ub    
  0.2588  0.1009  2.5653  0.0103  0.0611  0.4565  * 

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

The above results table shows that the grand mean effect size is 0.259, with a standard error of 0.101 and 95% CIs of 0.061 to 0.457.

Testing for effects of moderators

Here, I use AICc model selection to compare 7 competing meta-analysis models, which fit Generations, Sex, Experiment_type, a combination of 2 or all of these, or the intercept-only null model. Generations gives the number of generations of selection at the time the focal trait was measured, Sex gives the sex of the individuals expressing the trait (male, female, or both sexes), and Experiment_type is either “Sex-specific inheritance” or “Sex-specific relaxation of selection”.

The top model includes the moderator Generations, and it is ahead of second-best model (which also includes Sex) by much more than 2 AICc, suggesting that Generations explains substantial variation in the data but the other moderators do not.

fit_ma <- function(formula){
  
  scaled_meta_analysis_data <-
    meta_analysis_data %>% 
    mutate(Generations = as.numeric(scale(Generations)))
  
  if(formula == "Intercept only"){
    return(meta_analysis)
  }
  
  rma.mv(
    yi = Estimate,
    V = Var,
    mods = as.formula(formula),
    random = ~ 1 | Origin_of_lines/Short_name,
    data = scaled_meta_analysis_data,
    method = "REML"
  )
}

fit_all <- function(formulae){
  meta <- lapply(formulae, fit_ma)
  get_p <- function(ma) as.numeric(ma$p)
  get_logLik <- function(ma) as.numeric(logLik(ma))
  get_AICc <- function(ma) as.numeric(AIC(ma, correct = T))
  
  data.frame(
    Model = formulae,
    k     = sapply(meta, get_p),
    logLik = sapply(meta, get_logLik),
    AICc   = sapply(meta, get_AICc)) %>% 
    arrange(AICc) %>% 
    mutate(Model = str_remove_all(Model, "~ "),
           delta_AICc = round(AICc - AICc[1], 2),
           delta_AICc = c(" ", delta_AICc[2:length(delta_AICc)]))
}

fit_all(c("~ Generations + Sex + Experiment_type1",
          "~ Generations + Sex",
          "~ Sex + Experiment_type1",
          "~ Generations",
          "~ Sex",
          "~ Experiment_type1",
          "Intercept only")) %>% 
  kable() %>% 
  kable_styling(full_width = FALSE)
Model k logLik AICc delta_AICc
Generations 2 -473.1500 955.1162
Generations + Sex 4 -474.7394 963.3454 8.23
Intercept only 1 -479.4613 965.3931 10.28
Generations + Sex + Experiment_type1 5 -474.4188 965.4423 10.33
Experiment_type1 2 -478.7294 966.2752 11.16
Sex 3 -481.8756 975.0279 19.91
Sex + Experiment_type1 4 -481.1773 976.2212 21.1

Below are the results of the top model containing moderators, showing the significant negative effect of the moderator Generations on effect size (note: Generations was scaled to have mean 0, SD 1, prior to analysis). This is a counter-intuitive result since we might expect greater effects after more generations of selection, but it is not very informative since Generations is confounded with other factors (e.g. species, type of experimental design, type of trait being measured, and whether I used the raw data or summary statistics to calculate effect size). The grand mean effect size (predicted here for the average value of Generations, which is 54.1) is much the same after adjusting for the effect of Generations.

fit_ma("~ Generations")

Multivariate Meta-Analysis Model (k = 56; method: REML)

Variance Components:

            estim    sqrt  nlvls  fixed                      factor 
sigma^2.1  0.0000  0.0001     11     no             Origin_of_lines 
sigma^2.2  0.1183  0.3440     16     no  Origin_of_lines/Short_name 

Test for Residual Heterogeneity:
QE(df = 54) = 1251.2231, p-val < .0001

Test of Moderators (coefficient 2):
QM(df = 1) = 14.0152, p-val = 0.0002

Model Results:

             estimate      se     zval    pval    ci.lb    ci.ub      
intrcpt        0.2512  0.0900   2.7925  0.0052   0.0749   0.4275   ** 
Generations   -0.1151  0.0307  -3.7437  0.0002  -0.1754  -0.0548  *** 

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

Forest plot of effect sizes (Figure 1)

plot_data <- meta_analysis_data %>% 
  select(Origin_of_lines, Short_name, 
         Experiment_type1, Experiment_type2, Trait, Estimate, 
         Lower_95_CI, 
         Upper_95_CI) 

plot_data$Trait[plot_data$Trait == "Male fitness" & plot_data$Short_name == "Abbott et al. (2013)"] <- "Male fitness (2013)"
plot_data$Trait[plot_data$Trait == "Male fitness" & plot_data$Short_name == "Abbott et al. (2020)"] <- "Male fitness (2020)"

plot_data <- plot_data %>% 
  group_by(Origin_of_lines) %>% 
  mutate(Short_name = paste0(unique(Short_name), collapse = ";\n")) %>% 
  bind_rows(tibble(
    Origin_of_lines = " ", Short_name = "Mean effect size", 
         Experiment_type1 = "  ", Trait = " ", 
    Estimate = as.numeric(meta_analysis$b), 
         Lower_95_CI = as.numeric(meta_analysis$ci.lb), 
    Upper_95_CI  = as.numeric(meta_analysis$ci.ub)
  )) 


levs <- plot_data$Short_name %>% unique()

plot_data <- plot_data %>% 
  mutate(Short_name = factor(Short_name, levs)) 

bands <- plot_data %>% 
  mutate(y = 1:n()) %>% 
  group_by(Short_name) %>% 
  summarise(
    ymin = min(y) -0.5,
    ymax = max(y) + 0.5,
    .groups = "drop")  %>% 
  dplyr::mutate(
    stripe = row_number() %% 2
  )

# order the traits within studies by effect size
plot_data <- plot_data %>% 
  mutate(Short_name = factor(Short_name, levs)) %>% 
  arrange(Estimate) %>% 
  mutate(Trait_study = factor(paste(Trait, Short_name, sep = "~"), 
                              unique(paste(Trait, Short_name, sep = "~"))))

plot_data %>% 
  ggplot(aes(y = Trait_study, x = Estimate, colour = Experiment_type1)) +
  geom_vline(xintercept = 0, linetype = 2, colour = "grey10") +
  geom_errorbar(aes(xmin = Lower_95_CI, 
                    xmax = Upper_95_CI)) +
  geom_point() + 
  scale_y_discrete(labels = function(x) sub("~.*$", "", x)) + 
  facet_grid2( rows = vars(Short_name),
    scales = "free_y",
    space = "free_y") +
  theme_minimal()+
  theme(legend.position = "top",
        panel.grid.major.y = element_blank(),
        strip.text.y = element_text(angle = 0),
        panel.spacing.y=unit(0, "lines"))+ 
  labs(x = "Effect size (Cohen's d) \u00B1 95% CIs", 
       y = NULL,
       colour = NULL) +  
  # scale_color_brewer(palette = "Set1") + 
  geom_rect(
    data = bands,
    aes(ymin = ymin, ymax = ymax, xmin = -Inf, xmax = Inf, fill = factor(stripe)),
    alpha = 0.1,
    inherit.aes = FALSE,
    fill = rep(c("purple", "orange"), 6)
  )

Version Author Date
f546a4c luekholman 2026-02-10
67d1c7a luekholman 2026-02-09
b0251e4 luekholman 2026-02-09
f4fb206 luekholman 2026-02-09
6029ddf luekholman 2026-02-09
7057ba1 luekholman 2026-02-09

Figure 1: Forest plot showing the estimated effect sizes (Cohen’s d \(\pm\) 95% CIs). The y-axis indicates which trait was measured, and the coloured bands group effect sizes that come from the same set of experimental evolution lines (which are covered in one or more publications, named on the right). Positive effect sizes indicate evolutionary change in the direction predicted under IaSC, while negative effects indicate evolution in the opposite direction. Point colour differentiates between studies that manipulated inheritance vs selection. The grand mean effect size, shown at the bottom, is significantly positive.

Plotting effect of moderator Generations

On the right of x-axis (high generation number), this result is driven by a couple of studies, Lund-Hansen et al. (2020) and Manat et al. (2021), which both relate to the same set of experimental evolution lines, and which both focus on female-limited evolution of X chromosomes (uniquely in the full dataset). I also calculated the effect sizes from the raw data for these studies, avoiding issues with falsely inflating them. to the left of the x-axis (short generation number), the trend is driven by a couple of older studies I calculated the effect sizes from summary statistics, possibly inflating them.

Therefore, I don’t feel there is good evidence for a true negative effect of Generations, and this result instead reflects confounding.

library(ggrepel)
meta_analysis_data %>% 
  ggplot(aes(Generations, Estimate)) + 
  geom_point() + 
  geom_text_repel(aes(label = Short_name), size = 2, max.overlaps = 1000)

Version Author Date
b0251e4 luekholman 2026-02-09
7057ba1 luekholman 2026-02-09

sessionInfo()
R version 4.5.2 (2025-10-31)
Platform: aarch64-apple-darwin20
Running under: macOS Tahoe 26.2

Matrix products: default
BLAS:   /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: Europe/London
tzcode source: internal

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

other attached packages:
 [1] ggrepel_0.9.6       kableExtra_1.4.0    ggh4x_0.3.1        
 [4] DT_0.34.0           metafor_4.8-0       numDeriv_2016.8-1.1
 [7] metadat_1.4-0       Matrix_1.7-4        lubridate_1.9.4    
[10] forcats_1.0.1       stringr_1.6.0       dplyr_1.2.0        
[13] purrr_1.2.1         readr_2.1.6         tidyr_1.3.2        
[16] tibble_3.3.1        ggplot2_4.0.2       tidyverse_2.0.0    
[19] workflowr_1.7.2    

loaded via a namespace (and not attached):
 [1] gtable_0.3.6       xfun_0.56          bslib_0.10.0       htmlwidgets_1.6.4 
 [5] processx_3.8.6     lattice_0.22-7     callr_3.7.6        mathjaxr_2.0-0    
 [9] tzdb_0.5.0         crosstalk_1.2.2    vctrs_0.7.1        tools_4.5.2       
[13] ps_1.9.1           generics_0.1.4     parallel_4.5.2     pkgconfig_2.0.3   
[17] RColorBrewer_1.1-3 S7_0.2.1           lifecycle_1.0.5    compiler_4.5.2    
[21] farver_2.1.2       git2r_0.36.2       textshaping_1.0.4  getPass_0.2-4     
[25] httpuv_1.6.16      htmltools_0.5.9    sass_0.4.10        yaml_2.3.12       
[29] crayon_1.5.3       later_1.4.5        pillar_1.11.1      jquerylib_0.1.4   
[33] whisker_0.4.1      cachem_1.1.0       nlme_3.1-168       tidyselect_1.2.1  
[37] digest_0.6.39      stringi_1.8.7      labeling_0.4.3     rprojroot_2.1.1   
[41] fastmap_1.2.0      grid_4.5.2         cli_3.6.5          magrittr_2.0.4    
[45] withr_3.0.2        scales_1.4.0       promises_1.5.0     bit64_4.6.0-1     
[49] timechange_0.4.0   rmarkdown_2.30     httr_1.4.7         bit_4.6.0         
[53] otel_0.2.0         hms_1.1.4          evaluate_1.0.5     knitr_1.51        
[57] viridisLite_0.4.2  rlang_1.1.7        Rcpp_1.1.1         glue_1.8.0        
[61] xml2_1.5.2         vroom_1.7.0        svglite_2.2.2      rstudioapi_0.18.0 
[65] jsonlite_2.0.0     R6_2.6.1           systemfonts_1.3.1  fs_1.6.6