Chapter 3 Method 1: Pooled One Stage Models without Study-Specific Effects

Method 1 is a fully pooled procedure in which a single estimate of a prospective Big Five personality characteristic-crystallized ability association is estimated across samples. In other words, there are no sample-specific estimates of the associations.

3.1 Step 1: Combine Data

loadRData <- function(fileName, type){
#loads an RData file, and returns it
    path <- sprintf("%s/data/clean/%s_cleaned.RData", local_path, fileName)
    load(path)
    get(ls()[grepl(type, ls())])
}

ipd_reg_data <- tibble(
  study = studies[!studies %in% c("CNLSY", "SLS")]
  , data = map(str_to_lower(study), ~loadRData(., "combined"))
  ) %>% mutate(
    data = map(data, ~(.) %>% 
                 ungroup() %>% 
                 mutate(SID = as.character(SID)))
    , study = mapvalues(study, studies, studies_long)
  ) %>%
  unnest(data) %>%
  mutate(age = ifelse(is.na(age), p_year - yearBrth, age))

ipd_reg_data
## # A tibble: 119,597 × 13
##    study Trait p_value p_year SID   Outcome      o_value o_year education gender SRhealth yearBrth   age
##    <chr> <chr>   <dbl>  <dbl> <chr> <chr>          <dbl>  <dbl>     <dbl>  <dbl>    <dbl>    <dbl> <dbl>
##  1 BASE  E        3.83   1990 10004 crystallized   2.17    2000        17      0        4     1918    72
##  2 BASE  E        3.83   1990 10010 crystallized   3.08    1995        11      0        2     1911    79
##  3 BASE  E        2.5    1990 10033 crystallized   0.909   1997         8      1        5     1910    80
##  4 BASE  E        3      1990 10034 crystallized   6.54    1995        10      0        5     1914    76
##  5 BASE  E        2.5    1990 10111 crystallized   6.54    1995        13      1        4     1901    89
##  6 BASE  E        3.17   1990 10115 crystallized  10       1995        11      0        2     1918    72
##  7 BASE  E        3      1990 10116 crystallized   3.85    1995        10      0        4     1917    73
##  8 BASE  E        3      1990 10145 crystallized   6.36    1997        10      1        5     1897    93
##  9 BASE  E        3.33   1990 10175 crystallized   2.31    1995         8      1        3     1911    79
## 10 BASE  E        3.33   1990 10188 crystallized   5.77    2004        10      1        2     1903    87
## # ℹ 119,587 more rows

3.1.1 Study-Level Moderators

First, we need to bring in a few study-level moderators from Table 4, which includes information about the continent and country of origin, and the type of personality scale. Then we’ll join those data with cleaned data from each study in order to calculate the average age at baseline, the average baseline year, and the average interval between personality and cogntiive measurements. Because these vary within studies across personality traits and outcomes, we calculate baseline age, year, and prediction interval for all combinations of these separately.

ipd_reg_data <- sprintf("%s/codebooks/crystallized_tables.xlsx", local_path) %>% 
  read_xlsx(., sheet = "Table 4") %>%
  select(-Category, -Construct, -category) %>%
  pivot_longer(cols = c("BASE-I":"SATSA")
               , names_to = "study"
               , values_to = "value") %>%
  pivot_wider(names_from = "name"
              , values_from = "value") %>%
  mutate(continent = relevel(factor(continent), ref = "North America")
         , country = relevel(factor(country), ref = "United States")
         , scale = relevel(factor(scale), ref = "NEO-FFI")) %>%
  # mutate(p_year = as.numeric(p_year)) %>%
  right_join(ipd_reg_data) %>%
  group_by(study, Trait, Outcome) %>%
  mutate(baseAge = mean(age, na.rm = T) - 60, # center at age 60
         predInt = mean(o_year - p_year) - 5, # center at 5 years
         baseYear = ifelse(study %in% c("MARS", "MAP", "ROS"), mean(p_year), mean(p_year) - 2000)) %>% # center at 2000
  ungroup()

ipd_reg_data
## # A tibble: 119,597 × 19
##    study continent  country scale baseAge baseYear predInt Trait p_value p_year SID   Outcome o_value o_year education gender
##    <chr> <fct>      <fct>   <fct>   <dbl>    <dbl>   <dbl> <chr>   <dbl>  <dbl> <chr> <chr>     <dbl>  <dbl>     <dbl>  <dbl>
##  1 EAS   North Ame… United… IPIP…    19.5     11.0   -2.85 A         3.7   2006 8024  crysta…    6.68   2006        10      0
##  2 EAS   North Ame… United… IPIP…    19.5     11.0   -2.85 A         3.6   2006 8265  crysta…    6.36   2011        12      0
##  3 EAS   North Ame… United… IPIP…    19.5     11.0   -2.85 A         5     2006 8296  crysta…    5.23   2006        14      0
##  4 EAS   North Ame… United… IPIP…    19.5     11.0   -2.85 A         4.4   2006 8375  crysta…    4.77   2010        12      1
##  5 EAS   North Ame… United… IPIP…    19.5     11.0   -2.85 A         3.2   2006 8597  crysta…    7.73   2009        14      0
##  6 EAS   North Ame… United… IPIP…    19.5     11.0   -2.85 A         4     2006 8622  crysta…    9.5    2011        19      1
##  7 EAS   North Ame… United… IPIP…    19.5     11.0   -2.85 A         4.2   2006 8634  crysta…    4.09   2007        12      0
##  8 EAS   North Ame… United… IPIP…    19.5     11.0   -2.85 A         4.2   2006 8637  crysta…    1.41   2006         8      1
##  9 EAS   North Ame… United… IPIP…    19.5     11.0   -2.85 A         4.5   2006 8674  crysta…    2.32   2006        14      1
## 10 EAS   North Ame… United… IPIP…    19.5     11.0   -2.85 A         4.1   2006 8675  crysta…    6.09   2006        15      0
## # ℹ 119,587 more rows
## # ℹ 3 more variables: SRhealth <dbl>, yearBrth <dbl>, age <dbl>

3.1.2 Harmonize Data

Next, we need to harmonize the data across studies. As we preregistered, continuous variables (personality, cognition, and self-rated health) will be calclulated as percentages of maximum possible separately for each study, Trait and outcome. Unlike standardization procedures, that have a mean of zero and unit variance and can be misleading when data are skewed, POMP does not rescale sample variance based on the observed data, which overly relies on deviations from the mean. Instead, POMP relies on the ratio between the difference between a score and the minimum and the maximum and minimum, or

POMP = \(\frac{observed-minimum}{maximum-minimum}\)*10.

In addition, gender will be dummy coded with male as the reference group, education will be centered at 12 years of education, and age will be grand mean-centered in each study (also for each trait and outcome combination).

ipd_reg_data <- ipd_reg_data %>%
  group_by(study, Trait, Outcome) %>%
  mutate_at(vars(p_value, o_value), 
        ~((. - min(., na.rm = T))/(max(., na.rm = T) - min(., na.rm = T))*10)) %>%
  mutate(gender = factor(gender, levels = c(0,1), labels = c("Male", "Female")),
         education = education - 12, # center at 12 years of education
         age = age - mean(age, na.rm = T)) %>% # center 
  ungroup()  %>%
  select(-yearBrth)

ipd_reg_data
## # A tibble: 119,597 × 18
##    study continent  country scale baseAge baseYear predInt Trait p_value p_year SID   Outcome o_value o_year education gender
##    <chr> <fct>      <fct>   <fct>   <dbl>    <dbl>   <dbl> <chr>   <dbl>  <dbl> <chr> <chr>     <dbl>  <dbl>     <dbl> <fct> 
##  1 EAS   North Ame… United… IPIP…    19.5     11.0   -2.85 A        5.81   2006 8024  crysta…    6.68   2006        -2 Male  
##  2 EAS   North Ame… United… IPIP…    19.5     11.0   -2.85 A        5.48   2006 8265  crysta…    6.36   2011         0 Male  
##  3 EAS   North Ame… United… IPIP…    19.5     11.0   -2.85 A       10      2006 8296  crysta…    5.23   2006         2 Male  
##  4 EAS   North Ame… United… IPIP…    19.5     11.0   -2.85 A        8.06   2006 8375  crysta…    4.77   2010         0 Female
##  5 EAS   North Ame… United… IPIP…    19.5     11.0   -2.85 A        4.19   2006 8597  crysta…    7.73   2009         2 Male  
##  6 EAS   North Ame… United… IPIP…    19.5     11.0   -2.85 A        6.77   2006 8622  crysta…    9.5    2011         7 Female
##  7 EAS   North Ame… United… IPIP…    19.5     11.0   -2.85 A        7.42   2006 8634  crysta…    4.09   2007         0 Male  
##  8 EAS   North Ame… United… IPIP…    19.5     11.0   -2.85 A        7.42   2006 8637  crysta…    1.41   2006        -4 Female
##  9 EAS   North Ame… United… IPIP…    19.5     11.0   -2.85 A        8.39   2006 8674  crysta…    2.32   2006         2 Female
## 10 EAS   North Ame… United… IPIP…    19.5     11.0   -2.85 A        7.10   2006 8675  crysta…    6.09   2006         3 Male  
## # ℹ 119,587 more rows
## # ℹ 2 more variables: SRhealth <dbl>, age <dbl>

3.1.3 Save Data Files

Now, we’ll save these data files into separate data files for each trait, outcome combination. This makes it easier to track each data set that will be used in subsequent analyses.

save_fun <- function(d, trait, outcome){
  save(d, file = sprintf("%s/data/one_stage/%s_%s.RData", local_path, trait, outcome))
}

ipd_reg_data %>%
  group_by(Trait, Outcome) %>%
  nest() %>%
  ungroup() %>%
  mutate(data = pmap(list(data, Trait, Outcome), save_fun))
## # A tibble: 5 × 3
##   Trait Outcome      data  
##   <chr> <chr>        <list>
## 1 A     crystallized <NULL>
## 2 C     crystallized <NULL>
## 3 E     crystallized <NULL>
## 4 N     crystallized <NULL>
## 5 O     crystallized <NULL>

3.2 Step 2: Run Models and Extract Results

Once the data are prepped, we are ready to begin running models! For Method 1: Pooled One Stage Models without Study-Specific Effects, we will run two variants. The first is a simple linear regression that does not account for nesting / clustering in the data (specifically individuals within studies). The second is a simple linear regression with cluster-robust standard errors. The latter is of particular interest and benefit when study-specific effects are not of interest and one wants the best estimate of an aggregate, fixed effect.

3.2.1 Method 1A: Linear Regression

3.2.1.1 Analytic Plan

In the present study, we aimed to examine associations between personality traits and crystallized cognitive abilities using fully pooled, single-stage regression models with individual participant data. Below, we detail each stage of the analysis.

3.2.1.1.1 Statistical Modeling

A single regression model tests the relationship between personality and crystallized cognitive ability across all samples. Using the R programming language, we created a series of functions to (1) set up and run the model and extract model coefficients and (2) extract simple-effects predictions for moderator models (i.e. predicted values across levels of the moderator values). The basic, unadjusted form of the model is as follows:

\(Y_{ij} = b_0 + b_1 \ast predictor_{ij} + \epsilon_{ij}\) \(\varepsilon_{ij}\sim\mathcal{N}(0, \sigma^2)\)

where \(b_1\) represents the overall effect of personality predicting the outcome. For moderator models, we add two additional terms, \(b_2\), which captures the prospective association between a moderator and cognitive ability, adjusting for personality, and b_3, which captures how cognitive ability varies as a function of both personality and the moderator (e.g., does the association differ for males and females).

Models were tested using the lm() function from base R and the tidy() function from the broom package (version 1.0.1; D. Robinson et al., 2022) to extract model coefficients and confidence intervals (CI). Inferences were made based on the 95% confidence intervals. Simple-effects predictions were calculated by providing the full range of personality (0-10) and average levels of the covariates from the data used to estimate the model as the “newdata” argument in the base R predict() function.

3.2.1.2 Model Function

The first thing we need is a function that will bring in the data, create a formula in the model based on input on the type (Frequentist or Bayesian), moderators (none, age, gender, SRhealth, and education), and combinations of covariates (single or fully adjusted based on age, gender, SRhealth, and education). Then we run the model, extract its fixed effect estimates, and save both for later. By saving the results, it will make it easier and faster for us to extract the necessary model results later while still retaining all information from the original model.

ipd1a_mod_fun <- function(trait, outcome, type, mod, cov){
  ## load the data
  load(sprintf("%s/data/one_stage/%s_%s.RData", local_path, trait, outcome))
  
  ## model formula 
  if (cov == "all") cv <- c("age", "gender", "education")
  if (!cov %in% c("all", "none")) cv <- cov
  rhs <- "p_value"
  rhs <- if(cov != "none") c(rhs, cv) else rhs
  if(mod != "none"){rhs <- c(rhs, paste("p_value", mod, sep = "*"))}
  rhs <- paste(rhs, collapse = " + ")
  f <- paste("o_value ~ ", rhs, collapse = "")
  
  ## compiled Bayesian model to speed up processing and avoid crashing
  if(type == "Bayesian") load(sprintf("%s/results/1a_ipd_reg/bayes_sample_mod.RData", local_path))
  
  ## run the models & save
  m <- if(type == "Frequentist"){do.call("lm", list(formula = f, data = quote(d)))} else {update(m, formula = f, nelocal_pathata = d, cores = 4)}
  save(m, file = sprintf("%s/results/1a_ipd_reg/%s/models/%s_%s_%s_%s.RData", local_path, type, outcome, trait, mod, cov))
  
  ## extract model terms and confidence intervals & save
  fx <- tidy(m, conf.int = T) %>%
    select(term, estimate, conf.low, conf.high)
  save(fx, file = sprintf("%s/results/1a_ipd_reg/%s/summary/%s_%s_%s_%s.RData", local_path, type, outcome, trait, mod, cov))
  
  ## get simple effects for moderator tests
  if(mod != "none"){
    pred.fx <- ipd1a_simpeff_fun(m, mod, type)
    save(pred.fx, file = sprintf("%s/results/1a_ipd_reg/%s/predicted/%s_%s_%s_%s.RData", local_path, type, outcome, trait, mod, cov))
  }
  
  ## clean up the local function environment
  rm(list = c("d", "f", "rhs", "m", "fx", "cv"))
  gc()
}

3.2.1.3 Simple Effects Function

ipd1a_simpeff_fun <- function(m, moder, type){
  d <- if(type == "Bayesian") m$data else m$model
  d <- d %>% select(-o_value, -p_value)
  cols <- colnames(d)
  md_cl <- class(d[,moder])
  if(any(sapply(d, class) == "numeric")){
    msd <- d %>%
      select_if(is.numeric) %>%
      pivot_longer(everything()
                   , names_to = "item"
                   , values_to = "value") %>%
      group_by(item) %>%
      summarize_at(vars(value), lst(mean, sd)) %>%
      ungroup()
  }
  if(any(sapply(d, class) == "factor")){
    fct_lev <- d %>% 
      select_if(is.factor) %>%
      summarize_all(~list(unique(.)))
  }
  d <- d %>% select(-one_of(moder))
  md_levs <- if(md_cl == "numeric"){
    if(moder %in% c("age", "baseAge", "baseYear")) {
      c(-10, 0, 10)
      } else if (moder %in% c("predInt", "education")) {
      c(-5, 0, 5) 
      } else {
        with(msd, c(mean[item == moder] - sd[item == moder], mean[item == moder], mean[item == moder] + sd[item == moder]))
      }
  } else { 
    unique(fct_lev[,moder][[1]])
  }
  
  mod_frame <- expand.grid(
    p_value = seq(0,10,.5)
    , modvalue = md_levs
    , stringsAsFactors = F
    ) %>% setNames(c("p_value", moder))
  
  if(ncol(d) > 0){
    if(any(sapply(d, class) == "numeric")){
      mod_frame <- tibble(mod_frame, d %>% select_if(is.numeric) %>% summarize_all(mean))
    } 
    if(any(sapply(d, class) == "factor")){
      mod_frame <- tibble(mod_frame, d %>% select_if(is.factor) %>% summarize_all(~levels(.)[1]))
    }
  }
  
  pred.fx <- if(type == "Bayesian"){
    bind_cols(
      mod_frame, 
      fitted(m, newdata = mod_frame) %>% data.frame
    ) %>%
      select(one_of(colnames(m$data)), pred = Estimate, lower = Q2.5, upper = Q97.5)
  } else {
    bind_cols(
      mod_frame, 
      predict(m, newdata = mod_frame, interval = "confidence") %>% data.frame 
    ) %>%
      select(one_of(colnames(m$model)), pred = fit, lower = lwr, upper = upr)
  }
  
  rm(list = c("m", "mod_frame", "d", "md_levs"))
  gc()
  return(pred.fx)
}

3.2.1.4 Run Models and Summaries

The code below generates all possible preregistrered combinations of traits, outcomes, types, moderators, and covariates. Then these combinations are fed serially to the model function written previously, which will run and save the results.

load(sprintf("%s/data/one_stage/E_crystallized.RData", local_path))

# clean data & keep only needed columns and a subset of the used variables
d <- d %>%
  group_by(study) %>%
  nest() %>%
  ungroup() %>%
  mutate(data = map(data, ~(.) %>% filter(row_number() %in% sample(1:nrow(.), 100, replace = F)))) %>%
  unnest(data) 

# set priors & model specifications 
Prior <-  c(set_prior("student_t(3, 0, 2)", class = "b"),
            set_prior("student_t(3, 0, 5)", class = "Intercept"))
Iter <- 30; Warmup <- 21; treedepth <- 20
f <- bf(o_value ~ p_value + age + gender + education)
m <- brm(formula = f
            , data = d
            , prior = Prior
            , iter = Iter
            , warmup = Warmup
            , cores = 4)
save(m, file = sprintf("%s/results/1a_ipd_reg/bayes_sample_mod.RData", local_path))
rm(list = c("d", "Prior", "Iter", "Warmup", "treedepth", "f", "m"))
plan(multisession(workers = 10L))
nested_ipd1a_reg <- 
  ## moderator combinations
  crossing(
    Trait = traits$short_name
    , Outcome = outcomes$short_name
    , type = c("Frequentist", "Bayesian")
    , Moderator = c("age", "gender", "education")
    , Covariate = c("none", "all")
) %>%
  full_join(
    # undmoderator combinations
    crossing(
      Trait = traits$short_name
      , Outcome = outcomes$short_name
      , type = c("Frequentist", "Bayesian")
      , Moderator = c("none", stdyModers$short_name)
      , Covariate = c("none", "age", "gender", "education", "all")
      )
) %>%
  mutate(run = 
           # pmap(list(Trait, Outcome, type, Moderator, Covariate)
           future_pmap(list(Trait, Outcome, type, Moderator, Covariate)
                , possibly(ipd1a_mod_fun, "uh-oh")
                , .progress = T
             , .options = furrr_options(
                                    globals = c("ipd1a_mod_fun"
                                                , "ipd1a_simpeff_fun"
                                                , "read_path"
                                                , "local_path"
                                                , "res_path"
                                                , "codebook"
                                                , "covars"
                                                , "moders"
                                                , "outcomes"
                                                , "studies"
                                                , "stdyModers"
                                                , "traits"
                                                , "data_path")
                                  , packages = c("lme4"
                                                 , "broom"
                                                 , "psych"
                                                 , "knitr"
                                                 , "broom.mixed"
                                                 , "brms"
                                                 #, "tidybayes"
                                                 #, "bootpredictlme4"
                                                 , "rstan"
                                                 , "estimatr"
                                                 #, "merTools"
                                                 , "plyr"
                                                 , "tidyverse"))
             )
         )
closeAllConnections()

3.2.1.5 Compile Results

Once all the models are run, we are ready to compile all their results. By saving the fixed effects results previously, we are able to simply load those results and ignore the models. However, because we also saved the models, we can also recall and extract information from them if and when needed.

loadRData <- function(fileName, type, obj, folder){
# loads an RData file, and returns it
    # path <- sprintf("%s/results/1a_ipd_reg/%s/%s/%s", res_path, type, folder, fileName)
    path <- sprintf("%s/results/1a_ipd_reg/%s/%s/%s", local_path, type, folder, fileName)
    # load(url(path))
    load(path)
    get(ls()[grepl(obj, ls())])
}

## load in "fixed" effects
## first get file names
nested_ipd1a_reg <- tibble(type = c("Frequentist", "Bayesian")) %>%
  mutate(file = map(type, ~list.files(sprintf("%s/results/1a_ipd_reg/%s/summary", local_path, .)))) %>%
  unnest(file) %>%
  separate(file, c("Outcome", "Trait", "Moderator", "Covariate"), sep = "_", remove = F) %>% 
  ## read in the files
  mutate(Covariate = str_remove(Covariate, ".RData"),
         fx = map2(file, type, ~loadRData(.x, .y, "fx", "summary"))) %>%
  select(-file) 
nested_ipd1a_reg
## # A tibble: 410 × 6
##    type        Outcome      Trait Moderator Covariate fx              
##    <chr>       <chr>        <chr> <chr>     <chr>     <list>          
##  1 Frequentist crystallized A     age       all       <tibble [7 × 4]>
##  2 Frequentist crystallized A     age       none      <tibble [4 × 4]>
##  3 Frequentist crystallized A     baseAge   age       <tibble [5 × 4]>
##  4 Frequentist crystallized A     baseAge   all       <tibble [8 × 4]>
##  5 Frequentist crystallized A     baseAge   education <tibble [5 × 4]>
##  6 Frequentist crystallized A     baseAge   gender    <tibble [5 × 4]>
##  7 Frequentist crystallized A     baseAge   none      <tibble [4 × 4]>
##  8 Frequentist crystallized A     baseYear  age       <tibble [5 × 4]>
##  9 Frequentist crystallized A     baseYear  all       <tibble [8 × 4]>
## 10 Frequentist crystallized A     baseYear  education <tibble [5 × 4]>
## # ℹ 400 more rows

As can be seen above, the resulting data frame is nested, but it can be easily unnested using the unnest() function.

nested_ipd1a_reg %>%
  unnest(fx)
## # A tibble: 2,993 × 9
##    type        Outcome      Trait Moderator Covariate term         estimate conf.low  conf.high
##    <chr>       <chr>        <chr> <chr>     <chr>     <chr>           <dbl>    <dbl>      <dbl>
##  1 Frequentist crystallized A     age       all       (Intercept)   5.86     5.70     6.01     
##  2 Frequentist crystallized A     age       all       p_value      -0.0456  -0.0642  -0.0269   
##  3 Frequentist crystallized A     age       all       age           0.0157   0.00510  0.0262   
##  4 Frequentist crystallized A     age       all       genderFemale  0.165    0.103    0.228    
##  5 Frequentist crystallized A     age       all       SRhealth     -0.00315 -0.0203   0.0140   
##  6 Frequentist crystallized A     age       all       education     0.260    0.249    0.270    
##  7 Frequentist crystallized A     age       all       p_value:age  -0.00136 -0.00275  0.0000259
##  8 Frequentist crystallized A     age       none      (Intercept)   7.20     7.07     7.34     
##  9 Frequentist crystallized A     age       none      p_value      -0.141   -0.158   -0.124    
## 10 Frequentist crystallized A     age       none      age           0.00858 -0.00218  0.0193   
## # ℹ 2,983 more rows
3.2.1.5.1 Tables
3.2.1.5.1.1 Fixed Effects

Next, we want to format the study results in APA table format. In this case, we are interested in the fixed effects of personality predicting cognitive ability when there were no moderators, and the personality x moderator interaction when there was a moderator.

## format results 
ipd1a_reg_tab <- nested_ipd1a_reg %>%
  unnest(fx) %>%
  # keep key terms 
  filter((Moderator == "none" & term == "p_value") |
         (Moderator != "none" & grepl("p_value:", term))) %>%
  # mark significance and prettify trait, outcome, and covariate names
  mutate(sig = ifelse(sign(conf.low) == sign(conf.high), "sig", "ns"),
         Trait = factor(Trait, traits$short_name),
         Outcome = factor(Outcome, outcomes$short_name, outcomes$long_name),
         Moderator = factor(Moderator, c(moders$short_name, stdyModers$short_name), c(moders$long_name, stdyModers$long_name)),
         Covariate = ifelse(Moderator != "None" & Covariate == "None", Moderator, Covariate),
         Covariate = factor(Covariate, moders$short_name, str_wrap(moders$long_name, 15)),
         term = str_remove_all(term, "p_value:"),
         term = mapvalues(term, c("scaleBFIMS", "scaleIPIPNEO", "scaleTDAM40", "countryTheNetherlands")
                          , c("scaleBFI-S", "scaleIPIP NEO", "scaleTDA-40", "countryThe Netherlands")),
         term2 = factor(term, c(moders$short_term, stdyModers$short_term),
                       c(moders$long_term, stdyModers$long_term))) %>%
  # format values as text, combine estimates and CI's, bold significance
  mutate_at(vars(estimate, conf.low, conf.high), 
            ~ifelse(abs(.) < .01, sprintf("%.3f", .), sprintf("%.2f", .))) %>%
  mutate(est = sprintf("%s<br>[%s, %s]", estimate, conf.low, conf.high),
         est = ifelse(sig == "sig", sprintf("<strong>%s</strong>", est), est)) %>%
  # mutate(est = sprintf("%s [%s, %s]", estimate, conf.low, conf.high),
  # est = ifelse(sig == "sig", sprintf("\\textbf{%s}", est), est)) %>%
  # final reshaping, remove extra columns, arrange values, and change to wide format
  select(-estimate, -conf.low, -conf.high, -sig, -term) %>%
  arrange(type, Outcome, Trait, Moderator, Covariate) %>%
  pivot_wider(names_from = "Trait", values_from = "est") 
ipd1a_reg_tab
## # A tibble: 172 × 10
##    type     Outcome              Moderator Covariate      term2                  E                    A     C     N     O    
##    <chr>    <fct>                <fct>     <fct>          <fct>                  <chr>                <chr> <chr> <chr> <chr>
##  1 Bayesian Crystallized Ability None      None           Personality            <strong>-0.08<br>[-… <str… 0.00… -0.1… <str…
##  2 Bayesian Crystallized Ability None      Age            Personality            <strong>-0.09<br>[-… <str… 0.00… 0.09… <str…
##  3 Bayesian Crystallized Ability None      Gender         Personality            <strong>-0.08<br>[-… <str… 0.02… -0.0… <str…
##  4 Bayesian Crystallized Ability None      Education      Personality            <strong>-0.07<br>[-… <str… -0.0… -0.1… <str…
##  5 Bayesian Crystallized Ability None      Fully Adjusted Personality            <strong>-0.07<br>[-… <str… -0.0… -0.0… <str…
##  6 Bayesian Crystallized Ability Age       None           Age                    0.000<br>[-0.001, 0… -0.0… <str… 0.02… -0.0…
##  7 Bayesian Crystallized Ability Age       Fully Adjusted Age                    0.000<br>[-0.001, 0… -0.0… -0.0… -0.0… <str…
##  8 Bayesian Crystallized Ability Gender    None           Gender (Male v Female) <strong>0.07<br>[0.… <str… 0.00… -0.1… -0.0…
##  9 Bayesian Crystallized Ability Gender    Fully Adjusted Gender (Male v Female) <strong>0.05<br>[0.… <str… 0.03… -0.0… -0.0…
## 10 Bayesian Crystallized Ability Education None           Education (Years)      <strong>0.004<br>[0… -0.0… <str… 0.19… 0.00…
## # ℹ 162 more rows

Now that we’ve formatted the values, we can group by moderators and save results as separate tables. Even though additional information could be included given that we have one outcome, we’ll stick with this split because it will make it easier for those using this tutorial who multiple traits, outcomes, covariates, and moderators.

## table function 
ipd1a_tab_fun <- function(d, type, moder){
  # long outcome name
  md <- mapvalues(moder, c(moders$long_name, stdyModers$long_name), c(moders$short_name, stdyModers$short_name), warn_missing = F)
  # getting row numbers for later grouping
  rs <- d %>% group_by(Outcome) %>% tally() %>% 
    mutate(end = cumsum(n), start = lag(end) + 1, start = ifelse(is.na(start), 1, start))
  # number and name of columns for span columns 
  cs <- if(length(unique(d$term2)) > 1) c(2, rep(1,5)) else rep(1,6)
  names(cs) <- c(" ", traits$long_name)
  cln <- if(length(unique(d$term2)) == 1) c("Covariate", rep("\\textit{b} [CI]", 5)) else c(" ", "Term", rep("\\textit{b} [CI]", 5))
  cln <- if(length(unique(d$term2)) == 1) c("Covariate", rep("<em>b</em> [CI]", 5)) else c(" ", "Term", rep("<em>b</em> [CI]", 5))
  al <- if(length(unique(d$term2)) == 1) c("r", rep("c", 5)) else c("r", "r", rep("c", 5))
  if(length(unique(d$term2)) == 1) {
    d <- d %>% select(-term2); dubs <- F
  } 
  # caption 
  cap <- if(md == "none") "1A Pooled Analysis of Individual Participant Data: Fixed Effect Personality-Crystallized Domain Associations" else sprintf("1A Pooled Analysis of Individual Participant Data: Fixed Effect %s Moderation of Personality-Crystallized Domain Associations", md)
  # kable the table
  tab <- d %>%
    select(-Outcome) %>%
    kable(., "html"
    # kable(., "latex"
          , booktabs = T
          , escape = F
          , col.names = cln
          , align = al
          , caption = cap
    ) %>% 
    kable_classic(full_width = F, html_font = "Times New Roman") %>%
    # kable_styling(full_width = F, font_size = 7) %>%
    add_header_above(cs)
  # for loop to add grouped sections 
  for (i in 1:nrow(rs)){
    tab <- tab %>% 
      kableExtra::group_rows(rs$Moderator[i], rs$start[i], rs$end[i]) 
  }
  # save the resulting html table
  save_kable(tab, file = sprintf("%s/results/1a_ipd_reg/%s/tables/overall/%s.html"
                                 , local_path, type, md))
  return(tab) # return the html table
}

ipd1a_fx_tab <- ipd1a_reg_tab %>%
  group_by(type, Moderator) %>%
  nest() %>%
  ungroup() %>%
  mutate(tab = pmap(list(data, type, Moderator), ipd1a_tab_fun))
ipd1a_reg_tab
## # A tibble: 172 × 10
##    type     Outcome              Moderator Covariate      term2                  E                    A     C     N     O    
##    <chr>    <fct>                <fct>     <fct>          <fct>                  <chr>                <chr> <chr> <chr> <chr>
##  1 Bayesian Crystallized Ability None      None           Personality            <strong>-0.08<br>[-… <str… 0.00… -0.1… <str…
##  2 Bayesian Crystallized Ability None      Age            Personality            <strong>-0.09<br>[-… <str… 0.00… 0.09… <str…
##  3 Bayesian Crystallized Ability None      Gender         Personality            <strong>-0.08<br>[-… <str… 0.02… -0.0… <str…
##  4 Bayesian Crystallized Ability None      Education      Personality            <strong>-0.07<br>[-… <str… -0.0… -0.1… <str…
##  5 Bayesian Crystallized Ability None      Fully Adjusted Personality            <strong>-0.07<br>[-… <str… -0.0… -0.0… <str…
##  6 Bayesian Crystallized Ability Age       None           Age                    0.000<br>[-0.001, 0… -0.0… <str… 0.02… -0.0…
##  7 Bayesian Crystallized Ability Age       Fully Adjusted Age                    0.000<br>[-0.001, 0… -0.0… -0.0… -0.0… <str…
##  8 Bayesian Crystallized Ability Gender    None           Gender (Male v Female) <strong>0.07<br>[0.… <str… 0.00… -0.1… -0.0…
##  9 Bayesian Crystallized Ability Gender    Fully Adjusted Gender (Male v Female) <strong>0.05<br>[0.… <str… 0.03… -0.0… -0.0…
## 10 Bayesian Crystallized Ability Education None           Education (Years)      <strong>0.004<br>[0… -0.0… <str… 0.19… 0.00…
## # ℹ 162 more rows
## Frequentist, no moderator
(ipd1a_fx_tab %>% filter(type == "Frequentist" & Moderator == "None"))$tab[[1]]
(#tab:ipd1a table saving)1A Pooled Analysis of Individual Participant Data: Fixed Effect Personality-Crystallized Domain Associations
Extraversion
Agreeableness
Conscientiousness
Neuroticism
Openness to Experience
Covariate b [CI] b [CI] b [CI] b [CI] b [CI]
None -0.08
[-0.10, -0.07]
-0.14
[-0.16, -0.12]
0.007
[-0.01, 0.02]
-0.003
[-0.02, 0.01]
0.20
[0.19, 0.22]
Age -0.09
[-0.10, -0.07]
-0.14
[-0.16, -0.12]
0.007
[-0.01, 0.02]
0.002
[-0.01, 0.02]
0.19
[0.18, 0.21]
Gender -0.08
[-0.09, -0.06]
-0.14
[-0.15, -0.12]
0.02
[-0.001, 0.03]
0.007
[-0.007, 0.02]
0.19
[0.17, 0.21]
Education -0.07
[-0.09, -0.06]
-0.06
[-0.08, -0.05]
-0.01
[-0.03, 0.005]
-0.03
[-0.04, -0.01]
0.10
[0.09, 0.12]
Fully Adjusted -0.03
[-0.05, -0.02]
-0.05
[-0.06, -0.03]
0.03
[0.02, 0.05]
-0.03
[-0.04, -0.01]
0.13
[0.11, 0.14]
## bayesian
(ipd1a_fx_tab %>% filter(type == "Bayesian" & Moderator == "None"))$tab[[1]]
(#tab:ipd1a table saving)1A Pooled Analysis of Individual Participant Data: Fixed Effect Personality-Crystallized Domain Associations
Extraversion
Agreeableness
Conscientiousness
Neuroticism
Openness to Experience
Covariate b [CI] b [CI] b [CI] b [CI] b [CI]
None -0.08
[-0.10, -0.07]
-0.14
[-0.16, -0.12]
0.007
[-0.010, 0.02]
-0.18
[-0.59, 0.18]
0.20
[0.19, 0.22]
Age -0.09
[-0.10, -0.07]
-0.14
[-0.16, -0.12]
0.007
[-0.01, 0.02]
0.09
[-0.21, 0.47]
0.19
[0.18, 0.21]
Gender -0.08
[-0.09, -0.06]
-0.14
[-0.15, -0.12]
0.02
[-0.000, 0.03]
-0.08
[-0.68, 0.41]
0.19
[0.17, 0.20]
Education -0.07
[-0.09, -0.06]
-0.06
[-0.08, -0.05]
-0.01
[-0.03, 0.006]
-0.11
[-0.72, 0.19]
0.10
[0.09, 0.12]
Fully Adjusted -0.07
[-0.08, -0.05]
-0.07
[-0.09, -0.06]
-0.01
[-0.03, 0.006]
-0.05
[-0.33, 0.29]
0.11
[0.09, 0.12]
ipd1a_tab_fun <- function(d, type, cov){
  # long outcome name
  covar <- mapvalues(cov, covars$long_name, covars$short_name, warn_missing = F)
  # getting row numbers for later grouping
  rs <- d %>% group_by(Moderator) %>% tally() %>% 
    mutate(end = cumsum(n), start = lag(end) + 1, start = ifelse(is.na(start), 1, start))
  # number and name of columns for span columns 
  cs <- rep(1,6)
  names(cs) <- c(" ", traits$long_name)
  # cln <- if(length(unique(d$term2)) == 1) c("Covariate", rep("\\textit{b} [CI]", 5)) else c(" ", "Term", rep("\\textit{b} [CI]", 5))
  cln <- c("Term", rep("<em>b</em> [CI]", 5))
  al <- c("r", rep("c", 5))
  # caption 
  cv <- if (cov == "None") "Unadjusted" else cov
  cap <- sprintf("1A Pooled Analysis of Individual Participant Data: Fixed Effect Estimates of %s Personality-Crystallized Domain Associations", cv)
  # kable the table
  tab <- d %>%
    select(-Moderator) %>%
    kable(., "html"
    # kable(., "latex"
          , booktabs = T
          , escape = F
          , col.names = cln
          , align = al
          , caption = cap
    ) %>% 
    kable_classic(full_width = F, html_font = "Times New Roman") %>%
    # kable_styling(full_width = F, font_size = 7) %>%
    add_header_above(cs)
  # for loop to add grouped sections 
  for (i in 1:nrow(rs)){
    tab <- tab %>% 
      kableExtra::group_rows(rs$Moderator[i], rs$start[i], rs$end[i]) 
  }
  # save the resulting html table
  save_kable(tab, file = sprintf("%s/results/1a_ipd_reg/%s/tables/key terms/%s.html"
                                 , local_path, type, covar))
  return(tab) # return the html table
}

ipd1a_fx_tab2 <- ipd1a_reg_tab %>%
  arrange(Outcome, Moderator, term2) %>%
  filter(Covariate %in% c("None", "Fully Adjusted")) %>%
  group_by(Outcome, type, Covariate) %>% 
  nest() %>%
  ungroup() %>%
  mutate(tab = pmap(list(data, type, Covariate), ipd1a_tab_fun))

## Frequentist, no moderator
(ipd1a_fx_tab2 %>% filter(type == "Frequentist" & Covariate == "Fully Adjusted"))$tab[[1]]
(#tab:ipd1a key term tab)1A Pooled Analysis of Individual Participant Data: Fixed Effect Estimates of Fully Adjusted Personality-Crystallized Domain Associations
Extraversion
Agreeableness
Conscientiousness
Neuroticism
Openness to Experience
Term b [CI] b [CI] b [CI] b [CI] b [CI]
None
Personality -0.03
[-0.05, -0.02]
-0.05
[-0.06, -0.03]
0.03
[0.02, 0.05]
-0.03
[-0.04, -0.01]
0.13
[0.11, 0.14]
Age
Age -0.000
[-0.002, 0.001]
-0.001
[-0.003, 0.000]
0.001
[-0.001, 0.002]
0.002
[0.001, 0.003]
-0.003
[-0.005, -0.002]
Gender
Gender (Male v Female) 0.05
[0.02, 0.08]
0.03
[-0.01, 0.06]
0.01
[-0.02, 0.05]
-0.01
[-0.04, 0.01]
-0.02
[-0.06, 0.009]
Education
Education (Years) 0.007
[0.002, 0.01]
-0.001
[-0.007, 0.005]
-0.007
[-0.01, -0.001]
-0.002
[-0.006, 0.001]
0.005
[-0.000, 0.01]
Continent
Continent (North America v Europe) 0.15
[0.10, 0.20]
-0.03
[-0.09, 0.04]
-0.04
[-0.10, 0.01]
0.06
[0.03, 0.09]
0.04
[-0.01, 0.10]
Continent (North America v Australia) 0.06
[0.02, 0.09]
0.002
[-0.04, 0.04]
-0.04
[-0.08, -0.008]
-0.13
[-0.17, -0.10]
0.11
[0.08, 0.14]
Country
Country (United States v Germany) 0.20
[0.12, 0.27]
0.005
[-0.08, 0.09]
-0.06
[-0.15, 0.04]
0.10
[0.03, 0.17]
0.02
[-0.04, 0.09]
Country (United States v Sweden) 0.08
[0.02, 0.15]
-0.22
[-0.33, -0.11]
-0.21
[-0.30, -0.11]
0.03
[-0.03, 0.09]
0.23
[0.09, 0.36]
Country (United States v The Netherlands) -0.02
[-0.07, 0.03]
Country (United States v Australia) 0.06
[0.02, 0.09]
0.001
[-0.04, 0.04]
-0.05
[-0.08, -0.01]
-0.13
[-0.17, -0.10]
0.11
[0.08, 0.14]
Personality Scale
Scale (NEO-FFI v DPQ) 0.11
[0.06, 0.17]
Scale (NEO-FFI v Eysenck) 0.02
[-0.15, 0.20]
-0.23
[-0.36, -0.09]
-0.15
[-0.28, -0.02]
0.21
[0.14, 0.27]
-0.06
[-0.25, 0.13]
Scale (NEO-FFI v MIDI) -0.06
[-0.22, 0.11]
-0.005
[-0.09, 0.08]
0.06
[-0.04, 0.15]
0.14
[0.10, 0.18]
-0.28
[-0.42, -0.14]
Scale (NEO-FFI v BFI-S) 0.07
[-0.11, 0.25]
0.14
[0.05, 0.22]
-0.32
[-0.47, -0.16]
Scale (NEO-FFI v IPIP NEO) 0.10
[0.01, 0.19]
Scale (NEO-FFI v TDA-40) -0.003
[-0.17, 0.16]
-0.004
[-0.09, 0.08]
0.01
[-0.08, 0.10]
0.02
[-0.02, 0.07]
-0.17
[-0.31, -0.03]
Baseline Age
Study Baseline Age -0.004
[-0.005, -0.003]
-0.001
[-0.002, 0.001]
-0.000
[-0.002, 0.001]
-0.000
[-0.001, 0.001]
-0.004
[-0.005, -0.003]
Baseline Year
Study Baseline Year -0.003
[-0.006, 0.001]
0.007
[0.002, 0.01]
0.007
[0.003, 0.01]
0.002
[0.000, 0.005]
-0.010
[-0.01, -0.004]
Prediction Interval
Prediction Interval 0.002
[-0.005, 0.009]
-0.03
[-0.04, -0.03]
-0.03
[-0.03, -0.02]
-0.006
[-0.01, -0.000]
0.02
[0.01, 0.03]
## bayesian
(ipd1a_fx_tab2 %>% filter(type == "Bayesian" & Covariate == "Fully Adjusted"))$tab[[1]]
(#tab:ipd1a key term tab)1A Pooled Analysis of Individual Participant Data: Fixed Effect Estimates of Fully Adjusted Personality-Crystallized Domain Associations
Extraversion
Agreeableness
Conscientiousness
Neuroticism
Openness to Experience
Term b [CI] b [CI] b [CI] b [CI] b [CI]
None
Personality -0.07
[-0.08, -0.05]
-0.07
[-0.09, -0.06]
-0.01
[-0.03, 0.006]
-0.05
[-0.33, 0.29]
0.11
[0.09, 0.12]
Age
Age 0.000
[-0.001, 0.002]
-0.000
[-0.002, 0.001]
-0.001
[-0.002, 0.001]
-0.009
[-0.09, 0.12]
-0.003
[-0.004, -0.002]
Gender
Gender (Male v Female) 0.05
[0.02, 0.08]
0.04
[0.005, 0.07]
0.03
[-0.005, 0.06]
-0.09
[-0.91, 0.44]
-0.02
[-0.05, 0.01]
Education
Education (Years) 0.005
[0.000, 0.009]
-0.003
[-0.008, 0.001]
-0.006
[-0.01, -0.002]
0.10
[-0.11, 0.39]
0.001
[-0.004, 0.006]
Continent
Continent (North America v Europe) 0.26
[0.21, 0.31]
0.08
[0.02, 0.14]
0.11
[0.05, 0.17]
-0.009
[-0.58, 0.26]
0.08
[0.01, 0.14]
Continent (North America v Australia) 0.18
[0.15, 0.21]
0.08
[0.05, 0.12]
0.09
[0.06, 0.13]
-0.13
[-0.58, 0.36]
0.13
[0.10, 0.16]
Country
Country (United States v Germany) 0.32
[0.24, 0.39]
0.09
[0.001, 0.18]
0.08
[-0.01, 0.18]
0.25
[-0.81, 1.61]
0.05
[-0.02, 0.12]
Country (United States v Sweden) 0.19
[0.12, 0.26]
-0.14
[-0.25, -0.03]
-0.07
[-0.17, 0.04]
-0.23
[-1.40, 0.61]
0.24
[0.09, 0.39]
Country (United States v The Netherlands) 1.00
[0.37, 1.69]
Country (United States v Australia) 0.18
[0.15, 0.21]
0.09
[0.05, 0.12]
0.09
[0.06, 0.13]
-0.01
[-0.56, 0.59]
0.13
[0.10, 0.16]
Personality Scale
Scale (NEO-FFI v DPQ) -0.11
[-1.65, 0.76]
Scale (NEO-FFI v Eysenck) -0.006
[-0.08, 0.07]
-0.24
[-0.36, -0.10]
-0.28
[-0.39, -0.18]
0.67
[0.34, 0.91]
0.25
[0.09, 0.40]
Scale (NEO-FFI v MIDI) -0.07
[-0.11, -0.02]
-0.007
[-0.08, 0.07]
-0.05
[-0.11, 0.007]
0.16
[-0.26, 0.54]
0.04
[-0.03, 0.11]
Scale (NEO-FFI v BFI-S) 0.05
[-0.04, 0.14]
-0.01
[-0.13, 0.10]
-0.14
[-0.24, -0.03]
0.20
[-1.47, 1.79]
-0.007
[-0.10, 0.09]
Scale (NEO-FFI v IPIP NEO) 0.04
[-0.06, 0.14]
0.01
[-0.10, 0.12]
-0.18
[-0.28, -0.09]
-0.02
[-1.12, 1.58]
0.12
[0.02, 0.23]
Scale (NEO-FFI v TDA-40) -0.02
[-0.07, 0.03]
-0.01
[-0.09, 0.07]
-0.11
[-0.17, -0.05]
-0.06
[-0.81, 0.63]
0.13
[0.06, 0.21]
Baseline Age
Study Baseline Age -0.006
[-0.007, -0.005]
-0.003
[-0.004, -0.002]
-0.005
[-0.007, -0.004]
-0.06
[-0.19, 0.000]
-0.005
[-0.006, -0.004]
Baseline Year
Study Baseline Year -0.005
[-0.008, -0.002]
0.01
[0.007, 0.02]
-0.003
[-0.007, 0.001]
-0.07
[-0.16, 0.009]
0.001
[-0.004, 0.006]
Prediction Interval
Prediction Interval 0.009
[0.003, 0.02]
-0.03
[-0.03, -0.02]
-0.01
[-0.02, -0.006]
-0.05
[-0.29, 0.92]
0.003
[-0.005, 0.01]
# save(ipd1a_fx_tab, ipd1a_fx_tab2, ipd1a_res, file = sprintf("%s/manuscript/results/ipd1a_fx_tab.RData", local_path))
3.2.1.5.1.2 Study-Specific Effects

This header is here to simply emphasize that this method does not provide study-specific estimates, unlike Methods 2ab, 3, and 4.

3.2.1.5.1.3 Heterogeneity Estimates

This header is here to simply emphasize that this method does not provide heterogeneity estimates because it does not provide study-specific estimates, unlike Methods 2ab, 3, and 4.

3.2.1.5.1.4 All Model Terms
ipd1a_mod_tab <- nested_ipd1a_reg %>%
  unnest(fx) %>%
  # keep key terms 
  # mark significance and prettify trait, outcome, and covariate names
  mutate(sig = ifelse(sign(conf.low) == sign(conf.high), "sig", "ns"),
         Trait = factor(Trait, traits$short_name),
         Outcome = factor(Outcome, outcomes$short_name, outcomes$long_name),
         Moderator = factor(Moderator, c(moders$short_name, stdyModers$short_name), c(moders$long_name, stdyModers$long_name)),
         Covariate = factor(Covariate, covars$short_name, str_wrap(covars$long_name, 15)),#ifelse(Moderator != "None" & Covariate == "none", Moderator, Covariate),
         ) %>%
  # format values as text, combine estimates and CI's, bold significance
  mutate_at(vars(estimate, conf.low, conf.high), 
            ~ifelse(abs(.) < .01, sprintf("%.3f", .), sprintf("%.2f", .))) %>%
  mutate(est = sprintf("%s<br>[%s, %s]", estimate, conf.low, conf.high),
         est = ifelse(sig == "sig", sprintf("<strong>%s</strong>", est), est)) %>%
  # mutate(est = sprintf("%s [%s, %s]", estimate, conf.low, conf.high),
  # est = ifelse(sig == "sig", sprintf("\\textbf{%s}", est), est)) %>%
  # final reshaping, remove extra columns, arrange values, and change to wide format
  select(-estimate, -conf.low, -conf.high, -sig) %>%
  arrange(type, Outcome, Trait, Moderator, Covariate) %>%
  pivot_wider(names_from = "Trait", values_from = "est") 

ipd1a_mod_tab_fun <- function(d, type, out, moder, cov){
  md <- mapvalues(moder, c(moders$long_name, stdyModers$long_name), c(moders$short_name, stdyModers$short_name), warn_missing = F)
  o <- mapvalues(out, outcomes$long_name, outcomes$short_name, warn_missing = F)
  cv <- mapvalues(cov, covars$long_name, covars$short_name, warn_missing = F)
  cs <- rep(1,6)
  names(cs) <- c(" ", traits$long_name)
  # cln <- if(length(unique(d$term2)) == 1) c("Covariate", rep("\\textit{b} [CI]", 5)) else c(" ", "Term", rep("\\textit{b} [CI]", 5))
  cln <- c("Term", rep("<em>b</em> [CI]", 5))
  al <- c("r", rep("c", 5))
  # caption 
  cap <- if(md == "none") "1A Pooled Analysis of Individual Participant Data: All Model Estimates of Fixed Effect Personality-Crystallized Domain Associations" else sprintf("1A Pooled Analysis of Individual Participant Data: All Model Estimates of Fixed Effect %s Moderation of Personality-Crystallized Domain Associations", md)
  
  # kable the table
  tab <- d %>%
    arrange(term) %>%
    kable(., "html"
    # kable(., "latex"
          , booktabs = T
          , escape = F
          , col.names = cln
          , align = al
          , caption = cap
    ) %>% 
    kable_classic(full_width = F, html_font = "Times New Roman") %>%
    # kable_styling(full_width = F, font_size = 7) %>%
    add_header_above(cs)
  # save the resulting html table
  save_kable(tab, file = sprintf("%s/results/1a_ipd_reg/%s/tables/all terms/%s-%s-%s.html"
                                 , local_path, type, o, md, cv))
  return(tab) # return the html table
}

ipd1a_mod_tab <- ipd1a_mod_tab %>%
  group_by(type, Outcome, Moderator, Covariate) %>%
  nest() %>%
  ungroup() %>%
  mutate(tab = pmap(list(data, type, Outcome, Moderator, Covariate), ipd1a_mod_tab_fun))
3.2.1.5.2 Figures
3.2.1.5.2.1 Overall Forest
ipd1a_fx_plot_fun <- function(df, mod, type, cov){
  m <- mapvalues(mod, c(moders$short_name, stdyModers$short_name), c(moders$long_name, stdyModers$long_name), warn_missing = F)
  cv <- mapvalues(cov, covars$short_name, covars$long_name, warn_missing = F)
  dl <- round(max(abs(min(df$estimate)), abs(max(df$estimate))), 3)
  # stds <- unique(df$study)
  lim <- c(0-dl-(dl/2.5), 0+dl+(dl/2.5))
  brk <- if(dl > .01) round(c(0-dl-(dl/5), 0, 0+dl+(dl/5)),2) else round(c(0-dl-(dl/5), 0, 0+dl+(dl/5)),3) 
  # lim_high <- lim[2]*4
  lab <- str_replace(brk, "^0.", ".")#str_remove(round(c(0-d-(d/5), 0, 0+d+(d/5)),2), "^0")
  shapes <- c(15, 16, 17, 18)[1:length(unique(df$term))]
  lt <- rep("solid", length(unique(df$term)))
  titl <- if(mod == "none"){NULL} else {sprintf("%s Moderation of Personality-Outcome Associations", m)}
  titl <- if(!cov %in% c("none", "all")) paste(cv, "Adjusted", titl, collapse = " ") else paste(cv, titl, collapse = " ")
  leg <- if(length(unique(df$term)) > 1){"bottom"} else {"none"}
  p <- df %>%
    mutate(conf.low = ifelse(conf.low < lim[1], lim[1], conf.low),
           conf.high = ifelse(conf.high > lim[2], lim[2], conf.high)) %>% 
  ggplot(aes(x = Outcome, y = estimate)) +
    scale_y_continuous(limits = lim, breaks = brk, labels = lab) + 
    scale_size_manual(values = c(2, 1.5)) +
    scale_shape_manual(values = shapes) +
    scale_color_manual(values = c("blue", "black")) +
    scale_linetype_manual(values = lt) +
    geom_hline(aes(yintercept = 0), size = .5, linetype = "dashed") +
    geom_errorbar(aes(ymin = conf.low, ymax = conf.high, linetype = term)
                  , width = 0
                  , position = position_dodge(width = .9)) + 
    geom_point(aes(color = sig, size = sig, shape = term)
                  , position = position_dodge(width = .9)) +
    labs(x = NULL
         , y = "Estimate (POMP)"
         , title = titl
         , subtitle = "Method 1A: Pooled Simple Linear Regression"
         , term = "Term"
         ) +
    guides(color = "none", size = "none") +
    facet_grid(~Trait, scales = "free_y", space = "free") +
    coord_flip() +
    theme_classic() +
    theme(legend.position = leg,
          plot.title = element_text(face = "bold", size = rel(1.2), hjust = .5),
          plot.subtitle = element_text(hjust = .5),
          panel.background = element_rect(color = "black", fill = "white"),
          strip.background = element_blank(),
          strip.text = element_text(face = "bold", color = "black", size = rel(1.4)),
          axis.text = element_text(color = "black"),
          axis.text.y = element_text(size = rel(1)))
  ht <- length(unique(df$Outcome)); ht2 <- length(unique(df$term))
  wdt <- length(unique(df$Trait))
ggsave(file = sprintf("%s/results/1a_ipd_reg/%s/figures/overall forest/%s_%s_fixed.png", local_path, type, mod, cov), width = wdt*2, height = ht + ht2)
# save(p, file = sprintf("%s/results/ipd1a_reg/%s/figures/overall forest/rdata/%s_fixed.RData", type, local_path, mod))
rm(p)
gc()
return(T)
}

# x1 = 1, x2 = 1, y = 2
# x1 = 1, x2 = 2, y = 3

ipd1a_fp <- nested_ipd1a_reg %>%
  unnest(fx) %>%
    filter((Moderator == "none" & term == "p_value")|
          (Moderator != "none" & grepl("p_value:", term))) %>%
    mutate(sig = ifelse(sign(conf.low) == sign(conf.high), "sig", "ns"),
           sig = factor(sig, levels = c("sig","ns")),
           Trait = factor(Trait, levels = traits$short_name),
           Outcome = factor(Outcome, levels = outcomes$short_name, labels = str_wrap(outcomes$long_name, 15)),
           Outcome = forcats::fct_rev(Outcome))  %>%
  group_by(type, Moderator, Covariate) %>%
  nest() %>%
  ungroup() %>%
  mutate(pmap(list(data, Moderator, type, Covariate), ipd1a_fx_plot_fun))
3.2.1.5.2.2 Study-Specific Forest Plots

This header is here to simply emphasize that this method does not provide study-specific estimates, unlike Methods 2ab, 3, and 4.

3.2.1.5.2.3 Overall Simple Effects Plots
loadRData <- function(fileName, type, obj, folder){
#loads an RData file, and returns it
    path <- sprintf("%s/results/1a_ipd_reg/%s/%s/%s", local_path, type, folder, fileName)
    load(path)
    get(ls()[grepl(obj, ls())])
}

## load in "fixed" effects
## first get file names
nested_ipd1a_simp <- tibble(type = c("Frequentist", "Bayesian")) %>%
  mutate(file = map(type, ~list.files(sprintf("%s/results/1a_ipd_reg/%s/predicted", local_path, .)))) %>%
  unnest(file) %>%
  separate(file, c("Outcome", "Trait", "Moderator", "Covariate"), sep = "_", remove = F) %>% 
  ## read in the files
  mutate(Covariate = str_remove(Covariate, ".RData"),
         pred = map2(file, type, ~loadRData(.x, .y, "pred.fx", "predicted"))) %>%
  select(-file) 
nested_ipd1a_simp
## # A tibble: 360 × 6
##    type        Outcome      Trait Moderator Covariate pred             
##    <chr>       <chr>        <chr> <chr>     <chr>     <list>           
##  1 Frequentist crystallized A     age       all       <tibble [63 × 8]>
##  2 Frequentist crystallized A     age       none      <df [63 × 5]>    
##  3 Frequentist crystallized A     baseAge   age       <tibble [63 × 6]>
##  4 Frequentist crystallized A     baseAge   all       <tibble [63 × 9]>
##  5 Frequentist crystallized A     baseAge   education <tibble [63 × 6]>
##  6 Frequentist crystallized A     baseAge   gender    <tibble [63 × 6]>
##  7 Frequentist crystallized A     baseAge   none      <df [63 × 5]>    
##  8 Frequentist crystallized A     baseYear  age       <tibble [63 × 6]>
##  9 Frequentist crystallized A     baseYear  all       <tibble [63 × 9]>
## 10 Frequentist crystallized A     baseYear  education <tibble [63 × 6]>
## # ℹ 350 more rows
3.2.1.5.2.4 Study-Specific Simple Effects Plots

This header is here to simply emphasize that this method does not provide study-specific estimates, unlike Methods 2ab, 3, and 4.

simp_eff_fun <- function(df, outcome, mod, type, cov){ 
  print(paste(outcome, mod))
  o <- mapvalues(outcome, outcomes$short_name, outcomes$long_name, warn_missing = F)
  cv <- mapvalues(cov, covars$short_name, covars$long_name, warn_missing = F)
  m <- mapvalues(mod, c(moders$short_name, stdyModers$short_name), c(moders$long_name, stdyModers$long_name), warn_missing = F)
  d <- round(max(abs(min(df$pred)), abs(max(df$pred))), 3)
  mini <- if(d > 2) .05 else 0-(d+(d/5))
  maxi <- if(d > 2) 2.05 else 0+d+(d/5)
  lim <- c(mini, maxi)
  brk <- if(d > 2) c(0, 1, 2) else{round(c(0-d-(d/10), 0, 0+d+(d/10)),2)}
  lab <- if(d > 2){c("0", "1", "2")} else{str_remove(c(round(0-d-(d/10),2), 0, round(0+d+(d/10),2)), "^0")}
  titl <- if(mod == "none"){o} else {sprintf("%s: Personality x %s Simple Effects", o, m)}
  # colnames(df)[colnames(df) == mod] <- "mod_value"
  df <- df %>% unclass %>% data.frame
  df$mod_value <- df[,mod]
  df <- df %>% select(-all_of(mod)) %>% as_tibble
  if(class(df$mod_value) == "factor"){df <- df %>% mutate(mod_fac = factor(mod_value))} 
  else{df <- df %>%
    group_by(Trait) %>%
    mutate(mod_fac = factor(mod_value, levels = unique(mod_value), labels = c("-1 SD", "M", "+1 SD"))) %>%
    ungroup()
  }
  lt <- c("dotted", "solid", "dashed", "dotdash", "longdash", "twodash")[1:length(unique(df$mod_fac))]
  
  df %>%
    mutate(Trait = factor(Trait, levels = traits$short_name, labels = traits$long_name),
           lower = ifelse(lower < 4, 4, lower),
           upper = ifelse(upper > 10, 10, upper)) %>%
    ggplot(aes(x = p_value
               , y = pred
               , group  = mod_fac))  +
      scale_y_continuous(limits = c(4,10)
                         , breaks = c(4, 6, 8, 10)
                         , labels = c(4, 6, 8, 10)) +
      scale_linetype_manual(values = lt) +
      geom_ribbon(aes(ymin = lower
                      , ymax = upper
                      , fill = mod_fac)
                  , alpha = .25) +
      geom_line(aes(linetype = mod_fac)) +
      labs(x = "Personality (POMP)"
           , y = paste(o, "(POMP)")
           , title = titl
           , linetype = m
           , fill = m
           , subtitle = "Method 1A: Pooled Simple Linear Regression") +
      facet_wrap(~Trait, nrow = 3) +
      theme_classic() +
      theme(legend.position = "bottom"
            , plot.title = element_text(face = "bold", size = rel(1.2), hjust = .5)
            , plot.subtitle = element_text(hjust = .5)
            , strip.background = element_rect(fill = "black")
            , strip.text = element_text(face = "bold", color = "white")
            , axis.text = element_text(color = "black"))
  ggsave(file = sprintf("%s/results/1a_ipd_reg/%s/figures/overall simple effects/%s_%s_%s.png", local_path, type, outcome, mod, cov), width = 6, height = 6)
}

ipd1a_se_plot <- nested_ipd1a_simp %>%
  group_by(type, Outcome, Moderator, Covariate) %>%
  nest() %>%
  ungroup() %>%
  mutate(data = map(data, ~(.) %>% unnest(pred)),
         plot = pmap(list(data, Outcome, Moderator, type, Covariate), simp_eff_fun))
## [1] "crystallized age"
## [1] "crystallized age"
## [1] "crystallized baseAge"
## [1] "crystallized baseAge"
## [1] "crystallized baseAge"
## [1] "crystallized baseAge"
## [1] "crystallized baseAge"
## [1] "crystallized baseYear"
## [1] "crystallized baseYear"
## [1] "crystallized baseYear"
## [1] "crystallized baseYear"
## [1] "crystallized baseYear"
## [1] "crystallized continent"
## [1] "crystallized continent"
## [1] "crystallized continent"
## [1] "crystallized continent"
## [1] "crystallized continent"
## [1] "crystallized country"
## [1] "crystallized country"
## [1] "crystallized country"
## [1] "crystallized country"
## [1] "crystallized country"
## [1] "crystallized education"
## [1] "crystallized education"
## [1] "crystallized gender"
## [1] "crystallized gender"
## [1] "crystallized predInt"
## [1] "crystallized predInt"
## [1] "crystallized predInt"
## [1] "crystallized predInt"
## [1] "crystallized predInt"
## [1] "crystallized scale"
## [1] "crystallized scale"
## [1] "crystallized scale"
## [1] "crystallized scale"
## [1] "crystallized scale"
## [1] "crystallized age"
## [1] "crystallized age"
## [1] "crystallized baseAge"
## [1] "crystallized baseAge"
## [1] "crystallized baseAge"
## [1] "crystallized baseAge"
## [1] "crystallized baseAge"
## [1] "crystallized baseYear"
## [1] "crystallized baseYear"
## [1] "crystallized baseYear"
## [1] "crystallized baseYear"
## [1] "crystallized baseYear"
## [1] "crystallized continent"
## [1] "crystallized continent"
## [1] "crystallized continent"
## [1] "crystallized continent"
## [1] "crystallized continent"
## [1] "crystallized country"
## [1] "crystallized country"
## [1] "crystallized country"
## [1] "crystallized country"
## [1] "crystallized country"
## [1] "crystallized education"
## [1] "crystallized education"
## [1] "crystallized gender"
## [1] "crystallized gender"
## [1] "crystallized predInt"
## [1] "crystallized predInt"
## [1] "crystallized predInt"
## [1] "crystallized predInt"
## [1] "crystallized predInt"
## [1] "crystallized scale"
## [1] "crystallized scale"
## [1] "crystallized scale"
## [1] "crystallized scale"
## [1] "crystallized scale"
# Extraversion continent
load("~/Documents/projects/data synthesis/crystallized/results/1a_ipd_reg/Frequentist/models/crystallized_E_continent_all.RData")
coef(m)
##                (Intercept)                    p_value                        age               genderFemale 
##                5.549134520               -0.046145461                0.004171313                0.141786850 
##                   SRhealth                  education         continentAustralia            continentEurope 
##                0.019022604                0.308632325               -0.485469914                1.179707965 
## p_value:continentAustralia    p_value:continentEurope 
##                0.057715148                0.150113337
cntrm <- c(
  "p_value = 0" # North America
  , "p_value + p_value:continentAustralia = 0" # Australia
  , "p_value + p_value:continentEurope = 0" # Europe
); names(cntrm) <- c("North America", "Australia", "Europe")
(multcomp::glht(m, cntrm) %>% # multcomp hypothesis function
      confint(., calpha = multcomp::univariate_calpha()))$confint %>%
      data.frame() %>% 
  data.frame() %>%
  rownames_to_column("cntr") %>%
  mutate(term = mapvalues(cntr, str_remove(cntrm, " = 0"), names(cntrm))) %>% 
  select(-cntr) %>%
  mutate(est = sprintf("b = %.2f, 95%% CI [%.2f, %.2f]", Estimate, lwr, upr))
##      Estimate         lwr         upr          term                              est
## 1 -0.04614546 -0.06939046 -0.02290047 North America b = -0.05, 95% CI [-0.07, -0.02]
## 2  0.01156969 -0.01240456  0.03554393     Australia   b = 0.01, 95% CI [-0.01, 0.04]
## 3  0.10396788  0.05998319  0.14795256        Europe    b = 0.10, 95% CI [0.06, 0.15]
# Neuroticism continent
load("~/Documents/projects/data synthesis/crystallized/results/1a_ipd_reg/Frequentist/models/crystallized_N_continent_all.RData")
coef(m)
##                (Intercept)                    p_value                        age               genderFemale 
##               5.9846147217              -0.0019269944              -0.0003912502              -0.1545757888 
##                  education         continentAustralia            continentEurope p_value:continentAustralia 
##               0.3065055260              -0.1803310763               1.1872866354              -0.1332956492 
##    p_value:continentEurope 
##               0.0611509497
cntrm <- c(
  "p_value = 0" # North America
  , "p_value + p_value:continentAustralia = 0" # Australia
  , "p_value + p_value:continentEurope = 0" # Europe
); names(cntrm) <- c("North America", "Australia", "Europe")
(multcomp::glht(m, cntrm) %>% # multcomp hypothesis function
      confint(., calpha = multcomp::univariate_calpha()))$confint %>%
      data.frame() %>% 
  data.frame() %>%
  rownames_to_column("cntr") %>%
  mutate(term = mapvalues(cntr, str_remove(cntrm, " = 0"), names(cntrm))) %>% 
  select(-cntr) %>%
  mutate(est = sprintf("b = %.2f, 95%% CI [%.2f, %.2f]", Estimate, lwr, upr))
##       Estimate         lwr         upr          term                              est
## 1 -0.001926994 -0.02067998  0.01682599 North America  b = -0.00, 95% CI [-0.02, 0.02]
## 2 -0.135222644 -0.16119743 -0.10924786     Australia b = -0.14, 95% CI [-0.16, -0.11]
## 3  0.059223955  0.03262787  0.08582004        Europe    b = 0.06, 95% CI [0.03, 0.09]
# Neuroticism continent
load("~/Documents/projects/data synthesis/crystallized/results/1a_ipd_reg/Frequentist/models/crystallized_O_scale_all.RData")
coef(m)
##          (Intercept)              p_value                  age         genderFemale             SRhealth 
##          3.339901072          0.368662547          0.006242933          0.160535249          0.037314257 
##            education           scaleBFI-S         scaleEysenck            scaleMIDI          scaleTDA-40 
##          0.271069104          4.327067799          2.644895382          1.206799564          0.684281664 
##   p_value:scaleBFI-S p_value:scaleEysenck    p_value:scaleMIDI  p_value:scaleTDA-40 
##         -0.316552453         -0.059001287         -0.281742471         -0.172583588
cntrm <- rbind(
  c(0,1,rep(0,12)) # NEO-FFI
  , c(0,1,rep(0,8),1,rep(0,3)) # BFI-S
  , c(0,1,rep(0,9),1,rep(0,2)) # Eysenck
  , c(0,1,rep(0,10),1,0) # MIDI
  , c(0,1,rep(0,11),1) # TDA-40
); rownames(cntrm) <- c("NEO-FFI", "BFI-S", "Eysenck", "MIDI", "TDA-40")
(multcomp::glht(m, cntrm) %>% # multcomp hypothesis function
      confint(., calpha = multcomp::univariate_calpha()))$confint %>%
      data.frame() %>% 
  data.frame() %>%
  rownames_to_column("cntr") %>%
  mutate(term = rownames(cntrm)) %>% 
  select(-cntr) %>%
  mutate(est = sprintf("b = %.2f, 95%% CI [%.2f, %.2f]", Estimate, lwr, upr))
##     Estimate         lwr       upr    term                            est
## 1 0.36866255  0.22956598 0.5077591 NEO-FFI  b = 0.37, 95% CI [0.23, 0.51]
## 2 0.05211009 -0.01683090 0.1210511   BFI-S b = 0.05, 95% CI [-0.02, 0.12]
## 3 0.30966126  0.17573543 0.4435871 Eysenck  b = 0.31, 95% CI [0.18, 0.44]
## 4 0.08692008  0.06461337 0.1092268    MIDI  b = 0.09, 95% CI [0.06, 0.11]
## 5 0.19607896  0.17193054 0.2202274  TDA-40  b = 0.20, 95% CI [0.17, 0.22]

3.2.1.6 Sample Results Section

We examined estimates of overall prospective associations between Big Five personality characteristics and crystallized abilities as well as participant and sample-level moderators of those associations using fully pooled, one-stage regression models with individual participant data in 11 studies. Table S1 presents the fully adjusted (age, gender, education) pooled estimates of the key terms from all unmoderated estimates as well as participant- and sample-level moderators. Across the 20 (5 unmoderated, 15 participant-level moderators) key participant-level terms, 10 (50%) were significant. For unmoderated personality-cognition associations, extraversion (-), agreeableness (-), conscientiousness (+), neuroticism (-), and openness (+) were significantly associated with later crystallized abilities. Forest plots of the overall point estimates are available in the online materials and web app, as are tables with all model terms (e.g., intercepts, covariate-cognitive ability associations).

(#tab:ipd1a key term tab)1A Pooled Analysis of Individual Participant Data: Fixed Effect Estimates of Fully Adjusted Personality-Crystallized Domain Associations
Extraversion
Agreeableness
Conscientiousness
Neuroticism
Openness to Experience
Term b [CI] b [CI] b [CI] b [CI] b [CI]
None
Personality -0.03
[-0.05, -0.02]
-0.05
[-0.06, -0.03]
0.03
[0.02, 0.05]
-0.03
[-0.04, -0.01]
0.13
[0.11, 0.14]
Age
Age -0.000
[-0.002, 0.001]
-0.001
[-0.003, 0.000]
0.001
[-0.001, 0.002]
0.002
[0.001, 0.003]
-0.003
[-0.005, -0.002]
Gender
Gender (Male v Female) 0.05
[0.02, 0.08]
0.03
[-0.01, 0.06]
0.01
[-0.02, 0.05]
-0.01
[-0.04, 0.01]
-0.02
[-0.06, 0.009]
Education
Education (Years) 0.007
[0.002, 0.01]
-0.001
[-0.007, 0.005]
-0.007
[-0.01, -0.001]
-0.002
[-0.006, 0.001]
0.005
[-0.000, 0.01]
Continent
Continent (North America v Europe) 0.15
[0.10, 0.20]
-0.03
[-0.09, 0.04]
-0.04
[-0.10, 0.01]
0.06
[0.03, 0.09]
0.04
[-0.01, 0.10]
Continent (North America v Australia) 0.06
[0.02, 0.09]
0.002
[-0.04, 0.04]
-0.04
[-0.08, -0.008]
-0.13
[-0.17, -0.10]
0.11
[0.08, 0.14]
Country
Country (United States v Germany) 0.20
[0.12, 0.27]
0.005
[-0.08, 0.09]
-0.06
[-0.15, 0.04]
0.10
[0.03, 0.17]
0.02
[-0.04, 0.09]
Country (United States v Sweden) 0.08
[0.02, 0.15]
-0.22
[-0.33, -0.11]
-0.21
[-0.30, -0.11]
0.03
[-0.03, 0.09]
0.23
[0.09, 0.36]
Country (United States v The Netherlands) -0.02
[-0.07, 0.03]
Country (United States v Australia) 0.06
[0.02, 0.09]
0.001
[-0.04, 0.04]
-0.05
[-0.08, -0.01]
-0.13
[-0.17, -0.10]
0.11
[0.08, 0.14]
Personality Scale
Scale (NEO-FFI v DPQ) 0.11
[0.06, 0.17]
Scale (NEO-FFI v Eysenck) 0.02
[-0.15, 0.20]
-0.23
[-0.36, -0.09]
-0.15
[-0.28, -0.02]
0.21
[0.14, 0.27]
-0.06
[-0.25, 0.13]
Scale (NEO-FFI v MIDI) -0.06
[-0.22, 0.11]
-0.005
[-0.09, 0.08]
0.06
[-0.04, 0.15]
0.14
[0.10, 0.18]
-0.28
[-0.42, -0.14]
Scale (NEO-FFI v BFI-S) 0.07
[-0.11, 0.25]
0.14
[0.05, 0.22]
-0.32
[-0.47, -0.16]
Scale (NEO-FFI v IPIP NEO) 0.10
[0.01, 0.19]
Scale (NEO-FFI v TDA-40) -0.003
[-0.17, 0.16]
-0.004
[-0.09, 0.08]
0.01
[-0.08, 0.10]
0.02
[-0.02, 0.07]
-0.17
[-0.31, -0.03]
Baseline Age
Study Baseline Age -0.004
[-0.005, -0.003]
-0.001
[-0.002, 0.001]
-0.000
[-0.002, 0.001]
-0.000
[-0.001, 0.001]
-0.004
[-0.005, -0.003]
Baseline Year
Study Baseline Year -0.003
[-0.006, 0.001]
0.007
[0.002, 0.01]
0.007
[0.003, 0.01]
0.002
[0.000, 0.005]
-0.010
[-0.01, -0.004]
Prediction Interval
Prediction Interval 0.002
[-0.005, 0.009]
-0.03
[-0.04, -0.03]
-0.03
[-0.03, -0.02]
-0.006
[-0.01, -0.000]
0.02
[0.01, 0.03]

In addition, gender (b = 0.05, 95% CI [0.02, 0.08]) moderated the prospective association between extraversion and crystallized abilities. To better understand how gender moderates the relationships, Figure S1 displays the fully adjusted (age, gender, education) predicted crystallized abilities levels at different levels of the Big Five for males and females. As is clear in the first column (Method 1A) and first row (extraversion) of the figure, the overall prospective association between personality-crystallized ability association was negative for males (b = -0.06, 95% CI [-0.08, -0.04]) and null for females (b = -0.01, 95% CI [-0.03, 0.006]). Moreover, education moderated the association between extraversion (b = 0.007, 95% CI [0.002, 0.01]) and conscientiousness (b = -0.007, 95% [-0.01, -0.001]) and later crystallized abilities. For extraversion, this indicated that less educated individuals who were higher in extraversion tended to have lower crystallized ability scores at later time points than those with more education and high in extraversion. For conscientiousness, less educated individuals who were lower in conscientiousness had worse crystallized ability at later time points than more educated individuals who were lower in conscientiousness. Finally, age moderated the relationship between both neuroticism (b = 0.002, 95% CI [0.001, 0.003]) and openness (b = -0.003, 95% CI [-0.005, -0.002]) and crystallized abilities. Older individuals who were higher in neuroticism had higher crystallized ability scores than younger individuals. Older individuals who were lower in openness had higher crystallized ability scores than younger individuals who were low in openness. All other simple effects of participant-level moderators are available in the online materials and in the web app.

Figure S1. Simple effects of the prospective association between Big Five personality characteristics (rows) and crystallized abilities across gender (male, female) using all methods of data synthesis (columns). The x-axis captures personality in percentage of maximum possible (POMP) units, the y-axis captures crystallized abilities in POMP, and different panels represent each of the Big Five characteristics. Different lines indicate the predicted levels of cognitive ability for males (dotted line) and females (solid line).

Figure 3.1: Figure S1. Simple effects of the prospective association between Big Five personality characteristics (rows) and crystallized abilities across gender (male, female) using all methods of data synthesis (columns). The x-axis captures personality in percentage of maximum possible (POMP) units, the y-axis captures crystallized abilities in POMP, and different panels represent each of the Big Five characteristics. Different lines indicate the predicted levels of cognitive ability for males (dotted line) and females (solid line).

Additionally, we examined sample-level moderators. These moderators are those that are the same across the individuals in the sample and speak more to similarities and differences in the samples as a whole. Specifically, we examined how the continent and country of origin of each sample, the personality scale used, the average baseline age of the sample, the average baseline year of assessment of personality, and the average interval between personality and cognitive assessment may predict the estimated personality-cognitive ability association. Estimates of the sample-level moderators of Big Five personality characteristic-crystallized / knowledge domain cognitive ability associations across methods can be seen in Table S1. Of these 55 tested associations, 36 (65.45%) were significant. For example, the associations between both extraversion and neuroticism and crystallized / knowledge domain associations were more positive for European and Australian samples than for North American samples. Although North American samples (b = -0.05, 95% CI [-0.07, -0.02]) demonstrated negative associations between extraversion and cognitive ability, European samples (b = 0.06, 95% CI [0.06, 0.15]) demonstrated positive associations, and Australian samples (b = 0.01, 95% CI [-0.01, 0.04]) null associations. For neuroticism, North American samples (b = -0.01, 95% CI [-0.02, 0.02]) demonstrated null associations with cognitive ability, while European (b = 0.06, 95% CI [0.03, 0.09]) samples demonstrated a positive association and Australian samples (b = -0.14, 95% CI [-0.16, -0.11]) demonstrated a negative association. For personality scales, NEO-FFI measures of openness (b = 0.37, 95% CI [0.23, 0.51]) tended to be differently associated with crystallized / knowledge domain cognitive ability than other personality scales, including the MIDI (HRS, b = 0.09, 95% CI [0.06, 0.11]), the BFI-S (GSOEP, b = 0.05, 95% CI [-0.02, 0.12]), and the TDA-40 (HILDA, b = 0.20, 95% CI [0.17, 0.22]).

rm(list = ls()[grepl("ipd1a", ls())])

3.2.2 Part 1B: Pooled Linear Regression with Cluster Robust Standard Errors

3.2.2.1 Analytic Plan

In the present study, we aimed to examine associations between personality traits and crystallized cognitive abilities using fully pooled, single-stage regression models with cluster robust standard errors (Gaure, 2013; Pustejovsky & Tipton, 2018) and individual participant data. Correcting for dependencies without explicitly modeling cross-sample heterogeneity is sometimes called nuisance clustering (Fitzmaurice & Laird, 1995). Below, we detail each stage of the analysis.

3.2.2.1.1 Statistical Modeling

A single regression model tests the relationship between personality and crystallized cognitive ability across all samples. Using the R programming language, we created two functions to (1) set up and run the model and extract model coefficients and (2) extract simple-effects predictions for moderator models (i.e. predicted values across levels of the moderator values). The basic form of the model is as follows:

\(Y_{ij}=b_0+b_1\ast predictor_{ij}+\epsilon_{ij}\) \(\varepsilon_{ij}\sim\mathcal{N}(0, \sigma^2)\),

with sample as a cluster, where \(b_1\) represents the overall effect of personality predicting the outcome. For moderator models, we add two additional terms, \(b_2\), which captures the prospective association between a moderator and cognitive ability, adjusting for personality, and b_3, which captures how cognitive ability varies as a function of both personality and the moderator (e.g., does the association differ for males and females).

Models will be tested using the lm_robust() function from the estimatr package in R, with cluster=study and se_type="CR0". The tidy() function from the broom package was used to extract model coefficients and confidence intervals (CI). Inferences will be based on the 95% confidence intervals. Simple-effects predictions were calculated by providing the full range of personality (0-10) and average levels of the covariates from the data used to estimate the model as the “newdata” argument in the predict() function.

3.2.2.2 Model Function

The first thing we need is a function that will bring in the data, create a formula in the model based on input on the type (Frequentist or Bayesian), moderators (none, age, gender, and education), and combinations of covariates (single or fully adjusted based on age, gender, and education). Then we run the model, which in this case uses the lm_robust() function from the estimatr pacakge, extract its fixed effect estimates, and save both for later. By saving the results, it will make it easier and faster for us to extract the necessary model results later while still retaining all information from the original model.

ipd1b_mod_fun <- function(trait, outcome, type, mod, cov){
  ## load the data
  load(sprintf("%s/data/one_stage/%s_%s.RData", local_path, trait, outcome))
  
  ## model formula 
  if (cov == "all") cv <- c("age", "gender", "education")
  if (!cov %in% c("all", "none")) cv <- cov
  rhs <- "p_value"
  rhs <- if(cov != "none") c(rhs, cv) else rhs
  if(mod != "none"){rhs <- c(rhs, paste("p_value", mod, sep = "*"))}
  rhs <- paste(rhs, collapse = " + ")
  f <- paste("o_value ~ ", rhs, collapse = "")
  
  ## compiled Bayesian model to speed up processing and avoid crashing
  if(type == "Bayesian") load(sprintf("%s/results/1a_ipd_reg/bayes_sample_mod.RData", local_path))
  
  ## run the models & save
  m <- if(type == "Frequentist"){lm_robust(formula(f), data = d, clusters = study, se_type = "CR0")} else {update(m, formula = f, newdata = d)}
  save(m, file = sprintf("%s/results/1b_ipd_fixef/%s/models/%s_%s_%s_%s.RData"
                         , local_path, type, outcome, trait, mod, cov))
  
  ## extract model terms and confidence intervals & save
  fx <- tidy(m, conf.int = T) %>%
    select(term, estimate, conf.low, conf.high)
  save(fx, file = sprintf("%s/results/1b_ipd_fixef/%s/summary/%s_%s_%s_%s.RData"
                          , local_path, type, outcome, trait, mod, cov))
  
   ## get simple effects for moderator tests
  if(mod != "none"){
    pred.fx <- ipd1b_simpeff_fun(m, mod, type, d)
    save(pred.fx, file = sprintf("%s/results/1b_ipd_fixef/%s/predicted/%s_%s_%s_%s.RData", local_path, type, outcome, trait, mod, cov))
  }
  
  ## clean up the local function environment
  rm(list = c("d", "f", "rhs", "m", "fx"))
  gc()
}

3.2.2.3 Simple Effects Function

ipd1b_simpeff_fun <- function(m, moder, type, d){
  d <- if(type == "Bayesian") m$data else d %>% select(o_value, one_of(rownames(attr(m$terms, "factors")))) %>% filter(complete.cases(.)) %>% data.frame()
  d <- d %>% select(-o_value, -p_value)
  cols <- colnames(d)
  md_cl <- class(d[,moder])
  if(any(sapply(d, class) == "numeric")){
    msd <- d %>%
      select_if(is.numeric) %>%
      pivot_longer(everything()
                   , names_to = "item"
                   , values_to = "value") %>%
      group_by(item) %>%
      summarize_at(vars(value), lst(mean, sd), na.rm = T) %>%
      ungroup()
  }
  if(any(sapply(d, class) == "factor")){
    fct_lev <- d %>% 
      select_if(is.factor) %>%
      summarize_all(~list(unique(.)))
  }
  d <- d %>% select(-one_of(moder))
  md_levs <- if(md_cl == "numeric"){
    if(moder %in% c("age", "baseAge", "baseYear")) {
      c(-10, 0, 10)
      } else if (moder %in% c("predInt", "education")) {
      c(-5, 0, 5) 
      } else {
        with(msd, c(mean[item == moder] - sd[item == moder], mean[item == moder], mean[item == moder] + sd[item == moder]))
      }
  } else { 
    unique(fct_lev[,moder][[1]])
  }
  
  mod_frame <- crossing(
    p_value = seq(0,10,.5)
    , modvalue = md_levs
    ) %>% setNames(c("p_value", moder))
  
  if(ncol(d) > 0){
    if(any(sapply(d, class) == "numeric")){
      mod_frame <- tibble(mod_frame, d %>% select_if(is.numeric) %>% summarize_all(mean))
    } 
    if(any(sapply(d, class) == "factor")){
      mod_frame <- tibble(mod_frame, d %>% select_if(is.factor) %>% summarize_all(~levels(.)[1]))
    }
  }
  
  pred.fx <- if(type == "Bayesian"){
    bind_cols(
      mod_frame, 
      fitted(m, newdata = mod_frame) %>% data.frame
    ) %>%
      select(one_of(colnames(m$data)), pred = Estimate, lower = Q2.5, upper = Q97.5)
  } else {
    bind_cols(
      mod_frame, 
      predict(m, newdata = mod_frame, interval = "confidence")$fit %>% data.frame 
    ) %>%
      select(one_of(rownames(attr(m$terms, "factors"))), pred = fit, lower = lwr, upper = upr)
  }
  
  rm(list = c("m", "mod_frame", "d", "md_levs"))
  gc()
  return(pred.fx)
}

3.2.2.4 Run Models and Summaries

The code below generates all possible preregistrered combinations of traits, outcomes, types, moderators, and covariates. Then these combinations are fed serially to the model function written previously, which will run and save the results.

plan(multisession(workers = 12L))
nested_ipd1b_fixef <- 
  # moderator combinations 
  crossing(
    Trait = traits$short_name
    , Outcome = outcomes$short_name
    , type = c("Frequentist")
    , Moderator = c("age", "gender", "education")
    , Covariate = c("none", "all")
  ) %>%
  full_join(
    # unmoderated combinations 
    crossing(
      Trait = traits$short_name
      , Outcome = outcomes$short_name
      , type = c("Frequentist")
      , Moderator = c("none")
      , Covariate = c("none", "age", "gender", "education", "all")
      )
) %>%
  # filter(Trait == "N") %>%
  mutate(run = future_pmap(
    list(Trait, Outcome, type, Moderator, Covariate)
    , ipd1b_mod_fun
    , .progress = T
    , .options = furrr_options(
      globals = c("res_path", "local_path", "ipd1b_simpeff_fun", "traits", "studies", "outcomes", "covars", "moders")
      , packages = c("plyr", "tidyverse", "broom.mixed", "broom", "estimatr")
           )
    ))
closeAllConnections()

# nested_ipd1b_fixef %>%
#   # select(-done) %>%
#   write.table(.
#               , file = sprintf("%s/scripts/cluster/args/frequentist/ipd1b_frequentist.txt", local_path)
              # , row.names = F)

3.2.2.5 Compile Results

Once all the models are run, we are ready to compile all their results. By saving the fixed effects results previously, we are able to simply load those results and ignore the models. However, because we also saved the models, we can also recall and extract information from them if and when needed.

loadRData <- function(fileName, type, obj, folder){
#loads an RData file, and returns it
    path <- sprintf("%s/results/1b_ipd_fixef/%s/%s/%s", local_path, type, folder, fileName)
    load(path)
    get(ls()[grepl(obj, ls())])
}

## load in "fixed" effects
## first get file names
nested_ipd1b_fixef <- tibble(type = c("Frequentist", "Bayesian")) %>%
  mutate(file = map(type, ~list.files(sprintf("%s/results/1b_ipd_fixef/%s/summary", local_path, .)))) %>%
  unnest(file) %>%
  separate(file, c("Outcome", "Trait", "Moderator", "Covariate"), sep = "_", remove = F) %>% 
  ## read in the files
  mutate(Covariate = str_remove(Covariate, ".RData"),
         fx = map2(file, type, ~loadRData(.x, .y, "fx", "summary"))) %>%
  select(-file) 
3.2.2.5.1 Tables

Next, we want to format the study results in APA table format. In this case, we are interested in the fixed effects of personality predicting cognitive ability when there were no moderators, and the personality x moderator interaction when there was a moderator.

3.2.2.5.1.1 Fixed Effects
## format results 
ipd1b_fixef_tab <- nested_ipd1b_fixef %>%
  unnest(fx) %>%
  filter((Moderator == "none" & term == "p_value") |
         (Moderator != "none" & grepl("p_value:", term))) %>%
  mutate(sig = ifelse(sign(conf.low) == sign(conf.high), "sig", "ns"),
         Trait = factor(Trait, traits$short_name),
         Outcome = factor(Outcome, outcomes$short_name, outcomes$long_name),
         Moderator = factor(Moderator, c(moders$short_name, stdyModers$short_name), c(moders$long_name, stdyModers$long_name)),
         Covariate = ifelse(Moderator != "None" & Covariate == "None", Moderator, Covariate),
         Covariate = factor(Covariate, moders$short_name, str_wrap(moders$long_name, 15)),
         term = str_remove_all(term, "p_value:"),
         term = mapvalues(term, c("scaleBFIMS", "scaleIPIPNEO", "scaleTDAM40", "countryTheNetherlands")
                          , c("scaleBFI-S", "scaleIPIP NEO", "scaleTDA-40", "countryThe Netherlands")),
         term2 = factor(term, c(moders$short_term, stdyModers$short_term),
                       c(moders$long_term, stdyModers$long_term))) %>%
  mutate_at(vars(estimate, conf.low, conf.high), 
            ~ifelse(abs(.) < .001, sprintf("%.3f", .), sprintf("%.2f", .))) %>%
  mutate(est = sprintf("%s<br>[%s, %s]", estimate, conf.low, conf.high),
         est = ifelse(sig == "sig", sprintf("<strong>%s</strong>", est), est)) %>%
  # mutate(est = sprintf("%s [%s, %s]", estimate, conf.low, conf.high),
  # est = ifelse(sig == "sig", sprintf("\\textbf{%s}", est), est)) %>%
  select(-estimate, -conf.low, -conf.high, -sig, -term) %>%
  arrange(type, Outcome, Trait, Moderator, Covariate) %>%
  pivot_wider(names_from = "Trait", values_from = "est") 
ipd1b_fixef_tab
## # A tibble: 98 × 10
##    type     Outcome              Moderator Covariate      term2                  E                    A     C     N     O    
##    <chr>    <fct>                <fct>     <fct>          <fct>                  <chr>                <chr> <chr> <chr> <chr>
##  1 Bayesian Crystallized Ability None      None           Personality            <strong>-0.05<br>[-… <str… <str… <str… <str…
##  2 Bayesian Crystallized Ability None      Age            Personality            <strong>-0.05<br>[-… <str… <str… <str… <str…
##  3 Bayesian Crystallized Ability None      Gender         Personality            <strong>-0.06<br>[-… <str… <str… <str… <str…
##  4 Bayesian Crystallized Ability None      Education      Personality            <strong>-0.06<br>[-… -0.0… <str… <str… <str…
##  5 Bayesian Crystallized Ability None      Fully Adjusted Personality            <strong>-0.08<br>[-… <str… 0.01… <str… <str…
##  6 Bayesian Crystallized Ability Age       None           Age                    <strong>-0.00<br>[-… <str… -0.0… <str… <str…
##  7 Bayesian Crystallized Ability Age       Fully Adjusted Age                    <strong>-0.00<br>[-… <str… -0.0… <str… <str…
##  8 Bayesian Crystallized Ability Gender    None           Gender (Male v Female) <strong>0.09<br>[0.… 0.03… -0.0… -0.0… -0.0…
##  9 Bayesian Crystallized Ability Gender    Fully Adjusted Gender (Male v Female) <strong>0.09<br>[0.… 0.03… 0.00… -0.0… -0.0…
## 10 Bayesian Crystallized Ability Education None           Education (Years)      -0.001<br>[-0.01, 0… <str… <str… <str… -0.0…
## # ℹ 88 more rows
ipd1b_res <- nested_ipd1b_fixef %>%
  unnest(fx) %>%
  # keep key terms 
  filter((Moderator == "none" & term == "p_value") |
         (Moderator != "none" & grepl("p_value:", term))) %>%
  mutate(sig = ifelse(sign(conf.low) == sign(conf.high), "sig", "ns")) %>%
  mutate_at(vars(estimate, conf.low, conf.high), 
            ~ifelse(abs(.) < .01, sprintf("%.3f", .), sprintf("%.2f", .))) %>%
  mutate(est = sprintf("\\textit{b} = %s, 95\\%% CI [%s, %s]", estimate, conf.low, conf.high)) 

Now that we’ve formatted the values, we can group by moderators and save results as separate tables. Even though additional information could be included given that we have one outcome, we’ll stick with this split because it will make it easier for those using this tutorial who multiple traits, outcomes, covariates, and moderators.

## table function 
ipd1b_tab_fun <- function(d, type, moder){
  # long outcome name
  md <- mapvalues(moder, c("None", moders$long_name, stdyModers$long_name), c("none", moders$short_name, stdyModers$short_name), warn_missing = F)
  d <- d %>% arrange(Outcome, Covariate, term2)
  # getting row numbers for later grouping
  rs <- d %>% group_by(Outcome) %>% tally() %>% 
    mutate(end = cumsum(n), start = lag(end) + 1, start = ifelse(is.na(start), 1, start))
  cs <- if(length(unique(d$term2)) > 1) c(2, rep(1,5)) else rep(1,6)
  names(cs) <- c(" ", paste0("<strong>", traits$long_name, "</strong>"))
  # cln <- if(length(unique(d$term2)) == 1) c(" ", rep("\\textit{b} [CI]", 5)) else c(" ", "Term", rep("\\textit{b} [CI]", 5))
  cln <- if(length(unique(d$term2)) == 1) c(" ", rep("<em>b</em> [CI]", 5)) else c(" ", "Term", rep("<em>b</em> [CI]", 5))
  al <- if(length(unique(d$term2)) == 1) c("r", rep("c", 5)) else c("r", "r", rep("c", 5))
  if(length(unique(d$term2)) == 1) {
    d <- d %>% select(-term2); dubs <- F
  }
  cap <- if(md == "none") "1B Pooled Analysis of Individual Participant Data: Cluster Robust Fixed Effect Personality-Crystallized Domain Associations" else sprintf("1B Pooled Analysis of Individual Participant Data: Cluster Robust Fixed Effect %s Moderation of Personality-Crystallized Domain Associations", md)
  tab <- d %>%
    select(-Outcome) %>%
    kable(., "html"
    # kable(., "latex"
          , booktabs = T
          , escape = F
          , col.names = cln
          , align = al
          , caption = cap
    ) %>% 
    kable_classic(full_width = F, html_font = "Times New Roman") %>%
    # kable_styling(full_width = F, font_size = 7) %>%
    collapse_rows(1, "top") %>%
    add_header_above(cs, escape = F) 
  for (i in nrow(rs)){
    tab <- tab %>% 
      kableExtra::group_rows(rs$Outcome[i], rs$start[i], rs$end[i]) 
  }
  save_kable(tab, file = sprintf("%s/results/1b_ipd_fixef/%s/tables/overall/%s.html"
                                 , local_path, type, md))
  return(tab)
}

ipd1b_fx_tab <- ipd1b_fixef_tab %>%
  group_by(type, Moderator) %>%
  nest() %>%
  ungroup() %>%
  mutate(tab = pmap(list(data, type, Moderator), ipd1b_tab_fun))
ipd1b_fx_tab
## # A tibble: 14 × 4
##    type        Moderator           data              tab           
##    <chr>       <fct>               <list>            <list>        
##  1 Bayesian    None                <tibble [5 × 8]>  <kablExtr [1]>
##  2 Bayesian    Age                 <tibble [2 × 8]>  <kablExtr [1]>
##  3 Bayesian    Gender              <tibble [2 × 8]>  <kablExtr [1]>
##  4 Bayesian    Education           <tibble [2 × 8]>  <kablExtr [1]>
##  5 Frequentist None                <tibble [6 × 8]>  <kablExtr [1]>
##  6 Frequentist Age                 <tibble [2 × 8]>  <kablExtr [1]>
##  7 Frequentist Gender              <tibble [2 × 8]>  <kablExtr [1]>
##  8 Frequentist Education           <tibble [2 × 8]>  <kablExtr [1]>
##  9 Frequentist Continent           <tibble [10 × 8]> <kablExtr [1]>
## 10 Frequentist Country             <tibble [20 × 8]> <kablExtr [1]>
## 11 Frequentist Personality Scale   <tibble [30 × 8]> <kablExtr [1]>
## 12 Frequentist Baseline Age        <tibble [5 × 8]>  <kablExtr [1]>
## 13 Frequentist Baseline Year       <tibble [5 × 8]>  <kablExtr [1]>
## 14 Frequentist Prediction Interval <tibble [5 × 8]>  <kablExtr [1]>
# save(ipd1b_fixef_tab, ipd1b_res, file = sprintf("%s/manuscript/results/ipd1b_fx_tab.RData", local_path))

## Frequentist
(ipd1b_fx_tab %>% filter(type == "Frequentist" & Moderator == "None"))$tab[[1]]
Table 3.1: 1B Pooled Analysis of Individual Participant Data: Cluster Robust Fixed Effect Personality-Crystallized Domain Associations
Extraversion
Agreeableness
Conscientiousness
Neuroticism
Openness to Experience
b [CI] b [CI] b [CI] b [CI] b [CI]
Crystallized Ability
None -0.08
[-0.27, 0.10]
-0.14
[-0.36, 0.08]
0.01
[-0.17, 0.18]
-0.00
[-0.15, 0.14]
0.20
[0.06, 0.34]
Age -0.09
[-0.28, 0.11]
-0.14
[-0.38, 0.10]
0.01
[-0.17, 0.19]
0.00
[-0.15, 0.15]
0.19
[0.04, 0.34]
Gender -0.08
[-0.26, 0.11]
-0.14
[-0.37, 0.10]
0.02
[-0.16, 0.19]
0.01
[-0.14, 0.16]
0.19
[0.06, 0.32]
Self-Rated Physical Health 0.01
[-0.35, 0.36]
Education -0.07
[-0.22, 0.08]
-0.06
[-0.23, 0.10]
-0.01
[-0.15, 0.13]
-0.03
[-0.13, 0.08]
0.10
[-0.03, 0.24]
-0.07
[-0.22, 0.08]
-0.07
[-0.23, 0.08]
-0.01
[-0.14, 0.12]
-0.03
[-0.13, 0.08]
0.11
[-0.03, 0.25]
## bayesian
(ipd1b_fx_tab %>% filter(type == "Bayesian" & Moderator == "None"))$tab[[1]]
Table 3.1: 1B Pooled Analysis of Individual Participant Data: Cluster Robust Fixed Effect Personality-Crystallized Domain Associations
Extraversion
Agreeableness
Conscientiousness
Neuroticism
Openness to Experience
b [CI] b [CI] b [CI] b [CI] b [CI]
Crystallized Ability
None -0.05
[-0.07, -0.04]
-0.03
[-0.05, -0.01]
0.04
[0.02, 0.06]
0.11
[0.10, 0.13]
0.16
[0.14, 0.18]
-0.05
[-0.07, -0.04]
-0.03
[-0.05, -0.01]
0.04
[0.02, 0.06]
0.11
[0.10, 0.13]
0.16
[0.14, 0.18]
Gender -0.06
[-0.07, -0.04]
-0.03
[-0.05, -0.01]
0.04
[0.02, 0.06]
0.12
[0.10, 0.13]
0.16
[0.14, 0.18]
Education -0.06
[-0.08, -0.05]
-0.02
[-0.04, 0.000]
0.02
[0.00, 0.03]
0.07
[0.06, 0.08]
0.08
[0.06, 0.10]
Fully Adjusted -0.08
[-0.10, -0.07]
-0.05
[-0.07, -0.03]
0.01
[-0.01, 0.03]
0.15
[0.13, 0.16]
0.14
[0.13, 0.16]
ipd1b_tab_fun <- function(d, type, cov){
  # long outcome name
  covar <- mapvalues(cov, covars$long_name, covars$short_name, warn_missing = F)
  # getting row numbers for later grouping
  rs <- d %>% group_by(Moderator) %>% tally() %>% 
    mutate(end = cumsum(n), start = lag(end) + 1, start = ifelse(is.na(start), 1, start))
  # number and name of columns for span columns 
  cs <- rep(1,6)
  names(cs) <- c(" ", traits$long_name)
  # cln <- if(length(unique(d$term2)) == 1) c("Covariate", rep("\\textit{b} [CI]", 5)) else c(" ", "Term", rep("\\textit{b} [CI]", 5))
  cln <- c("Term", rep("<em>b</em> [CI]", 5))
  al <- c("r", rep("c", 5))
  # caption 
  cv <- if (cov == "None") "Unadjusted" else cov
  cap <- sprintf("1B Pooled Analysis of Individual Participant Data with Cluster Corrected Standard Errors: Fixed Effect Estimates of %s Personality-Crystallized Domain Associations", cv)
  # kable the table
  tab <- d %>%
    select(-Moderator) %>%
    kable(., "html"
    # kable(., "latex"
          , booktabs = T
          , escape = F
          , col.names = cln
          , align = al
          , caption = cap
    ) %>% 
    kable_classic(full_width = F, html_font = "Times New Roman") %>%
    # kable_styling(full_width = F, font_size = 7) %>%
    add_header_above(cs)
  # for loop to add grouped sections 
  for (i in 1:nrow(rs)){
    tab <- tab %>% 
      kableExtra::group_rows(rs$Moderator[i], rs$start[i], rs$end[i]) 
  }
  # save the resulting html table
  save_kable(tab, file = sprintf("%s/results/1b_ipd_fixef/%s/tables/key terms/%s.html"
                                 , local_path, type, covar))
  return(tab) # return the html table
}

ipd1b_fx_tab2 <- ipd1b_fixef_tab %>%
  filter(!Moderator %in% stdyModers$long_name) %>%
  arrange(Moderator, term2) %>%
  filter(Covariate %in% c("None", "Fully Adjusted")) %>%
  group_by(Outcome, type, Covariate) %>% 
  nest() %>%
  ungroup() %>%
  mutate(tab = pmap(list(data, type, Covariate), ipd1b_tab_fun))


## Frequentist, no moderator
(ipd1b_fx_tab2 %>% filter(type == "Frequentist" & Covariate == "Fully Adjusted"))$tab[[1]]
Table 3.2: 1B Pooled Analysis of Individual Participant Data with Cluster Corrected Standard Errors: Fixed Effect Estimates of Fully Adjusted Personality-Crystallized Domain Associations
Extraversion
Agreeableness
Conscientiousness
Neuroticism
Openness to Experience
Term b [CI] b [CI] b [CI] b [CI] b [CI]
None
Personality -0.07
[-0.22, 0.08]
-0.07
[-0.23, 0.08]
-0.01
[-0.14, 0.12]
-0.03
[-0.13, 0.08]
0.11
[-0.03, 0.25]
Age
Age 0.000
[-0.00, 0.00]
-0.000
[-0.00, 0.00]
-0.001
[-0.00, 0.00]
0.00
[-0.00, 0.00]
-0.00
[-0.01, 0.001]
Gender
Gender (Male v Female) 0.05
[0.001, 0.10]
0.04
[-0.00, 0.08]
0.03
[-0.07, 0.13]
-0.01
[-0.08, 0.05]
-0.02
[-0.08, 0.04]
Education
Education (Years) 0.00
[-0.01, 0.02]
-0.00
[-0.03, 0.02]
-0.01
[-0.03, 0.02]
-0.00
[-0.02, 0.01]
0.00
[-0.01, 0.01]
## bayesian
(ipd1b_fx_tab2 %>% filter(type == "Bayesian" & Covariate == "Fully Adjusted"))$tab[[1]]
Table 3.2: 1B Pooled Analysis of Individual Participant Data with Cluster Corrected Standard Errors: Fixed Effect Estimates of Fully Adjusted Personality-Crystallized Domain Associations
Extraversion
Agreeableness
Conscientiousness
Neuroticism
Openness to Experience
Term b [CI] b [CI] b [CI] b [CI] b [CI]
None
Personality -0.08
[-0.10, -0.07]
-0.05
[-0.07, -0.03]
0.01
[-0.01, 0.03]
0.15
[0.13, 0.16]
0.14
[0.13, 0.16]
Age
Age -0.00
[-0.00, -0.000]
-0.00
[-0.00, -0.000]
-0.000
[-0.00, 0.00]
0.00
[0.00, 0.00]
-0.00
[-0.00, -0.000]
Gender
Gender (Male v Female) 0.09
[0.05, 0.12]
0.03
[-0.01, 0.08]
0.000
[-0.04, 0.04]
-0.02
[-0.04, 0.01]
-0.03
[-0.07, 0.00]
Education
Education (Years) -0.01
[-0.01, -0.000]
-0.02
[-0.03, -0.01]
-0.02
[-0.02, -0.01]
0.01
[0.01, 0.01]
-0.00
[-0.01, 0.00]
# save(ipd1a_fx_tab, ipd1a_fx_tab2, ipd1a_res, file = sprintf("%s/manuscript/results/ipd1b_fx_tab.RData", local_path))
3.2.2.5.1.2 Study-Specific Effects

This header is here to simply emphasize that this method does not provide study-specific estimates, unlike Methods 2ab, 3, and 4.

3.2.2.5.1.3 Heterogeneity Estimates

This header is here to simply emphasize that this method does not provide heterogeneity estimates because it does not provide study-specific estimates, unlike Methods 2ab, 3, and 4.

3.2.2.5.1.4 All Model Terms
ipd1b_mod_tab <- nested_ipd1b_fixef %>%
  unnest(fx) %>%
  # keep key terms 
  # mark significance and prettify trait, outcome, and covariate names
  mutate(sig = ifelse(sign(conf.low) == sign(conf.high), "sig", "ns"),
         Trait = factor(Trait, traits$short_name),
         Outcome = factor(Outcome, outcomes$short_name, outcomes$long_name),
         Moderator = factor(Moderator, c(moders$short_name, stdyModers$short_name), c(moders$long_name, stdyModers$long_name)),
         Covariate = factor(Covariate, covars$short_name, str_wrap(covars$long_name, 15))) %>%
  # format values as text, combine estimates and CI's, bold significance
  mutate_at(vars(estimate, conf.low, conf.high), 
            ~ifelse(abs(.) < .01, sprintf("%.3f", .), sprintf("%.2f", .))) %>%
  mutate(est = sprintf("%s<br>[%s, %s]", estimate, conf.low, conf.high),
         est = ifelse(sig == "sig", sprintf("<strong>%s</strong>", est), est)) %>%
  # mutate(est = sprintf("%s [%s, %s]", estimate, conf.low, conf.high),
  # est = ifelse(sig == "sig", sprintf("\\textbf{%s}", est), est)) %>%
  # final reshaping, remove extra columns, arrange values, and change to wide format
  select(-estimate, -conf.low, -conf.high, -sig) %>%
  arrange(type, Outcome, Trait, Moderator, Covariate) %>%
  pivot_wider(names_from = "Trait", values_from = "est") 

ipd1a_mod_tab_fun <- function(d, type, out, moder, cov){
  md <- mapvalues(moder, c(moders$long_name, stdyModers$long_name), c(moders$short_name, stdyModers$short_name), warn_missing = F)
  o <- mapvalues(out, outcomes$long_name, outcomes$short_name, warn_missing = F)
  cv <- mapvalues(cov, covars$long_name, covars$short_name, warn_missing = F)
  cs <- rep(1,6)
  names(cs) <- c(" ", paste0("<strong>", traits$long_name, "</strong>"))
  # cln <- if(length(unique(d$term2)) == 1) c("Covariate", rep("\\textit{b} [CI]", 5)) else c(" ", "Term", rep("\\textit{b} [CI]", 5))
  cln <- c("Term", rep("<em>b</em> [CI]", 5))
  al <- c("r", rep("c", 5))
  # caption 
  cap <- if(md == "none") "1B Pooled Analysis of Individual Participant Data with Cluster Corrected Standard Errors: All Model Estimates of Fixed Effect Personality-Crystallized Domain Associations" else sprintf("1B Pooled Analysis of Individual Participant Data with Cluster Corrected Standard Errors: All Model Estimates of Fixed Effect %s Moderation of Personality-Crystallized Domain Associations", md)
  
  # kable the table
  tab <- d %>%
    arrange(term) %>%
    kable(., "html"
    # kable(., "latex"
          , booktabs = T
          , escape = F
          , col.names = cln
          , align = al
          , caption = cap
    ) %>% 
    kable_classic(full_width = F, html_font = "Times New Roman") %>%
    # kable_styling(full_width = F, font_size = 7) %>%
    add_header_above(cs, escape = F)
  # save the resulting html table
  save_kable(tab, file = sprintf("%s/results/1b_ipd_fixef/%s/tables/all terms/%s-%s-%s.html"
                                 , local_path, type, o, md, cv))
  return(tab) # return the html table
}

ipd1b_mod_tab <- ipd1b_mod_tab %>%
  group_by(type, Outcome, Moderator, Covariate) %>%
  nest() %>%
  ungroup() %>%
  mutate(tab = pmap(list(data, type, Outcome, Moderator, Covariate), ipd1a_mod_tab_fun))
3.2.2.5.2 Figures
3.2.2.5.2.1 Overall Forest
ipd1b_fx_plot_fun <- function(df, mod, type, cov){
  m <- mapvalues(mod, c(moders$short_name, stdyModers$short_name), c(moders$long_name, stdyModers$long_name), warn_missing = F)
  cv <- mapvalues(cov, covars$short_name, covars$long_name, warn_missing = F)
  d <- round(max(abs(min(df$estimate)), abs(max(df$estimate))), 3)
  # stds <- unique(df$study)
  lim <- c(0-d-(d/2.5), 0+d+(d/2.5))
  brk <- if(d > .01) round(c(0-d-(d/5), 0, 0+d+(d/5)),2) else round(c(0-d-(d/5), 0, 0+d+(d/5)),3) 
  # lim_high <- lim[2]*4
  lab <- str_replace(brk, "^0.", ".")#str_remove(round(c(0-d-(d/5), 0, 0+d+(d/5)),2), "^0")
  shapes <- c(15, 16, 17, 18)[1:length(unique(df$term))]
  lt <- rep("solid", length(unique(df$term)))
  titl <- if(mod == "none"){NULL} else {sprintf("%s Moderation of Personality-Outcome Associations", m)}
  titl <- if(!cov %in% c("none", "all")) paste(cv, "Adjusted", titl, collapse = " ") else paste(cv, titl, collapse = " ")
  leg <- if(length(unique(df$term)) > 1){"bottom"} else {"none"}
  p <- df %>%
    mutate(conf.low = ifelse(conf.low < lim[1], lim[1], conf.low),
           conf.high = ifelse(conf.high > lim[2], lim[2], conf.high)) %>% 
  ggplot(aes(x = Outcome, y = estimate)) +
    scale_y_continuous(limits = lim, breaks = brk, labels = lab) + 
    scale_size_manual(values = c(1.4, 1.8)) +
    scale_shape_manual(values = shapes) +
    scale_color_manual(values = c("black", "blue")) +
    scale_linetype_manual(values = lt) +
    geom_hline(aes(yintercept = 0), size = .25, color = "gray50") +
    geom_errorbar(aes(ymin = conf.low, ymax = conf.high, linetype = term)
                  , width = 0
                  , position = position_dodge(width = .9)) + 
    geom_point(aes(color = sig, size = sig, shape = term)
                  , position = position_dodge(width = .9)) +
    labs(x = NULL
         , y = "Estimate (POMP)"
         , title = titl
         , subtitle = "Method 1B: Pooled Linear Regression with Cluster-Corrected Standard Errors"
         ) +
    guides(color = "none", size = "none") +
    facet_grid(~Trait, scales = "free_y", space = "free") +
    coord_flip() +
    theme_classic() +
    theme(legend.position = leg,
          plot.title = element_text(face = "bold", size = rel(1.2), hjust = .5),
          plot.subtitle = element_text(hjust = .5),
          panel.background = element_rect(color = "black", fill = "white"),
          strip.background = element_blank(),
          strip.text = element_text(face = "bold", color = "black", size = rel(1.4)),
          axis.text = element_text(color = "black"),
          axis.text.y = element_text(size = rel(1)))
  ht <- length(unique(df$Outcome)); ht2 <- length(unique(df$term))
  wdt <- length(unique(df$Trait))
ggsave(file = sprintf("%s/results/1b_ipd_fixef/%s/figures/overall forest/%s_%s_fixed.png", local_path, type, mod, cov), width = wdt*2, height = ht+ht2)
rm(p)
gc()
return(T)
}

nested_ipd1b_fixef %>%
  unnest(fx) %>%
    filter((Moderator == "none" & term == "p_value")|
          (Moderator != "none" & grepl("p_value:", term))) %>%
    mutate(sig = ifelse(sign(conf.low) == sign(conf.high), "sig", "ns"),
           sig = factor(sig, levels = c("sig","ns")),
           Trait = factor(Trait, levels = traits$short_name),
           Outcome = factor(Outcome, levels = outcomes$short_name, labels = str_wrap(outcomes$long_name, 15)),
           Outcome = forcats::fct_rev(Outcome))  %>%
  group_by(type, Moderator, Covariate) %>%
  nest() %>%
  ungroup() %>%
  mutate(pmap(list(data, Moderator, type, Covariate), ipd1b_fx_plot_fun))
3.2.2.5.2.2 Study-Specific Forest Plots

This header is here to simply emphasize that this method does not provide study-specific estimates, unlike Methods 2ab, 3, and 4.

3.2.2.5.2.3 Overall Simple Effects Plots
loadRData <- function(fileName, type, obj, folder){
#loads an RData file, and returns it
    path <- sprintf("%s/results/1b_ipd_fixef/%s/%s/%s", local_path, type, folder, fileName)
    load(path)
    get(ls()[grepl(obj, ls())])
}

## load in "fixed" effects
## first get file names
nested_ipd1b_simp <- tibble(type = c("Frequentist", "Bayesian")) %>%
  mutate(file = map(type, ~list.files(sprintf("%s/results/1b_ipd_fixef/%s/predicted", local_path, .)))) %>%
  unnest(file) %>%
  separate(file, c("Outcome", "Trait", "Moderator", "Covariate"), sep = "_", remove = F) %>% 
  ## read in the files
  mutate(Covariate = str_remove(Covariate, ".RData"),
         pred = map2(file, type, ~loadRData(.x, .y, "pred.fx", "predicted"))) %>%
  select(-file) 
nested_ipd1a_simp
simp_eff_fun <- function(df, outcome, mod, type, cov){ 
  print(paste(outcome, mod))
  o <- mapvalues(outcome, outcomes$short_name, outcomes$long_name, warn_missing = F)
  cv <- mapvalues(cov, covars$short_name, covars$long_name, warn_missing = F)
  m <- mapvalues(mod, moders$short_name, moders$long_name, warn_missing = F)
  d <- round(max(abs(min(df$pred)), abs(max(df$pred))), 3)
  mini <- if(d > 2) .05 else 0-(d+(d/5))
  maxi <- if(d > 2) 2.05 else 0+d+(d/5)
  lim <- c(mini, maxi)
  brk <- if(d > 2) c(0, 1, 2) else{round(c(0-d-(d/10), 0, 0+d+(d/10)),2)}
  lab <- if(d > 2){c("0", "1", "2")} else{str_remove(c(round(0-d-(d/10),2), 0, round(0+d+(d/10),2)), "^0")}
  titl <- if(mod == "none"){o} else {sprintf("%s: Personality x %s Simple Effects", o, m)}
  # colnames(df)[colnames(df) == mod] <- "mod_value"
  df <- df %>% unclass %>% data.frame
  df$mod_value <- df[,mod]
  df <- df %>% select(-all_of(mod)) %>% as_tibble
  if(class(df$mod_value) == "factor"){df <- df %>% mutate(mod_fac = factor(mod_value))} 
  else{df <- df %>%
    group_by(Trait) %>%
    mutate(mod_fac = factor(mod_value, levels = unique(mod_value), labels = c("-1 SD", "M", "+1 SD"))) %>%
    ungroup()
  }
  lt <- c("dotted", "solid", "dashed", "dotdash", "longdash", "twodash")[1:length(unique(df$mod_fac))]
  
  df %>%
    mutate(Trait = factor(Trait, levels = traits$short_name, labels = traits$long_name),
           lower = ifelse(lower < 4, 4, lower),
           upper = ifelse(upper > 10, 10, upper)) %>%
    ggplot(aes(x = p_value
               , y = pred
               , group  = mod_fac))  +
      scale_y_continuous(limits = c(4,10)
                         , breaks = c(4, 6, 8, 10)
                         , labels = c(4, 6, 8, 10)) +
      scale_linetype_manual(values = lt) +
      geom_ribbon(aes(ymin = lower
                      , ymax = upper
                      , fill = mod_fac)
                  , alpha = .25) +
      geom_line(aes(linetype = mod_fac)) +
      labs(x = "Personality (POMP)"
           , y = paste(o, "(POMP)")
           , title = titl
           , linetype = m
           , fill = m
           , subtitle = "Method 1B: Pooled Linear Regression with Cluster-Corrected Standard Errors") +
      facet_wrap(~Trait, nrow = 3) +
      theme_classic() +
      theme(legend.position = "bottom"
            , plot.title = element_text(face = "bold", size = rel(1.2), hjust = .5)
            , strip.background = element_rect(fill = "black")
            , strip.text = element_text(face = "bold", color = "white")
            , axis.text = element_text(color = "black"))
  ggsave(file = sprintf("%s/results/1b_ipd_fixef/%s/figures/overall simple effects/%s_%s_%s.png", local_path, type, outcome, mod, cov), width = 6, height = 6)
}

ipd1b_se_plot <- nested_ipd1b_simp %>%
  group_by(type, Outcome, Moderator, Covariate) %>%
  nest() %>%
  ungroup() %>%
  mutate(data = map(data, ~(.) %>% unnest(pred)),
         plot = pmap(list(data, Outcome, Moderator, type, Covariate), simp_eff_fun))
3.2.2.5.2.4 Study-Specific Simple Effects Plots

This header is here to simply emphasize that this method does not provide study-specific estimates, unlike Methods 2ab, 3, and 4.

3.2.2.6 Sample Results Section

We examined estimates of overall prospective associations between Big Five personality characteristics and crystallized abilities as well as participant-level moderators of those associations using fully pooled, one-stage regression models with cluster-corrected standard errors using individual participant data in 11 studies. This allowed us to correct for clustering of data within studies without estimating sample-specific estimates. Table S2 presents the estimates of the key terms from all unmoderated and participant and sample-level moderators estimates from fully adjusted models. Across the 20 participant-level personality-cognitive ability associations and moderators of those associations, none were significant, including unmoderated associations between personality and crystallized abilities.

Table 3.2: 1B Pooled Analysis of Individual Participant Data with Cluster Corrected Standard Errors: Fixed Effect Estimates of Fully Adjusted Personality-Crystallized Domain Associations
Extraversion
Agreeableness
Conscientiousness
Neuroticism
Openness to Experience
Term b [CI] b [CI] b [CI] b [CI] b [CI]
None
Personality -0.07
[-0.22, 0.08]
-0.07
[-0.23, 0.08]
-0.01
[-0.14, 0.12]
-0.03
[-0.13, 0.08]
0.11
[-0.03, 0.25]
Age
Age 0.000
[-0.00, 0.00]
-0.000
[-0.00, 0.00]
-0.001
[-0.00, 0.00]
0.00
[-0.00, 0.00]
-0.00
[-0.01, 0.001]
Gender
Gender (Male v Female) 0.05
[0.001, 0.10]
0.04
[-0.00, 0.08]
0.03
[-0.07, 0.13]
-0.01
[-0.08, 0.05]
-0.02
[-0.08, 0.04]
Education
Education (Years) 0.00
[-0.01, 0.02]
-0.00
[-0.03, 0.02]
-0.01
[-0.03, 0.02]
-0.00
[-0.02, 0.01]
0.00
[-0.01, 0.01]

The simple effects plots shown in Figure S2 provide a visualization to help better understand the moderators. The figure displays the predicted crystallized ability levels at different levels of the Big Five for males and females. Gender moderated the relationship between extraversion and later crystallized abilities (b = 0.06, 95% CI [0.001, 0.12]). The overall association between personality traits and later crystallized ability was null for extraversion for men (b = -0.01, 95% CI [-0.06, 0.03]) but positive for women (b = 0.05, 95% CI [0.004, 0.09]), such that women who were higher in extraversion tended to score higher on crystallized domain tasks. All other simple effects are available in the online materials and in the web app.

Figure S2. Simple effects of the prospective association between Big Five personality characteristics (rows) and crystallized abilities across gender (male, female) using all methods of data synthesis (columns). The x-axis captures personality in percentage of maximum possible (POMP) units, the y-axis captures crystallized abilities in POMP, and different panels represent each of the Big Five characteristics. Different lines indicate the predicted levels of cognitive ability for males (dotted line) and females (solid line).

Figure 3.2: Figure S2. Simple effects of the prospective association between Big Five personality characteristics (rows) and crystallized abilities across gender (male, female) using all methods of data synthesis (columns). The x-axis captures personality in percentage of maximum possible (POMP) units, the y-axis captures crystallized abilities in POMP, and different panels represent each of the Big Five characteristics. Different lines indicate the predicted levels of cognitive ability for males (dotted line) and females (solid line).

rm(list = ls()[grepl("ipd1b", ls())])