--- title: "Estimating trial-level effects" output: rmarkdown::html_vignette bibliography: citations.bib csl: apa.csl link-citations: true vignette: > %\VignetteIndexEntry{Estimating trial-level effects} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ## Introduction By default, the `hmetad` package uses aggregated data (i.e., counts of the number of trials with the same stimulus, type 1 response, and type 2 response.) This is because data aggregation makes model fitting and simulation much more efficient. But sometimes researchers will be interested in trial-level effects. One common example would be what is often called "crossed random effects". For example, in a design where all participants make responses to the same set of items, a researcher might want to estimate both participant-level and item-level effects on their model parameters. We can simulate data from such a design like so: ``` r library(tidyverse) library(tidybayes) library(hmetad) ## average model parameters K <- 3 ## number of confidence levels mu_log_M <- -0.5 mu_dprime <- 1.5 mu_c <- 0 mu_c2_0 <- rep(-1, K - 1) mu_c2_1 <- rep(-1, K - 1) ## participant-level standard deviations sd_log_M_participant <- 0.25 sd_dprime_participant <- 0.5 sd_c_participant <- 0.33 sd_c2_0_participant <- cov_matrix(rep(0.25, K - 1), diag(K - 1)) sd_c2_1_participant <- cov_matrix(rep(0.25, K - 1), diag(K - 1)) ## item-level standard deviations sd_log_M_item <- 0.1 sd_dprime_item <- 0.5 sd_c_item <- 0.75 sd_c2_0_item <- cov_matrix(rep(0.1, K - 1), diag(K - 1)) sd_c2_1_item <- cov_matrix(rep(0.1, K - 1), diag(K - 1)) ## simulate data d <- expand_grid( participant = 1:50, item = 1:25 ) |> ## simulate participant-level differences group_by(participant) |> mutate( z_log_M_participant = rnorm(1, sd = sd_log_M_participant), z_dprime_participant = rnorm(1, sd = sd_dprime_participant), z_c_participant = rnorm(1, sd = sd_c_participant), z_c2_0_participant = list(rmulti_normal(1, mu = rep(0, K - 1), Sigma = sd_c2_0_participant)), z_c2_1_participant = list(rmulti_normal(1, mu = rep(0, K - 1), Sigma = sd_c2_1_participant)) ) |> ## simulate item-level differences group_by(item) |> mutate( z_log_M_item = rnorm(1, sd = sd_log_M_item), z_dprime_item = rnorm(1, sd = sd_dprime_item), z_c_item = rnorm(1, sd = sd_c_item), z_c2_0_item = list(rmulti_normal(1, mu = rep(0, K - 1), Sigma = sd_c2_0_item)), z_c2_1_item = list(rmulti_normal(1, mu = rep(0, K - 1), Sigma = sd_c2_1_item)) ) |> ungroup() |> ## compute model parameters mutate( log_M = mu_log_M + z_log_M_participant + z_log_M_item, dprime = mu_dprime + z_dprime_participant + z_dprime_item, c = mu_c + z_c_participant + z_c_item, c2_0_diff = map2( z_c2_0_participant, z_c2_0_item, ~ exp(mu_c2_0 + .x + .y) ), c2_1_diff = map2( z_c2_1_participant, z_c2_1_item, ~ exp(mu_c2_1 + .x + .y) ) ) |> ## simulate two trials per participant/item (stimulus = 0 and stimulus = 1) mutate(trial = pmap(list(dprime, c, log_M, c2_0_diff, c2_1_diff), sim_metad, N_trials = 2)) |> select(participant, item, trial) |> unnest(trial) ``` ``` #> # A tibble: 2,500 × 16 #> participant item trial stimulus response correct confidence dprime c meta_dprime M meta_c2_0 #> #> 1 1 1 1 0 1 0 3 0.725 -0.843 0.614 0.847 #> 2 1 1 1 1 1 1 3 0.725 -0.843 0.614 0.847 #> 3 1 2 1 0 0 1 2 1.74 -1.24 1.12 0.642 #> 4 1 2 1 1 1 1 3 1.74 -1.24 1.12 0.642 #> 5 1 3 1 0 0 1 3 0.746 0.614 0.565 0.758 #> 6 1 3 1 1 0 0 2 0.746 0.614 0.565 0.758 #> 7 1 4 1 0 0 1 2 1.50 -0.0110 1.21 0.809 #> 8 1 4 1 1 1 1 3 1.50 -0.0110 1.21 0.809 #> 9 1 5 1 0 0 1 1 1.21 -0.231 1.13 0.931 #> 10 1 5 1 1 1 1 1 1.21 -0.231 1.13 0.931 #> # ℹ 2,490 more rows #> # ℹ 4 more variables: meta_c2_1 , theta , theta_1 , theta_2 ``` Don't worry about the details of the simulation code- what matters is that we have a data set with repeated measures for participants: ``` r count(d, participant) #> # A tibble: 50 × 2 #> participant n #> #> 1 1 50 #> 2 2 50 #> 3 3 50 #> 4 4 50 #> 5 5 50 #> 6 6 50 #> 7 7 50 #> 8 8 50 #> 9 9 50 #> 10 10 50 #> # ℹ 40 more rows ``` And repeated measures for items: ``` r count(d, item) #> # A tibble: 25 × 2 #> item n #> #> 1 1 100 #> 2 2 100 #> 3 3 100 #> 4 4 100 #> 5 5 100 #> 6 6 100 #> 7 7 100 #> 8 8 100 #> 9 9 100 #> 10 10 100 #> # ℹ 15 more rows ``` ## Standard model with data aggregation If we would like, we can use the `fit_metad` function on this data with participant-level and item-level effects. However, if we aggregate the data ourselves, we can see that the aggregation doesn't really help us here: ``` r aggregate_metad(d, participant, item) #> `hmetad` has inferred that there are K=3 confidence levels in the data. If this is incorrect, please set this manually using the argument `K=` #> # A tibble: 1,250 × 5 #> participant item N_0 N_1 N[,"N_0_1"] [,"N_0_2"] [,"N_0_3"] [,"N_0_4"] [,"N_0_5"] [,"N_0_6"] [,"N_1_1"] #> #> 1 1 1 1 1 0 0 0 0 0 1 0 #> 2 1 2 1 1 0 1 0 0 0 0 0 #> 3 1 3 1 1 1 0 0 0 0 0 0 #> 4 1 4 1 1 0 1 0 0 0 0 0 #> 5 1 5 1 1 0 0 1 0 0 0 0 #> 6 1 6 1 1 1 0 0 0 0 0 0 #> 7 1 7 1 1 0 0 0 0 1 0 0 #> 8 1 8 1 1 1 0 0 0 0 0 0 #> 9 1 9 1 1 1 0 0 0 0 0 0 #> 10 1 10 1 1 1 0 0 0 0 0 0 #> # ℹ 1,240 more rows #> # ℹ 1 more variable: N[8:12] ``` As you can see, the aggregated data set has 1250 rows (with two observations per row), which is not much smaller than the trial-level data that we started with! So, in this case, it will probably be easier *not* to aggregate our data. Nevertheless, there is nothing stopping us from fitting the model like normal:^[Note that in practice, fitting hierarchical models will usually require setting informed priors and adjusting the Stan sampler settings.] ``` r # Priors are chosen arbitrarily for this example. # Please choose your own wisely! priors <- prior(normal(0, .25), class = Intercept) + set_prior( "normal(0, .25)", class = "Intercept", dpar = c("dprime", "c") ) + set_prior("normal(-0.5, .1)", class = "Intercept", dpar = metac2_parameters(K = 3)) + prior(normal(0, 1), class = sd) + set_prior("normal(0, 1)", class = "sd", dpar = c("dprime", "c", metac2_parameters(K = 3))) m.multinomial <- fit_metad( bf( N ~ 1 + (1 | participant) + (1 | item), dprime + c + metac2zero1diff + metac2zero2diff + metac2one1diff + metac2one2diff ~ 1 + (1 | participant) + (1 | item) ), data = d, init = 0, cores = 4, prior = priors ) #> `hmetad` has inferred that there are K=3 confidence levels in the data. If this is incorrect, please set this manually using the argument `K=` #> Compiling Stan program... #> Start sampling #> Warning: Bulk Effective Samples Size (ESS) is too low, indicating posterior means and medians may be unreliable. #> Running the chains for more iterations may help. See #> https://mc-stan.org/misc/warnings.html#bulk-ess ``` ``` #> Family: metad__3__normal__absolute__multinomial #> Links: mu = log; dprime = identity; c = identity; metac2zero1diff = log; metac2zero2diff = log; metac2one1diff = log; metac2one2diff = log #> Formula: N ~ 1 + (1 | participant) + (1 | item) #> dprime ~ 1 + (1 | participant) + (1 | item) #> c ~ 1 + (1 | participant) + (1 | item) #> metac2zero1diff ~ 1 + (1 | participant) + (1 | item) #> metac2zero2diff ~ 1 + (1 | participant) + (1 | item) #> metac2one1diff ~ 1 + (1 | participant) + (1 | item) #> metac2one2diff ~ 1 + (1 | participant) + (1 | item) #> Data: data.aggregated (Number of observations: 1250) #> Draws: 4 chains, each with iter = 2000; warmup = 1000; thin = 1; #> total post-warmup draws = 4000 #> #> Multilevel Hyperparameters: #> ~item (Number of levels: 25) #> Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS #> sd(Intercept) 0.14 0.10 0.01 0.37 1.00 1074 1538 #> sd(dprime_Intercept) 0.57 0.14 0.35 0.88 1.00 757 1379 #> sd(c_Intercept) 0.76 0.12 0.57 1.03 1.00 614 1061 #> sd(metac2zero1diff_Intercept) 0.07 0.05 0.00 0.20 1.00 1876 1525 #> sd(metac2zero2diff_Intercept) 0.13 0.09 0.01 0.32 1.00 927 1399 #> sd(metac2one1diff_Intercept) 0.11 0.08 0.00 0.31 1.00 1085 1964 #> sd(metac2one2diff_Intercept) 0.18 0.10 0.01 0.41 1.00 921 1546 #> #> ~participant (Number of levels: 50) #> Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS #> sd(Intercept) 0.12 0.09 0.01 0.32 1.00 1247 1931 #> sd(dprime_Intercept) 0.44 0.08 0.29 0.62 1.00 1200 2111 #> sd(c_Intercept) 0.35 0.05 0.27 0.45 1.00 1329 2091 #> sd(metac2zero1diff_Intercept) 0.19 0.10 0.02 0.38 1.01 717 955 #> sd(metac2zero2diff_Intercept) 0.30 0.10 0.11 0.49 1.01 1012 1093 #> sd(metac2one1diff_Intercept) 0.20 0.10 0.02 0.41 1.01 815 1241 #> sd(metac2one2diff_Intercept) 0.36 0.10 0.15 0.56 1.00 887 716 #> #> Regression Coefficients: #> Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS #> Intercept -0.23 0.09 -0.41 -0.07 1.00 3386 2946 #> dprime_Intercept 1.10 0.15 0.77 1.36 1.00 930 1255 #> c_Intercept 0.15 0.14 -0.12 0.42 1.02 275 496 #> metac2zero1diff_Intercept -0.83 0.06 -0.93 -0.72 1.00 3284 2931 #> metac2zero2diff_Intercept -0.79 0.06 -0.91 -0.67 1.00 2643 2965 #> metac2one1diff_Intercept -0.84 0.06 -0.95 -0.71 1.00 3121 2733 #> metac2one2diff_Intercept -0.79 0.07 -0.92 -0.65 1.00 2296 2603 #> #> Draws were sampled using sampling(NUTS). For each parameter, Bulk_ESS #> and Tail_ESS are effective sample size measures, and Rhat is the potential #> scale reduction factor on split chains (at convergence, Rhat = 1). ``` ## Data preparation Fitting the trial-level model does not require data aggregation, however it still requires a small amount of data preparation. To fit the model, we will need two things: * a column with the stimulus per trial (`0` or `1`), and * a column containing the joint type 1/type 2 responses per trial (between `1` and `2*K`). Our data already has a `stimulus` column but separate columns for the two responses. So, we can add in a joint response column now: ``` r d <- d |> mutate(joint_response = joint_response(response, confidence, K)) |> relocate(joint_response, .after = "stimulus") ``` ``` #> # A tibble: 2,500 × 17 #> participant item trial stimulus joint_response response correct confidence dprime c meta_dprime M #> #> 1 1 1 1 0 6 1 0 3 0.725 -0.843 0.614 0.847 #> 2 1 1 1 1 6 1 1 3 0.725 -0.843 0.614 0.847 #> 3 1 2 1 0 2 0 1 2 1.74 -1.24 1.12 0.642 #> 4 1 2 1 1 6 1 1 3 1.74 -1.24 1.12 0.642 #> 5 1 3 1 0 1 0 1 3 0.746 0.614 0.565 0.758 #> 6 1 3 1 1 2 0 0 2 0.746 0.614 0.565 0.758 #> 7 1 4 1 0 2 0 1 2 1.50 -0.0110 1.21 0.809 #> 8 1 4 1 1 6 1 1 3 1.50 -0.0110 1.21 0.809 #> 9 1 5 1 0 3 0 1 1 1.21 -0.231 1.13 0.931 #> 10 1 5 1 1 4 1 1 1 1.21 -0.231 1.13 0.931 #> # ℹ 2,490 more rows #> # ℹ 5 more variables: meta_c2_0 , meta_c2_1 , theta , theta_1 , theta_2 ``` ## Model fitting Now that we have our data, we can fit the trial-level model using `joint_response` as our response variable, `stimulus` as an extra variable passed to `brms`, and the argument `categorical=TRUE` to tell `fit_metad` not to aggregate the data: ``` r m.categorical <- fit_metad( bf( joint_response | vint(stimulus) ~ 1 + (1 | participant) + (1 | item), dprime + c + metac2zero1diff + metac2zero2diff + metac2one1diff + metac2one2diff ~ 1 + (1 | participant) + (1 | item) ), data = d, categorical = TRUE, init = 0, cores = 4, prior = priors ) #> `hmetad` has inferred that there are K=3 confidence levels in the data. If this is incorrect, please set this manually using the argument `K=` #> Compiling Stan program... #> Start sampling ``` ``` #> Family: metad__3__normal__absolute__categorical #> Links: mu = log; dprime = identity; c = identity; metac2zero1diff = log; metac2zero2diff = log; metac2one1diff = log; metac2one2diff = log #> Formula: joint_response | vint(stimulus) ~ 1 + (1 | participant) + (1 | item) #> dprime ~ 1 + (1 | participant) + (1 | item) #> c ~ 1 + (1 | participant) + (1 | item) #> metac2zero1diff ~ 1 + (1 | participant) + (1 | item) #> metac2zero2diff ~ 1 + (1 | participant) + (1 | item) #> metac2one1diff ~ 1 + (1 | participant) + (1 | item) #> metac2one2diff ~ 1 + (1 | participant) + (1 | item) #> Data: data.aggregated (Number of observations: 2500) #> Draws: 4 chains, each with iter = 2000; warmup = 1000; thin = 1; #> total post-warmup draws = 4000 #> #> Multilevel Hyperparameters: #> ~item (Number of levels: 25) #> Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS #> sd(Intercept) 0.14 0.10 0.01 0.37 1.00 1467 1998 #> sd(dprime_Intercept) 0.57 0.14 0.36 0.89 1.00 854 1603 #> sd(c_Intercept) 0.76 0.12 0.57 1.02 1.00 792 1543 #> sd(metac2zero1diff_Intercept) 0.07 0.05 0.00 0.19 1.00 2130 2015 #> sd(metac2zero2diff_Intercept) 0.13 0.09 0.01 0.33 1.00 1264 1853 #> sd(metac2one1diff_Intercept) 0.12 0.09 0.00 0.31 1.00 1053 1268 #> sd(metac2one2diff_Intercept) 0.18 0.10 0.02 0.41 1.00 927 1443 #> #> ~participant (Number of levels: 50) #> Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS #> sd(Intercept) 0.12 0.09 0.00 0.31 1.00 1295 1610 #> sd(dprime_Intercept) 0.44 0.08 0.29 0.62 1.00 1513 2242 #> sd(c_Intercept) 0.35 0.05 0.27 0.45 1.00 1330 2340 #> sd(metac2zero1diff_Intercept) 0.18 0.10 0.01 0.38 1.00 739 1234 #> sd(metac2zero2diff_Intercept) 0.30 0.10 0.07 0.51 1.01 698 502 #> sd(metac2one1diff_Intercept) 0.21 0.10 0.02 0.41 1.00 702 973 #> sd(metac2one2diff_Intercept) 0.36 0.10 0.17 0.56 1.01 954 837 #> #> Regression Coefficients: #> Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS #> Intercept -0.23 0.09 -0.42 -0.06 1.00 3799 2616 #> dprime_Intercept 1.09 0.15 0.78 1.35 1.00 851 1703 #> c_Intercept 0.14 0.13 -0.12 0.40 1.01 501 1045 #> metac2zero1diff_Intercept -0.83 0.05 -0.93 -0.72 1.00 4309 2970 #> metac2zero2diff_Intercept -0.79 0.06 -0.91 -0.66 1.00 2891 2937 #> metac2one1diff_Intercept -0.83 0.06 -0.95 -0.71 1.00 2537 2565 #> metac2one2diff_Intercept -0.79 0.07 -0.92 -0.65 1.00 2479 2625 #> #> Draws were sampled using sampling(NUTS). For each parameter, Bulk_ESS #> and Tail_ESS are effective sample size measures, and Rhat is the potential #> scale reduction factor on split chains (at convergence, Rhat = 1). ``` As you can see, aside from the way that the data is formatted, this model is exactly the same as the multinomial model above. ## Extracting model estimates Obtaining posterior estimates over model parameters, predictions, and other estimates is very similar to the multinomial model (for details, see `vignette("hmetad")`). So, here we will focus on the type 1 ROC curves, this time using `roc1_rvars` instead of `roc1_draws` for increased efficiency. To get the posterior estimates, we need to specify a dataset to make predictions for, as well as a random effects formula to use in model predictions. For example, to estimate a ROC averaging over participants and items, we can use an empty data set with `re_formula=NA`: ``` r roc1_rvars(m.multinomial, tibble(.row = 1), re_formula = NA) #> # A tibble: 5 × 6 #> # Groups: .row, joint_response, response, confidence [5] #> .row joint_response response confidence p_fa p_hit #> #> 1 1 1 0 3 0.600 ± 0.058 0.89 ± 0.031 #> 2 1 2 0 2 0.413 ± 0.058 0.79 ± 0.047 #> 3 1 3 0 1 0.244 ± 0.048 0.65 ± 0.059 #> 4 1 4 1 1 0.136 ± 0.034 0.47 ± 0.062 #> 5 1 5 1 2 0.063 ± 0.020 0.29 ± 0.054 ``` The process is exactly the same for the categorical model: ``` r roc1_rvars(m.categorical, tibble(.row = 1), re_formula = NA) #> # A tibble: 5 × 6 #> # Groups: .row, joint_response, response, confidence [5] #> .row joint_response response confidence p_fa p_hit #> #> 1 1 1 0 3 0.605 ± 0.057 0.89 ± 0.028 #> 2 1 2 0 2 0.418 ± 0.057 0.79 ± 0.043 #> 3 1 3 0 1 0.248 ± 0.047 0.66 ± 0.055 #> 4 1 4 1 1 0.138 ± 0.033 0.47 ± 0.058 #> 5 1 5 1 2 0.064 ± 0.019 0.30 ± 0.051 ``` Next, to get participant-level ROCs (averaging over items), we can use a dataset with one row per participant and only the participant-level random effects: ``` r roc1_rvars(m.categorical, distinct(d, participant), re_formula = ~ (1 | participant)) #> # A tibble: 250 × 7 #> # Groups: .row, participant, joint_response, response, confidence [250] #> .row participant joint_response response confidence p_fa p_hit #> #> 1 1 1 1 0 3 0.54 ± 0.103 0.93 ± 0.038 #> 2 2 2 1 0 3 0.75 ± 0.077 0.91 ± 0.042 #> 3 3 3 1 0 3 0.84 ± 0.066 0.96 ± 0.024 #> 4 4 4 1 0 3 0.45 ± 0.099 0.78 ± 0.073 #> 5 5 5 1 0 3 0.63 ± 0.093 0.93 ± 0.035 #> 6 6 6 1 0 3 0.44 ± 0.099 0.91 ± 0.043 #> 7 7 7 1 0 3 0.60 ± 0.092 0.88 ± 0.051 #> 8 8 8 1 0 3 0.40 ± 0.095 0.67 ± 0.088 #> 9 9 9 1 0 3 0.72 ± 0.083 0.88 ± 0.052 #> 10 10 10 1 0 3 0.50 ± 0.099 0.68 ± 0.087 #> # ℹ 240 more rows ``` We can use a similar process to get item-level ROCs (averaging over participants): ``` r roc1_rvars(m.categorical, distinct(d, item), re_formula = ~ (1 | item)) #> # A tibble: 125 × 7 #> # Groups: .row, item, joint_response, response, confidence [125] #> .row item joint_response response confidence p_fa p_hit #> #> 1 1 1 1 0 3 0.92 ± 0.026 0.98 ± 0.0115 #> 2 2 2 1 0 3 0.93 ± 0.023 1.00 ± 0.0023 #> 3 3 3 1 0 3 0.59 ± 0.064 0.74 ± 0.0521 #> 4 4 4 1 0 3 0.50 ± 0.066 0.91 ± 0.0292 #> 5 5 5 1 0 3 0.71 ± 0.055 0.93 ± 0.0252 #> 6 6 6 1 0 3 0.50 ± 0.067 0.92 ± 0.0249 #> 7 7 7 1 0 3 0.47 ± 0.067 0.84 ± 0.0413 #> 8 8 8 1 0 3 0.43 ± 0.064 0.89 ± 0.0324 #> 9 9 9 1 0 3 0.56 ± 0.065 0.88 ± 0.0334 #> 10 10 10 1 0 3 0.41 ± 0.065 0.87 ± 0.0349 #> # ℹ 115 more rows ``` ## Other benefits Aside from representing the data in a more convenient format, the trial-level model should be more useful for things like model comparison using the `loo` package, multivariate models, and mediation models. These features should mostly work out of the box but they are still under active development, so stay tuned!