Baby steps in Bayes: Recoding predictors and homing in on specific comparisons

Bayesian statistics
brms
R
graphs
mixed-effects models
contrast coding
Author

Jan Vanhove

Published

December 20, 2018

Interpreting models that take into account a host of possible interactions between predictor variables can be a pain, especially when some of the predictors contain more than two levels. In this post, I show how I went about fitting and then making sense of a multilevel model containing a three-way interaction between its categorical fixed-effect predictors. To this end, I used the brms package, which makes it relatively easy to fit Bayesian models using a notation that hardly differs from the one used in the popular lme4 package. I won’t discuss the Bayesian bit much here (I don’t think it’s too important), and I will instead cover the following points:

  1. How to fit a multilevel model with brms using R’s default way of handling categorical predictors (treatment coding).
  2. How to interpret this model’s fixed parameter estimates.
  3. How to visualise the modelled effects.
  4. How to recode predictors to obtain more useful parameter estimates.
  5. How to extract information from the model to home in on specific comparisons.

The data

For a longitudinal project, 328 children wrote narrative and argumentative texts in Portuguese at three points in time. About a third of the children hailed from Portugal, about a third were children of Portuguese heritage living in the French-speaking part of Switzerland, and about a third were children of Portuguese heritage living in the German-speaking part of Switzerland. Not all children wrote both kinds of texts at all three points in time, and 1,040 texts were retained for the analysis. For each text, we computed the Guiraud index, which is a function of the number of words (tokens) and the number of different words (types) in the texts. Higher values are assumed to reflect greater diversity in vocabulary use.

If you want to know more about this project, check out Bonvin et al. (2018), Lambelet et al. (2017a,b) and Vanhove et al. (2019); you’ll find the references at the bottom of this page.

Update (2023-08-06): I ran all of the R code again with newer software versions when converting the format of this blog.

Read in the data:

# Load tidyverse suite
library(tidyverse)

# Read in data from my webspace
d <- read_csv("http://homeweb.unifr.ch/VanhoveJ/Pub/Data/portuguese_guiraud.csv")

# Need to code factors explicitly
d$Group    <- factor(d$Group)
d$TextType <- factor(d$TextType)
d$Time     <- factor(d$Time)

str(d)
spc_tbl_ [1,040 × 6] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
 $ Group   : Factor w/ 3 levels "monolingual Portuguese",..: 1 1 1 1 1 1 1 1 1 1 ...
 $ Class   : chr [1:1040] "monolingual Portuguese_AI" "monolingual Portuguese_AI" "monolingual Portuguese_AI" "monolingual Portuguese_AI" ...
 $ Child   : chr [1:1040] "monolingual Portuguese_AI_1" "monolingual Portuguese_AI_1" "monolingual Portuguese_AI_1" "monolingual Portuguese_AI_10" ...
 $ Time    : Factor w/ 3 levels "T1","T2","T3": 1 1 2 1 1 3 3 1 3 1 ...
 $ TextType: Factor w/ 2 levels "argumentative",..: 1 2 2 1 2 1 2 2 2 1 ...
 $ Guiraud : num [1:1040] 4.73 5.83 3.9 4.22 4.57 ...
 - attr(*, "spec")=
  .. cols(
  ..   Group = col_character(),
  ..   Class = col_character(),
  ..   Child = col_character(),
  ..   Time = col_character(),
  ..   TextType = col_character(),
  ..   Guiraud = col_double()
  .. )
 - attr(*, "problems")=<externalptr> 
summary(d)
                    Group        Class              Child           Time    
 monolingual Portuguese:360   Length:1040        Length:1040        T1:320  
 Portuguese-French     :360   Class :character   Class :character   T2:340  
 Portuguese-German     :320   Mode  :character   Mode  :character   T3:380  
                                                                            
                                                                            
                                                                            
          TextType      Guiraud    
 argumentative:560   Min.   :2.32  
 narrative    :480   1st Qu.:3.93  
                     Median :4.64  
                     Mean   :4.75  
                     3rd Qu.:5.48  
                     Max.   :8.43  

Let’s also plot the data. Incidentally, and contrary to popular belief, I don’t write ggplot code such as this from scratch. What you see is the result of drawing and redrawing (see comments).

# Plot Guiraud scores
ggplot(d,
       aes(x = Time,
           y = Guiraud,
           # reorder: sort the Groups by their median Guiraud value
           fill = reorder(Group, Guiraud, median))) +
  # I prefer empty (shape = 1) to filled circles (shape = 16).
  geom_boxplot(outlier.shape = 1) +
  facet_grid(. ~ TextType) +
  # The legend name ("Group") seems superfluous, so suppress it;
  # the default colours contain red and green, which can be hard to
  #  distinguish for some people.
  scale_fill_brewer(name = element_blank(), type = "qual") +
  # I prefer the black and white look to the default grey one.
  theme_bw() +
  # Put the legend at the bottom rather than on the right
  theme(legend.position = "bottom")

Figure 1. The texts’ Guiraud values by time of data collection, text type, and language background.

A multilevel model with treatment coding

Our data are nested: Each child wrote up to 6 texts, and the data were collected in classes, with each child belong to one class. It’s advisable to take such nesting into account since you may end up overestimating your degree of certainty about the results otherwise. I mostly use lme4’s lmer() and glmer() functions to handle such data, but as will become clearer in a minute, brms’s brm() function offers some distinct advantages. So let’s load that package:

library(brms)

Fitting the model

We’ll fit a model with a three-way fixed-effect interaction between Time, TextType and Group as well as with by-Child and by-Class random intercepts. In order to take into account the possibility that children vary in the development of their lexical diversity, we add a random slope of Time by Child, and in order to take into account the possibility that their lexical diversity varies by text type, we do the same for TextType. Similarly, we add by-Class random slopes for Time and TextType.

m_default <- brm(Guiraud ~ Time*TextType*Group +
                   (1 + TextType + Time|Class) +
                   (1 + TextType + Time|Child),
                 cores = 4, iter = 4000,
                 silent = 2,
                 control = list(adapt_delta = 0.95),
                 data = d)
Running /usr/lib/R/bin/R CMD SHLIB foo.c
using C compiler: ‘gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0’
gcc -I"/usr/share/R/include" -DNDEBUG   -I"/home/jan/R/x86_64-pc-linux-gnu-library/4.3/Rcpp/include/"  -I"/home/jan/R/x86_64-pc-linux-gnu-library/4.3/RcppEigen/include/"  -I"/home/jan/R/x86_64-pc-linux-gnu-library/4.3/RcppEigen/include/unsupported"  -I"/home/jan/R/x86_64-pc-linux-gnu-library/4.3/BH/include" -I"/home/jan/R/x86_64-pc-linux-gnu-library/4.3/StanHeaders/include/src/"  -I"/home/jan/R/x86_64-pc-linux-gnu-library/4.3/StanHeaders/include/"  -I"/home/jan/R/x86_64-pc-linux-gnu-library/4.3/RcppParallel/include/"  -I"/home/jan/R/x86_64-pc-linux-gnu-library/4.3/rstan/include" -DEIGEN_NO_DEBUG  -DBOOST_DISABLE_ASSERTS  -DBOOST_PENDING_INTEGER_LOG2_HPP  -DSTAN_THREADS  -DUSE_STANC3 -DSTRICT_R_HEADERS  -DBOOST_PHOENIX_NO_VARIADIC_EXPRESSION  -DBOOST_NO_AUTO_PTR  -include '/home/jan/R/x86_64-pc-linux-gnu-library/4.3/StanHeaders/include/stan/math/prim/fun/Eigen.hpp'  -D_REENTRANT -DRCPP_PARALLEL_USE_TBB=1       -fpic  -g -O2 -ffile-prefix-map=/build/r-base-1upgAf/r-base-4.3.1=. -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2  -c foo.c -o foo.o
In file included from /home/jan/R/x86_64-pc-linux-gnu-library/4.3/RcppEigen/include/Eigen/Core:88,
                 from /home/jan/R/x86_64-pc-linux-gnu-library/4.3/RcppEigen/include/Eigen/Dense:1,
                 from /home/jan/R/x86_64-pc-linux-gnu-library/4.3/StanHeaders/include/stan/math/prim/fun/Eigen.hpp:22,
                 from <command-line>:
/home/jan/R/x86_64-pc-linux-gnu-library/4.3/RcppEigen/include/Eigen/src/Core/util/Macros.h:628:1: error: unknown type name ‘namespace’
  628 | namespace Eigen {
      | ^~~~~~~~~
/home/jan/R/x86_64-pc-linux-gnu-library/4.3/RcppEigen/include/Eigen/src/Core/util/Macros.h:628:17: error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before ‘{’ token
  628 | namespace Eigen {
      |                 ^
In file included from /home/jan/R/x86_64-pc-linux-gnu-library/4.3/RcppEigen/include/Eigen/Dense:1,
                 from /home/jan/R/x86_64-pc-linux-gnu-library/4.3/StanHeaders/include/stan/math/prim/fun/Eigen.hpp:22,
                 from <command-line>:
/home/jan/R/x86_64-pc-linux-gnu-library/4.3/RcppEigen/include/Eigen/Core:96:10: fatal error: complex: No such file or directory
   96 | #include <complex>
      |          ^~~~~~~~~
compilation terminated.
make: *** [/usr/lib/R/etc/Makeconf:191: foo.o] Error 1

Interpreting the parameter estimates

summary(m_default)
 Family: gaussian 
  Links: mu = identity; sigma = identity 
Formula: Guiraud ~ Time * TextType * Group + (1 + TextType + Time | Class) + (1 + TextType + Time | Child) 
   Data: d (Number of observations: 1040) 
  Draws: 4 chains, each with iter = 4000; warmup = 2000; thin = 1;
         total post-warmup draws = 8000

Group-Level Effects: 
~Child (Number of levels: 328) 
                                 Estimate Est.Error l-95% CI u-95% CI Rhat
sd(Intercept)                        0.42      0.05     0.33     0.52 1.00
sd(TextTypenarrative)                0.26      0.09     0.07     0.43 1.01
sd(TimeT2)                           0.09      0.07     0.00     0.25 1.01
sd(TimeT3)                           0.39      0.09     0.19     0.55 1.01
cor(Intercept,TextTypenarrative)     0.23      0.28    -0.26     0.80 1.00
cor(Intercept,TimeT2)               -0.10      0.41    -0.80     0.74 1.00
cor(TextTypenarrative,TimeT2)        0.06      0.43    -0.77     0.82 1.00
cor(Intercept,TimeT3)                0.16      0.24    -0.25     0.67 1.01
cor(TextTypenarrative,TimeT3)       -0.06      0.30    -0.60     0.60 1.00
cor(TimeT2,TimeT3)                   0.05      0.45    -0.79     0.83 1.04
                                 Bulk_ESS Tail_ESS
sd(Intercept)                        2748     4437
sd(TextTypenarrative)                 462     1266
sd(TimeT2)                           1314     2095
sd(TimeT3)                            523      969
cor(Intercept,TextTypenarrative)      697     1957
cor(Intercept,TimeT2)                5842     5625
cor(TextTypenarrative,TimeT2)        3522     4685
cor(Intercept,TimeT3)                 474     1509
cor(TextTypenarrative,TimeT3)         546      696
cor(TimeT2,TimeT3)                    152      478

~Class (Number of levels: 25) 
                                 Estimate Est.Error l-95% CI u-95% CI Rhat
sd(Intercept)                        0.16      0.08     0.01     0.33 1.00
sd(TextTypenarrative)                0.24      0.08     0.09     0.42 1.00
sd(TimeT2)                           0.11      0.07     0.00     0.28 1.00
sd(TimeT3)                           0.10      0.07     0.00     0.28 1.00
cor(Intercept,TextTypenarrative)    -0.13      0.37    -0.76     0.66 1.00
cor(Intercept,TimeT2)               -0.09      0.42    -0.83     0.74 1.00
cor(TextTypenarrative,TimeT2)        0.07      0.40    -0.72     0.78 1.00
cor(Intercept,TimeT3)                0.11      0.43    -0.72     0.84 1.00
cor(TextTypenarrative,TimeT3)       -0.14      0.42    -0.84     0.71 1.00
cor(TimeT2,TimeT3)                   0.11      0.44    -0.76     0.85 1.00
                                 Bulk_ESS Tail_ESS
sd(Intercept)                        1094     1326
sd(TextTypenarrative)                1827     1824
sd(TimeT2)                           1983     2669
sd(TimeT3)                           2187     2828
cor(Intercept,TextTypenarrative)     1479     2428
cor(Intercept,TimeT2)                4581     5429
cor(TextTypenarrative,TimeT2)        5598     5821
cor(Intercept,TimeT3)                5042     4693
cor(TextTypenarrative,TimeT3)        5856     5884
cor(TimeT2,TimeT3)                   4193     5326

Population-Level Effects: 
                                                Estimate Est.Error l-95% CI
Intercept                                           5.25      0.13     4.99
TimeT2                                              0.57      0.13     0.31
TimeT3                                              0.91      0.14     0.64
TextTypenarrative                                  -0.26      0.17    -0.60
GroupPortugueseMFrench                             -1.06      0.17    -1.40
GroupPortugueseMGerman                             -1.30      0.17    -1.63
TimeT2:TextTypenarrative                           -0.24      0.16    -0.56
TimeT3:TextTypenarrative                           -0.06      0.17    -0.39
TimeT2:GroupPortugueseMFrench                      -0.55      0.18    -0.91
TimeT3:GroupPortugueseMFrench                      -0.38      0.19    -0.74
TimeT2:GroupPortugueseMGerman                      -0.64      0.19    -1.00
TimeT3:GroupPortugueseMGerman                      -0.23      0.19    -0.60
TextTypenarrative:GroupPortugueseMFrench            0.14      0.24    -0.32
TextTypenarrative:GroupPortugueseMGerman            0.17      0.23    -0.29
TimeT2:TextTypenarrative:GroupPortugueseMFrench     0.37      0.24    -0.11
TimeT3:TextTypenarrative:GroupPortugueseMFrench     0.25      0.24    -0.23
TimeT2:TextTypenarrative:GroupPortugueseMGerman     0.55      0.25     0.05
TimeT3:TextTypenarrative:GroupPortugueseMGerman     0.27      0.25    -0.23
                                                u-95% CI Rhat Bulk_ESS Tail_ESS
Intercept                                           5.50 1.00     2481     3890
TimeT2                                              0.83 1.00     2430     4080
TimeT3                                              1.18 1.00     2750     4480
TextTypenarrative                                   0.07 1.00     2323     3570
GroupPortugueseMFrench                             -0.72 1.00     2655     3588
GroupPortugueseMGerman                             -0.96 1.00     2574     4200
TimeT2:TextTypenarrative                            0.08 1.00     2586     4743
TimeT3:TextTypenarrative                            0.27 1.00     2454     4467
TimeT2:GroupPortugueseMFrench                      -0.18 1.00     2706     3864
TimeT3:GroupPortugueseMFrench                       0.00 1.00     3087     5057
TimeT2:GroupPortugueseMGerman                      -0.28 1.00     2642     4234
TimeT3:GroupPortugueseMGerman                       0.15 1.00     3000     4337
TextTypenarrative:GroupPortugueseMFrench            0.62 1.00     2668     4283
TextTypenarrative:GroupPortugueseMGerman            0.62 1.00     2537     4079
TimeT2:TextTypenarrative:GroupPortugueseMFrench     0.84 1.00     2969     5155
TimeT3:TextTypenarrative:GroupPortugueseMFrench     0.72 1.00     2858     4537
TimeT2:TextTypenarrative:GroupPortugueseMGerman     1.03 1.00     2896     4577
TimeT3:TextTypenarrative:GroupPortugueseMGerman     0.76 1.00     2666     4880

Family Specific Parameters: 
      Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
sigma     0.60      0.02     0.55     0.65 1.01      730     1692

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).

The output looks pretty similar to what we’d obtain when using lmer(), but let’s review what these estimates actually refer to. By default, R uses treatment coding. This entails that the Intercept refers to a specific combination of factors: the combination of all reference levels. Again by default, the reference levels are chosen alphabetically:

  • Time consists of three levels (T1, T2, T3); for alphabetical reasons, T1 is chosen as the default reference level.
  • Group also consists of three levels (monolingual Portuguese, Portuguese-French, Portuguese-German); monolingual Portuguese is chosen as the default level.
  • TextType consists of two levels (argumentative, narrative); argumentative is the default reference level.

The Intercept, then, shows the modelled mean Guiraud value of argumentative texts written by monolingual Portuguese children at T1: 5.25.

If you’re unsure which factor level was used as the reference level, you can use the contrasts() function. The reference level is the one in whose rows only zeroes occur.

contrasts(d$Group)
                       Portuguese-French Portuguese-German
monolingual Portuguese                 0                 0
Portuguese-French                      1                 0
Portuguese-German                      0                 1

Crucially, all other estimated effects are computed with respect to this intercept. That is, TimeT2 (0.57) shows the difference between T1 and T2 for monolingual Portuguese children writing argumentative texts. Similarly, TimeT3 (0.91) shows the difference between T1 and T3 for monolingual Portuguese children writing argumentative texts, and TextTypenarrative (-0.27) shows the difference between the mean Guiraud values of argumentative and narrative texts written by monolingual Portuguese children writing at T1. The texts written by the Portuguese-German and Portuguese-French bilinguals don’t enter into these estimates.

Now, it’s possible to piece together the mean values associated with each combination of predictor values, but questions such as the following remain difficult to answer with just these estimates at hand:

  • What’s the overall difference between T2 and T3 and its uncertainty?
  • What’s the overall difference between the Guiraud values of texts written by Portuguese-French and Portuguese-German children and its uncertainty?

We’ll tackle these questions in a minute; for now, the point is merely that the estimated parameters above all refer to highly specific comparisons that may not be the most relevant.

Plotting the fitted values and the uncertainty about them

When working with brms, it’s relatively easy to obtain the modelled average outcome value for each combination of the predictor variables as well as a measure of the uncertainty associated with them.

First construct a small data frame containing the unique combinations of predictor variables in our dataset:

d_pred <- d |> 
  select(Group, Time, TextType) |> 
  distinct() |> 
  arrange(Group, Time, TextType)
d_pred
# A tibble: 18 × 3
   Group                  Time  TextType     
   <fct>                  <fct> <fct>        
 1 monolingual Portuguese T1    argumentative
 2 monolingual Portuguese T1    narrative    
 3 monolingual Portuguese T2    argumentative
 4 monolingual Portuguese T2    narrative    
 5 monolingual Portuguese T3    argumentative
 6 monolingual Portuguese T3    narrative    
 7 Portuguese-French      T1    argumentative
 8 Portuguese-French      T1    narrative    
 9 Portuguese-French      T2    argumentative
10 Portuguese-French      T2    narrative    
11 Portuguese-French      T3    argumentative
12 Portuguese-French      T3    narrative    
13 Portuguese-German      T1    argumentative
14 Portuguese-German      T1    narrative    
15 Portuguese-German      T2    argumentative
16 Portuguese-German      T2    narrative    
17 Portuguese-German      T3    argumentative
18 Portuguese-German      T3    narrative    

If you feed the model (here: m_default) and the data frame we’ve just created (d_pred) to the fitted() function, it outputs the modelled mean estimate for each combination of predictor values (Estimate), the estimated error of this mean estimate (Est.Error), and a 95% uncertainty interval about the estimate (Q2.5 and Q97.5). One more thing: The re_formula = NA line specifies that we do not want the variability associated with the by-Class and by-Child random effects to affect the estimates and their uncertainty. This is what I typically want.

cbind(
  d_pred, 
  fitted(m_default, 
         newdata = d_pred, 
         re_formula = NA)
  )
                    Group Time      TextType Estimate Est.Error Q2.5 Q97.5
1  monolingual Portuguese   T1 argumentative     5.25     0.128 4.99  5.50
2  monolingual Portuguese   T1     narrative     4.99     0.172 4.65  5.33
3  monolingual Portuguese   T2 argumentative     5.82     0.143 5.54  6.10
4  monolingual Portuguese   T2     narrative     5.32     0.186 4.95  5.69
5  monolingual Portuguese   T3 argumentative     6.16     0.157 5.85  6.47
6  monolingual Portuguese   T3     narrative     5.84     0.189 5.47  6.23
7       Portuguese-French   T1 argumentative     4.19     0.115 3.97  4.42
8       Portuguese-French   T1     narrative     4.07     0.163 3.76  4.39
9       Portuguese-French   T2 argumentative     4.21     0.123 3.97  4.46
10      Portuguese-French   T2     narrative     4.21     0.156 3.91  4.53
11      Portuguese-French   T3 argumentative     4.73     0.130 4.48  4.99
12      Portuguese-French   T3     narrative     4.80     0.157 4.49  5.11
13      Portuguese-German   T1 argumentative     3.95     0.109 3.73  4.17
14      Portuguese-German   T1     narrative     3.86     0.148 3.56  4.15
15      Portuguese-German   T2 argumentative     3.88     0.116 3.65  4.11
16      Portuguese-German   T2     narrative     4.10     0.157 3.78  4.41
17      Portuguese-German   T3 argumentative     4.64     0.127 4.38  4.89
18      Portuguese-German   T3     narrative     4.76     0.147 4.47  5.04

So where do these estimates and uncertainty intervals come from? In the Bayesian approach, every model parameter hasn’t got just one estimate but an entire distribution of estimates. Moreover, everything that depends on model parameters also has an entire distribution of estimates associated with it. The mean modelled outcome values per cell depend on the model parameters, so they, too, have entire distributions associated with them. The fitted() function summarises these distributions for us: it returns their means as Estimate, their standard deviations as Est.Error and their 2.5th and 97.5 percentiles as Q2.5 and Q97.5. If so inclined, you can generate these distributions yourself using the posterior_linpred() function:

posterior_fit <- posterior_linpred(m_default, newdata = d_pred, re_formula = NA)
dim(posterior_fit)
[1] 8000   18

This returns matrix of 4000 rows and 18 columns. 4000 is the number of ‘post-warmup samples’ (see the output of summary(m_default); 18 is the number of combinations of predictor values in d_pred.

The first column of posterior_fit contains the distribution associated with the first row in d_pred. If you compute its mean, standard deviation and 2.5th and 97.5th percentiles, you end up with the same numbers as above:

mean(posterior_fit[, 1])
[1] 5.25
sd(posterior_fit[, 1])
[1] 0.128
quantile(posterior_fit[, 1], probs = c(0.025, 0.975))
 2.5% 97.5% 
 4.99  5.50 

Or similarly for the 10th row of d_pred (Portuguese-French, T2, narrative):

mean(posterior_fit[, 10])
[1] 4.21
sd(posterior_fit[, 10])
[1] 0.156
quantile(posterior_fit[, 10], probs = c(0.025, 0.975))
 2.5% 97.5% 
 3.91  4.53 

At the moment, using posterior_linpred() has no added value, but it’s good to know where these numbers come from.

Let’s draw a graph showing these modelled averages and the uncertainty about them. 95% uncertainty intervals are typically used, but they may instill dichotomous thinking. To highlight that such an interval highlights but two points on a continuum, I’m tempted to add 80% intervals as well:

# Obtain fitted values + uncertainty
fitted_values <- fitted(m_default, newdata = d_pred, re_formula = NA, 
                        # 95% interval: between 2.5th and 97.5th percentile
                        # 80% interval: between 10th and 90th percentile
                        probs = c(0.025, 0.10, 0.90, 0.975))
# Combine fitted values with predictor values
fitted_values <- cbind(d_pred, fitted_values)
fitted_values
                    Group Time      TextType Estimate Est.Error Q2.5  Q10  Q90
1  monolingual Portuguese   T1 argumentative     5.25     0.128 4.99 5.09 5.41
2  monolingual Portuguese   T1     narrative     4.99     0.172 4.65 4.77 5.20
3  monolingual Portuguese   T2 argumentative     5.82     0.143 5.54 5.64 6.00
4  monolingual Portuguese   T2     narrative     5.32     0.186 4.95 5.09 5.55
5  monolingual Portuguese   T3 argumentative     6.16     0.157 5.85 5.97 6.36
6  monolingual Portuguese   T3     narrative     5.84     0.189 5.47 5.60 6.08
7       Portuguese-French   T1 argumentative     4.19     0.115 3.97 4.04 4.33
8       Portuguese-French   T1     narrative     4.07     0.163 3.76 3.86 4.28
9       Portuguese-French   T2 argumentative     4.21     0.123 3.97 4.06 4.36
10      Portuguese-French   T2     narrative     4.21     0.156 3.91 4.02 4.41
11      Portuguese-French   T3 argumentative     4.73     0.130 4.48 4.56 4.89
12      Portuguese-French   T3     narrative     4.80     0.157 4.49 4.60 5.00
13      Portuguese-German   T1 argumentative     3.95     0.109 3.73 3.81 4.09
14      Portuguese-German   T1     narrative     3.86     0.148 3.56 3.68 4.05
15      Portuguese-German   T2 argumentative     3.88     0.116 3.65 3.73 4.03
16      Portuguese-German   T2     narrative     4.10     0.157 3.78 3.90 4.30
17      Portuguese-German   T3 argumentative     4.64     0.127 4.38 4.48 4.80
18      Portuguese-German   T3     narrative     4.76     0.147 4.47 4.57 4.94
   Q97.5
1   5.50
2   5.33
3   6.10
4   5.69
5   6.47
6   6.23
7   4.42
8   4.39
9   4.46
10  4.53
11  4.99
12  5.11
13  4.17
14  4.15
15  4.11
16  4.41
17  4.89
18  5.04

And now for the graph:

# Move all points apart horizontally to reduce overlap
position_adjustment <- position_dodge(width = 0.3)

ggplot(fitted_values,
       aes(x = Time,
           y = Estimate,
           # Sort Groups from low to high
           colour = reorder(Group, Estimate),
           group = Group)) +
  # Move point apart:
  geom_point(position = position_adjustment) +
  # Move lines apart:
  geom_path(position = position_adjustment) +
  # Add 95% intervals; move them apart, too
  geom_linerange(aes(ymin = Q2.5, ymax = Q97.5), linewidth = 0.4,
                 position = position_adjustment) +
  # Add 80% intervals; move them apart, too
  geom_linerange(aes(ymin = Q10, ymax = Q90), linewidth = 0.9,
                 position = position_adjustment) +
  facet_wrap(~ TextType) +
  # Override default colour
  scale_colour_brewer(name = element_blank(), type = "qual") +
  ylab("Modelled mean Guiraud") +
  theme_bw() +
  theme(legend.position = "bottom")

Figure 2. The modelled mean Guiraud values and their uncertainty (thick vertical lines: 80% interval; thin vertical lines: 95% interval).

A model with more sensible coding

Tailoring the coding of categorical predictors to the research questions

The summary() output for m_default was difficult to interpret because treatment coding was used. However, we can override this default behaviour to end up with estimates that are more readily and more usefully interpretable.

The first thing we can do is to override the default refence level. Figure 1 showed that the Guiraud values at T2 tend to be somewhere midway between those at T1 and T3, so we can make the intercept estimate more representative of the dataset as a whole by making T2 the reference level of Time rather than T1. A benefit of doing so is that we will now have two parameters, TimeT1 and TimeT3 that specify the difference between T1-T2 and T2-T3, respectively. In other words, the estimated parameters will directly reflect the progression from data collection to data collection. (Before, the parameter estimates specified the differences between T1-T2 and T1-T3, so a direct estimate for T2-T3 was lacking.)

# Set T2 as default time; retain treatment coding
d$Time <- relevel(d$Time, "T2")

Second, there’s no reason for preferring argumentative or narrative texts as the reference level. If we sum-code this predictor, the intercept reflects the grand mean of the argumentative and narrative texts (at T2), and the estimated parameter then specifies how far the mean Guiraud value of each text type is removed from this mean:

# Sum (or deviation) coding for TextType (2 levels)
contrasts(d$TextType) <- contr.sum(2)

Similarly, there are a couple of reasonable ways to choose the reference level for Group when using treatment coding. But you can also sum-code this predictor so that the intercept reflects the grand mean of the Guiraud values of texts written by monolingual Portuguese and bilingual Portuguese-French and Portuguese-German kids (at T2).

# Sum (or deviation) coding for Group (3 levels)
contrasts(d$Group) <- contr.sum(3)

Refitting the model

No difference here.

m_recoded <- brm(Guiraud ~ Time*TextType*Group +
                   (1 + TextType + Time|Class) +
                   (1 + TextType + Time|Child),
                 cores = 4, iter = 4000,
                 silent = 2,
                 control = list(adapt_delta = 0.95),
                 data = d)
Running /usr/lib/R/bin/R CMD SHLIB foo.c
using C compiler: ‘gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0’
gcc -I"/usr/share/R/include" -DNDEBUG   -I"/home/jan/R/x86_64-pc-linux-gnu-library/4.3/Rcpp/include/"  -I"/home/jan/R/x86_64-pc-linux-gnu-library/4.3/RcppEigen/include/"  -I"/home/jan/R/x86_64-pc-linux-gnu-library/4.3/RcppEigen/include/unsupported"  -I"/home/jan/R/x86_64-pc-linux-gnu-library/4.3/BH/include" -I"/home/jan/R/x86_64-pc-linux-gnu-library/4.3/StanHeaders/include/src/"  -I"/home/jan/R/x86_64-pc-linux-gnu-library/4.3/StanHeaders/include/"  -I"/home/jan/R/x86_64-pc-linux-gnu-library/4.3/RcppParallel/include/"  -I"/home/jan/R/x86_64-pc-linux-gnu-library/4.3/rstan/include" -DEIGEN_NO_DEBUG  -DBOOST_DISABLE_ASSERTS  -DBOOST_PENDING_INTEGER_LOG2_HPP  -DSTAN_THREADS  -DUSE_STANC3 -DSTRICT_R_HEADERS  -DBOOST_PHOENIX_NO_VARIADIC_EXPRESSION  -DBOOST_NO_AUTO_PTR  -include '/home/jan/R/x86_64-pc-linux-gnu-library/4.3/StanHeaders/include/stan/math/prim/fun/Eigen.hpp'  -D_REENTRANT -DRCPP_PARALLEL_USE_TBB=1       -fpic  -g -O2 -ffile-prefix-map=/build/r-base-1upgAf/r-base-4.3.1=. -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2  -c foo.c -o foo.o
In file included from /home/jan/R/x86_64-pc-linux-gnu-library/4.3/RcppEigen/include/Eigen/Core:88,
                 from /home/jan/R/x86_64-pc-linux-gnu-library/4.3/RcppEigen/include/Eigen/Dense:1,
                 from /home/jan/R/x86_64-pc-linux-gnu-library/4.3/StanHeaders/include/stan/math/prim/fun/Eigen.hpp:22,
                 from <command-line>:
/home/jan/R/x86_64-pc-linux-gnu-library/4.3/RcppEigen/include/Eigen/src/Core/util/Macros.h:628:1: error: unknown type name ‘namespace’
  628 | namespace Eigen {
      | ^~~~~~~~~
/home/jan/R/x86_64-pc-linux-gnu-library/4.3/RcppEigen/include/Eigen/src/Core/util/Macros.h:628:17: error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before ‘{’ token
  628 | namespace Eigen {
      |                 ^
In file included from /home/jan/R/x86_64-pc-linux-gnu-library/4.3/RcppEigen/include/Eigen/Dense:1,
                 from /home/jan/R/x86_64-pc-linux-gnu-library/4.3/StanHeaders/include/stan/math/prim/fun/Eigen.hpp:22,
                 from <command-line>:
/home/jan/R/x86_64-pc-linux-gnu-library/4.3/RcppEigen/include/Eigen/Core:96:10: fatal error: complex: No such file or directory
   96 | #include <complex>
      |          ^~~~~~~~~
compilation terminated.
make: *** [/usr/lib/R/etc/Makeconf:191: foo.o] Error 1

Interpreting the parameter estimates

summary(m_recoded)
 Family: gaussian 
  Links: mu = identity; sigma = identity 
Formula: Guiraud ~ Time * TextType * Group + (1 + TextType + Time | Class) + (1 + TextType + Time | Child) 
   Data: d (Number of observations: 1040) 
  Draws: 4 chains, each with iter = 4000; warmup = 2000; thin = 1;
         total post-warmup draws = 8000

Group-Level Effects: 
~Child (Number of levels: 328) 
                         Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS
sd(Intercept)                0.47      0.04     0.38     0.55 1.00     2449
sd(TextType1)                0.13      0.05     0.02     0.22 1.01      597
sd(TimeT1)                   0.10      0.07     0.00     0.27 1.00     1114
sd(TimeT3)                   0.38      0.09     0.19     0.55 1.01      616
cor(Intercept,TextType1)    -0.47      0.22    -0.86    -0.00 1.00     2195
cor(Intercept,TimeT1)       -0.03      0.39    -0.74     0.74 1.00     5843
cor(TextType1,TimeT1)        0.03      0.43    -0.79     0.81 1.00     3610
cor(Intercept,TimeT3)        0.14      0.22    -0.24     0.64 1.01      945
cor(TextType1,TimeT3)        0.05      0.32    -0.65     0.64 1.01      412
cor(TimeT1,TimeT3)          -0.07      0.42    -0.82     0.74 1.01      280
                         Tail_ESS
sd(Intercept)                4309
sd(TextType1)                1011
sd(TimeT1)                   1881
sd(TimeT3)                   1077
cor(Intercept,TextType1)     2927
cor(Intercept,TimeT1)        5186
cor(TextType1,TimeT1)        4759
cor(Intercept,TimeT3)        1621
cor(TextType1,TimeT3)         672
cor(TimeT1,TimeT3)            823

~Class (Number of levels: 25) 
                         Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS
sd(Intercept)                0.18      0.08     0.03     0.34 1.00     1193
sd(TextType1)                0.12      0.04     0.05     0.21 1.00     2767
sd(TimeT1)                   0.10      0.07     0.00     0.27 1.00     2164
sd(TimeT3)                   0.10      0.07     0.00     0.27 1.00     2111
cor(Intercept,TextType1)    -0.19      0.35    -0.79     0.53 1.00     2478
cor(Intercept,TimeT1)       -0.12      0.43    -0.85     0.74 1.00     5476
cor(TextType1,TimeT1)       -0.02      0.42    -0.78     0.76 1.00     5773
cor(Intercept,TimeT3)        0.04      0.43    -0.77     0.81 1.00     6506
cor(TextType1,TimeT3)        0.15      0.41    -0.69     0.85 1.00     5899
cor(TimeT1,TimeT3)           0.13      0.44    -0.76     0.86 1.00     4553
                         Tail_ESS
sd(Intercept)                1273
sd(TextType1)                2274
sd(TimeT1)                   3196
sd(TimeT3)                   3346
cor(Intercept,TextType1)     3033
cor(Intercept,TimeT1)        5363
cor(TextType1,TimeT1)        5650
cor(Intercept,TimeT3)        5504
cor(TextType1,TimeT3)        6160
cor(TimeT1,TimeT3)           5721

Population-Level Effects: 
                        Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS
Intercept                   4.59      0.06     4.46     4.72 1.00     4216
TimeT1                     -0.20      0.06    -0.32    -0.08 1.00     6096
TimeT3                      0.56      0.06     0.44     0.69 1.00     5736
TextType1                   0.05      0.05    -0.05     0.14 1.00     4230
Group1                      0.98      0.09     0.78     1.16 1.00     3129
Group2                     -0.38      0.09    -0.55    -0.20 1.00     2923
TimeT1:TextType1            0.03      0.05    -0.07     0.13 1.00     6671
TimeT3:TextType1           -0.02      0.05    -0.12     0.07 1.00     5945
TimeT1:Group1              -0.24      0.08    -0.40    -0.07 1.00     5325
TimeT3:Group1              -0.13      0.09    -0.31     0.05 1.00     5069
TimeT1:Group2               0.12      0.09    -0.05     0.29 1.00     5178
TimeT3:Group2              -0.01      0.09    -0.18     0.16 1.00     5133
TextType1:Group1            0.21      0.07     0.07     0.34 1.00     3122
TextType1:Group2           -0.04      0.07    -0.18     0.09 1.00     3293
TimeT1:TextType1:Group1    -0.15      0.07    -0.29    -0.01 1.00     5085
TimeT3:TextType1:Group1    -0.07      0.07    -0.20     0.07 1.00     5624
TimeT1:TextType1:Group2     0.03      0.07    -0.12     0.17 1.00     5303
TimeT3:TextType1:Group2    -0.01      0.07    -0.15     0.12 1.00     5218
                        Tail_ESS
Intercept                   4839
TimeT1                      5507
TimeT3                      5002
TextType1                   5381
Group1                      3741
Group2                      4022
TimeT1:TextType1            6505
TimeT3:TextType1            5426
TimeT1:Group1               5184
TimeT3:Group1               5097
TimeT1:Group2               5426
TimeT3:Group2               5284
TextType1:Group1            4089
TextType1:Group2            4598
TimeT1:TextType1:Group1     5711
TimeT3:TextType1:Group1     6277
TimeT1:TextType1:Group2     5777
TimeT3:TextType1:Group2     5951

Family Specific Parameters: 
      Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
sigma     0.60      0.02     0.55     0.65 1.00      867     2169

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).

Now the Intercept reflects the grand mean of the Guiraud values for both argumentative and narrative texts for all three groups written at T2. The TimeT1 estimate (-0.20) shows the difference between T1 and T2 averaged over all text types and all groups (0.20 points worse at T1); the TimeT3 estimate (0.56) shows the difference between T2 and T3 averaged over all text types and all groups (0.56 points better at T3).

TextType1 (0.05) shows that the mean Guiraud value of one text type (still written at T2!) averaged over all groups is 0.05 points higher than the grand mean; and by implication that the mean Guiraud value of the other text type is 0.05 lower than the grand mean. To find out which text type is which, use contrasts():

contrasts(d$TextType)
              [,1]
argumentative    1
narrative       -1

Since argumentative is coded as 1, it’s the argumentative texts that have the higher Guiraud values at T2.

Similarly, Group1 (0.98) shows that one group has higher-than-average Guiraud values averaged across text types at T2, whereas Group2 (-0.38) shows that another group has a mean Guiraud value that lies 0.38 points below the average at T2. By implication, the third group’s mean Guiraud value lies 0.60 points below average ((0.98-0.38-0.60)/3 = 0). To see which group is which, use contrasts():

contrasts(d$Group)
                       [,1] [,2]
monolingual Portuguese    1    0
Portuguese-French         0    1
Portuguese-German        -1   -1

monolingual Portuguese is ‘1’ for the purposes of Group1, Portuguese-French is 1 for the purposes of Group2, and Portuguese-German is the third group.

We can double-check these numbers by generating the modelled mean values for each predictor value combination:

double_check <- cbind(
  d_pred, 
  fitted(m_recoded, 
         newdata = d_pred, 
         re_formula = NA)
  )
double_check
                    Group Time      TextType Estimate Est.Error Q2.5 Q97.5
1  monolingual Portuguese   T1 argumentative     5.25     0.147 4.96  5.54
2  monolingual Portuguese   T1     narrative     4.98     0.166 4.65  5.32
3  monolingual Portuguese   T2 argumentative     5.82     0.142 5.54  6.10
4  monolingual Portuguese   T2     narrative     5.31     0.159 4.99  5.62
5  monolingual Portuguese   T3 argumentative     6.16     0.169 5.82  6.49
6  monolingual Portuguese   T3     narrative     5.84     0.176 5.48  6.18
7       Portuguese-French   T1 argumentative     4.19     0.128 3.94  4.45
8       Portuguese-French   T1     narrative     4.06     0.163 3.75  4.38
9       Portuguese-French   T2 argumentative     4.21     0.125 3.97  4.47
10      Portuguese-French   T2     narrative     4.21     0.138 3.94  4.48
11      Portuguese-French   T3 argumentative     4.73     0.135 4.47  5.00
12      Portuguese-French   T3     narrative     4.80     0.152 4.49  5.10
13      Portuguese-German   T1 argumentative     3.95     0.116 3.71  4.18
14      Portuguese-German   T1     narrative     3.87     0.147 3.58  4.16
15      Portuguese-German   T2 argumentative     3.88     0.115 3.65  4.10
16      Portuguese-German   T2     narrative     4.11     0.148 3.81  4.40
17      Portuguese-German   T3 argumentative     4.63     0.137 4.36  4.90
18      Portuguese-German   T3     narrative     4.75     0.141 4.47  5.03

Some sanity checks:

  1. Intercept = 4.59 = grand mean at T2:
double_check |> 
  filter(Time == "T2") |> 
  summarise(mean_est = mean(Estimate))
  mean_est
1     4.59
  1. TimeT3 = 0.56 = T2/T3 difference across texts and groups:
double_check |> 
  group_by(Time) |> 
  summarise(mean_est = mean(Estimate)) |> 
  spread(Time, mean_est) |> 
  summarise(diff_T2T3 = T3 - T2)
# A tibble: 1 × 1
  diff_T2T3
      <dbl>
1     0.562
  1. Portuguese-German lies 0.60 below average at T2 across texts:
double_check |> 
  filter(Time == "T2") |> 
  group_by(Group) |> 
  summarise(mean_est = mean(Estimate)) |> 
  mutate(diff_mean = mean_est - mean(mean_est))
# A tibble: 3 × 3
  Group                  mean_est diff_mean
  <fct>                     <dbl>     <dbl>
1 monolingual Portuguese     5.56     0.975
2 Portuguese-French          4.21    -0.378
3 Portuguese-German          3.99    -0.597

I won’t plot the modelled averages and their uncertainty, because the result will be the same as before: Recoding the predictors in this way doesn’t affect the modelled averages per cell; it just makes the summary output easier to parse.

Homing in on specific comparisons

Finally, let’s see how we can target some specific comparisons without having to refit the model several times. A specific comparison you might be interested in could be “How large is the difference in Guiraud scores for narrative texts written by Portuguese-French bilinguals between T1 and T2?” Or a more complicated one: “How large is the difference in the progression from T1 to T3 for argumentative texts between Portuguese-French and Portuguese-German children?”

To answer such questions, we need to generate the distribution of the modelled averages per predictor value combination:

posterior_fit <- posterior_linpred(m_recoded, newdata = d_pred, re_formula = NA)

Question 1: Progression T1-T2 for narrative texts, Portuguese-French bilinguals?

This question requires us to compare the modelled average for narrative texts written by Portuguese-French bilinguals at T2 to that of the narrative texts written by Portuguese-French bilinguals at T1. The first combination of predictor values can be found in row 10 in d_pred, so the corresponding estimates are in column 10 in posterior_fit. The second combination of predictor values can be found in row 8 in d_pred, so the corresponding estimates are in column 8 in posterior_fit.

t2 <- posterior_fit[, 10]
t1 <- posterior_fit[, 8]
df <- data.frame(t2, t1)

Now compute and plot the pairwise differences:

df <- df |> 
  mutate(progression = t2 - t1)
ggplot(df,
       aes(x = progression)) +
  geom_histogram(bins = 50, fill = "lightgrey", colour = "black") +
  theme_bw()

Figure 3. Estimate of the progression in Guiraud values for narrative texts by Portuguese-French bilinguals from T1 to T2.

The mean progression is easily calculated:

mean(df$progression)
[1] 0.148

The estimated error for this estimate is:

sd(df$progression)
[1] 0.149

And its 95% uncertainty interval is:

quantile(df$progression, probs = c(0.025, 0.975))
  2.5%  97.5% 
-0.152  0.436 

According to the model, there’s about a 84% chance that there’s indeed some progression going from T1 to T2.

mean(df$progression > 0)
[1] 0.845

Question 2: T1-T3 progression for argumentative texts, Portuguese-French vs. Portuguese-German?

This question requires us to take into consideration the modelled average for argumentative texts written by Portuguese-French bilinguals at T1, that for argumentative texts written by Portuguese-French bilinguals at T3, and the same for the texts written by Portuguese-German bilinguals. We need the following columns in posterior_fit:

  • 7 (Portuguese-French, T1, argumentative)
  • 11 (Portuguese-French, T3, argumentative)
  • 13 (Portuguese-German, T1, argumentative)
  • 17 (Portuguese-German, T3, argumentative)
fr_t1 <- posterior_fit[, 7]
fr_t3 <- posterior_fit[, 11]
gm_t1 <- posterior_fit[, 13]
gm_t3 <- posterior_fit[, 17]
df <- data.frame(fr_t1, fr_t3, gm_t1, gm_t3)

We compute the progression for the Portuguese-French bilinguals and that for the Portuguese-German bilinguals. Then we compute the difference between these progressions:

df <- df |> 
  mutate(prog_fr = fr_t3 - fr_t1,
         prog_gm = gm_t3 - gm_t1,
         diff_prog = prog_gm - prog_fr)

The mean progression for the Portuguese-French bilinguals was 0.54 compared to 0.68 for the Portuguese-German bilinguals:

mean(df$prog_fr)
[1] 0.542
mean(df$prog_gm)
[1] 0.684

The mean difference between these progressions, then, is 0.14 in favour of the Portuguese-German bilinguals:

mean(df$diff_prog)
[1] 0.142

However, there is considerable uncertainty about this difference:

ggplot(df,
       aes(x = diff_prog)) +
  geom_histogram(bins = 50, fill = "lightgrey", colour = "black") +
  theme_bw()

The probability that the Portuguese-German bilinguals make more progress than the Portuguese-French bilinguals is 77%, and according to the model, there’s a 95% chance its size is somewhere between -0.25 and 0.52 points.

mean(df$diff_prog > 0)
[1] 0.768
quantile(df$diff_prog, probs = c(0.025, 0.975))
  2.5%  97.5% 
-0.251  0.530 

Summary

By investing some time in recoding your predictors, you can make the parameter estimates more relevant to your questions. Any specific comparisons you may be interested in can additionally be addressed by making use of the entire distribution of estimates. You can also use these estimate distributions to draw effect plots.

Further resources

References

Audrey Bonvin, Jan Vanhove, Raphael Berthele and Amelia Lambelet. 2018. Die Entwicklung von produktiven lexikalischen Kompetenzen bei Schüler(innen) mit portugiesischem Migrationshintergrund in der Schweiz. Zeitschrift für Interkulturellen Fremdsprachenunterricht 23(1). 135-148. Data and R code available from figshare.

Amelia Lambelet, Raphael Berthele, Magalie Desgrippes, Carlos Pestana and Jan Vanhove. 2017a. Chapter 2: Testing interdependence in Portuguese Heritage speakers in Switzerland: the HELASCOT project. In Raphael Berthele and Amelia Lambelet (eds.), Heritage and school language literacy development in migrant children: Interdependence or independence?, pp. 26-33. Multilingual Matters.

Amelia Lambelet, Magalie Desgrippes and Jan Vanhove. 2017b. Chapter 5: The development of argumentative and narrative writing skills in Portuguese heritage speakers in Switzerland (HELASCOT project). In Raphael Berthele and Amelia Lambelet (eds.), Heritage and school language literacy development in migrant children: Interdependence or independence?, pp. 83-96. Multilingual Matters.

Jan Vanhove, Audrey Bonvin, Amelia Lambelet and Raphael Berthele. 2019. Predicting perceptions of the lexical richness of short French, German, and Portuguese texts. Journal of Writing Research. Technical report, data (including texts), elicitation materials, and R code available from the Open Science Framework.

Session info

devtools::session_info()
─ Session info ───────────────────────────────────────────────────────────────
 setting  value
 version  R version 4.3.1 (2023-06-16)
 os       Ubuntu 22.04.2 LTS
 system   x86_64, linux-gnu
 ui       X11
 language en_US
 collate  en_US.UTF-8
 ctype    en_US.UTF-8
 tz       Europe/Zurich
 date     2023-08-06
 pandoc   3.1.1 @ /usr/lib/rstudio/resources/app/bin/quarto/bin/tools/ (via rmarkdown)

─ Packages ───────────────────────────────────────────────────────────────────
 package        * version date (UTC) lib source
 abind            1.4-5   2016-07-21 [1] CRAN (R 4.3.1)
 backports        1.4.1   2021-12-13 [1] CRAN (R 4.3.0)
 base64enc        0.1-3   2015-07-28 [1] CRAN (R 4.3.0)
 bayesplot        1.10.0  2022-11-16 [1] CRAN (R 4.3.1)
 bit              4.0.5   2022-11-15 [1] CRAN (R 4.3.0)
 bit64            4.0.5   2020-08-30 [1] CRAN (R 4.3.0)
 bridgesampling   1.1-2   2021-04-16 [1] CRAN (R 4.3.1)
 brms           * 2.19.0  2023-03-14 [1] CRAN (R 4.3.1)
 Brobdingnag      1.2-9   2022-10-19 [1] CRAN (R 4.3.1)
 cachem           1.0.6   2021-08-19 [2] CRAN (R 4.2.0)
 callr            3.7.3   2022-11-02 [1] CRAN (R 4.3.1)
 checkmate        2.2.0   2023-04-27 [1] CRAN (R 4.3.1)
 cli              3.6.1   2023-03-23 [1] CRAN (R 4.3.0)
 coda             0.19-4  2020-09-30 [1] CRAN (R 4.3.1)
 codetools        0.2-19  2023-02-01 [4] CRAN (R 4.2.2)
 colorspace       2.1-0   2023-01-23 [1] CRAN (R 4.3.0)
 colourpicker     1.2.0   2022-10-28 [1] CRAN (R 4.3.1)
 crayon           1.5.2   2022-09-29 [1] CRAN (R 4.3.1)
 crosstalk        1.2.0   2021-11-04 [1] CRAN (R 4.3.1)
 curl             4.3.2   2021-06-23 [2] CRAN (R 4.2.0)
 devtools         2.4.5   2022-10-11 [1] CRAN (R 4.3.1)
 digest           0.6.29  2021-12-01 [2] CRAN (R 4.2.0)
 distributional   0.3.2   2023-03-22 [1] CRAN (R 4.3.1)
 dplyr          * 1.1.2   2023-04-20 [1] CRAN (R 4.3.0)
 DT               0.28    2023-05-18 [1] CRAN (R 4.3.1)
 dygraphs         1.1.1.6 2018-07-11 [1] CRAN (R 4.3.1)
 ellipsis         0.3.2   2021-04-29 [2] CRAN (R 4.2.0)
 evaluate         0.15    2022-02-18 [2] CRAN (R 4.2.0)
 fansi            1.0.4   2023-01-22 [1] CRAN (R 4.3.1)
 farver           2.1.1   2022-07-06 [1] CRAN (R 4.3.0)
 fastmap          1.1.0   2021-01-25 [2] CRAN (R 4.2.0)
 forcats        * 1.0.0   2023-01-29 [1] CRAN (R 4.3.0)
 fs               1.5.2   2021-12-08 [2] CRAN (R 4.2.0)
 generics         0.1.3   2022-07-05 [1] CRAN (R 4.3.0)
 ggplot2        * 3.4.2   2023-04-03 [1] CRAN (R 4.3.0)
 glue             1.6.2   2022-02-24 [2] CRAN (R 4.2.0)
 gridExtra        2.3     2017-09-09 [1] CRAN (R 4.3.0)
 gtable           0.3.3   2023-03-21 [1] CRAN (R 4.3.0)
 gtools           3.9.4   2022-11-27 [1] CRAN (R 4.3.1)
 hms              1.1.3   2023-03-21 [1] CRAN (R 4.3.0)
 htmltools        0.5.5   2023-03-23 [1] CRAN (R 4.3.0)
 htmlwidgets      1.6.2   2023-03-17 [1] CRAN (R 4.3.1)
 httpuv           1.6.11  2023-05-11 [1] CRAN (R 4.3.1)
 igraph           1.5.0.1 2023-07-23 [1] CRAN (R 4.3.1)
 inline           0.3.19  2021-05-31 [1] CRAN (R 4.3.1)
 jsonlite         1.8.5   2023-06-05 [1] CRAN (R 4.3.0)
 knitr            1.39    2022-04-26 [2] CRAN (R 4.2.0)
 labeling         0.4.2   2020-10-20 [1] CRAN (R 4.3.0)
 later            1.3.1   2023-05-02 [1] CRAN (R 4.3.1)
 lattice          0.21-8  2023-04-05 [4] CRAN (R 4.3.0)
 lifecycle        1.0.3   2022-10-07 [1] CRAN (R 4.3.0)
 loo              2.6.0   2023-03-31 [1] CRAN (R 4.3.1)
 lubridate      * 1.9.2   2023-02-10 [1] CRAN (R 4.3.0)
 magrittr         2.0.3   2022-03-30 [1] CRAN (R 4.3.0)
 markdown         1.7     2023-05-16 [1] CRAN (R 4.3.0)
 Matrix           1.6-0   2023-07-08 [4] CRAN (R 4.3.1)
 matrixStats      1.0.0   2023-06-02 [1] CRAN (R 4.3.1)
 memoise          2.0.1   2021-11-26 [2] CRAN (R 4.2.0)
 mime             0.10    2021-02-13 [2] CRAN (R 4.0.2)
 miniUI           0.1.1.1 2018-05-18 [1] CRAN (R 4.3.1)
 munsell          0.5.0   2018-06-12 [1] CRAN (R 4.3.0)
 mvtnorm          1.2-2   2023-06-08 [1] CRAN (R 4.3.1)
 nlme             3.1-162 2023-01-31 [4] CRAN (R 4.2.2)
 pillar           1.9.0   2023-03-22 [1] CRAN (R 4.3.0)
 pkgbuild         1.4.2   2023-06-26 [1] CRAN (R 4.3.1)
 pkgconfig        2.0.3   2019-09-22 [2] CRAN (R 4.2.0)
 pkgload          1.3.2.1 2023-07-08 [1] CRAN (R 4.3.1)
 plyr             1.8.8   2022-11-11 [1] CRAN (R 4.3.1)
 posterior        1.4.1   2023-03-14 [1] CRAN (R 4.3.1)
 prettyunits      1.1.1   2020-01-24 [2] CRAN (R 4.2.0)
 processx         3.8.2   2023-06-30 [1] CRAN (R 4.3.1)
 profvis          0.3.8   2023-05-02 [1] CRAN (R 4.3.1)
 promises         1.2.0.1 2021-02-11 [1] CRAN (R 4.3.1)
 ps               1.7.5   2023-04-18 [1] CRAN (R 4.3.1)
 purrr          * 1.0.1   2023-01-10 [1] CRAN (R 4.3.0)
 R6               2.5.1   2021-08-19 [2] CRAN (R 4.2.0)
 RColorBrewer     1.1-3   2022-04-03 [1] CRAN (R 4.3.0)
 Rcpp           * 1.0.11  2023-07-06 [1] CRAN (R 4.3.1)
 RcppParallel     5.1.7   2023-02-27 [1] CRAN (R 4.3.1)
 readr          * 2.1.4   2023-02-10 [1] CRAN (R 4.3.0)
 remotes          2.4.2   2021-11-30 [2] CRAN (R 4.2.0)
 reshape2         1.4.4   2020-04-09 [1] CRAN (R 4.3.1)
 rlang            1.1.1   2023-04-28 [1] CRAN (R 4.3.0)
 rmarkdown        2.21    2023-03-26 [1] CRAN (R 4.3.0)
 rstan            2.26.22 2023-08-01 [1] local
 rstantools       2.3.1.1 2023-07-18 [1] CRAN (R 4.3.1)
 rstudioapi       0.14    2022-08-22 [1] CRAN (R 4.3.0)
 scales           1.2.1   2022-08-20 [1] CRAN (R 4.3.0)
 sessioninfo      1.2.2   2021-12-06 [2] CRAN (R 4.2.0)
 shiny            1.7.4.1 2023-07-06 [1] CRAN (R 4.3.1)
 shinyjs          2.1.0   2021-12-23 [1] CRAN (R 4.3.1)
 shinystan        2.6.0   2022-03-03 [1] CRAN (R 4.3.1)
 shinythemes      1.2.0   2021-01-25 [1] CRAN (R 4.3.1)
 StanHeaders      2.26.27 2023-06-14 [1] CRAN (R 4.3.1)
 stringi          1.7.6   2021-11-29 [2] CRAN (R 4.2.0)
 stringr        * 1.5.0   2022-12-02 [1] CRAN (R 4.3.0)
 tensorA          0.36.2  2020-11-19 [1] CRAN (R 4.3.1)
 threejs          0.3.3   2020-01-21 [1] CRAN (R 4.3.1)
 tibble         * 3.2.1   2023-03-20 [1] CRAN (R 4.3.0)
 tidyr          * 1.3.0   2023-01-24 [1] CRAN (R 4.3.0)
 tidyselect       1.2.0   2022-10-10 [1] CRAN (R 4.3.0)
 tidyverse      * 2.0.0   2023-02-22 [1] CRAN (R 4.3.1)
 timechange       0.2.0   2023-01-11 [1] CRAN (R 4.3.0)
 tzdb             0.4.0   2023-05-12 [1] CRAN (R 4.3.0)
 urlchecker       1.0.1   2021-11-30 [1] CRAN (R 4.3.1)
 usethis          2.2.2   2023-07-06 [1] CRAN (R 4.3.1)
 utf8             1.2.3   2023-01-31 [1] CRAN (R 4.3.1)
 V8               4.3.0   2023-04-08 [1] CRAN (R 4.3.0)
 vctrs            0.6.3   2023-06-14 [1] CRAN (R 4.3.0)
 vroom            1.6.3   2023-04-28 [1] CRAN (R 4.3.0)
 withr            2.5.0   2022-03-03 [2] CRAN (R 4.2.0)
 xfun             0.39    2023-04-20 [1] CRAN (R 4.3.0)
 xtable           1.8-4   2019-04-21 [1] CRAN (R 4.3.1)
 xts              0.13.1  2023-04-16 [1] CRAN (R 4.3.1)
 yaml             2.3.5   2022-02-21 [2] CRAN (R 4.2.0)
 zoo              1.8-12  2023-04-13 [1] CRAN (R 4.3.1)

 [1] /home/jan/R/x86_64-pc-linux-gnu-library/4.3
 [2] /usr/local/lib/R/site-library
 [3] /usr/lib/R/site-library
 [4] /usr/lib/R/library

──────────────────────────────────────────────────────────────────────────────