4 Analysis
library(lavaan)
library(tidyverse)
library(here)
library(ggplot2)
library(scales)
library(showtext)
library(ggstance)
library(extrafont)
library(kableExtra)
Set figure options like before.
# Plotting options
Font <- "Titillium Web"
font_add_google(Font, Font)
theme_set(
theme_linedraw(
base_family = Font,
base_size = 12
) +
theme(
panel.grid.minor = element_blank(),
panel.grid.major.x = element_blank()
)
)
col1 <- "#2980b9"
col2 <- "#2980b9"
Load the cleaned data set from the previous section.
data_path <- here("data", "cleaned_data.rds")
if (file.exists(data_path)) {
d <- read_rds(file = data_path)
} else {
stop(str_glue("{data_path} doesn't exist, run `01-process.Rmd` to create it."))
}
4.1 Run model
The syntax for lavaan. Note that we constrain the cross-lagged effects to be the same, but within each game (so the effects can be different for the games). See explanation here: https://lavaan.ugent.be/tutorial/groups.html
riclpm_constrained <- [1058 chars quoted with '"']
Transform the data to wide format for lavaan to take the arguments above.
d_riclpm <- d %>%
select(
Game, pid, wid,
Hours, Angry
) %>%
# Long format on anger (outcome)
pivot_longer(
Angry,
names_to = "y_var", values_to = "y"
) %>%
# Long format on hours (predictor)
pivot_longer(
Hours,
names_to = "x_var", values_to = "x"
) %>%
pivot_wider(
names_from = wid,
values_from = c(x,y),
names_sep = ""
)
Fitting the model.
lavaan_fit <- lavaan(
riclpm_constrained,
data = d_riclpm,
missing = "ml",
meanstructure = TRUE,
int.ov.free = TRUE,
group = "Game"
)
Inspecting summary.
summary(lavaan_fit)
## lavaan 0.6-9 ended normally after 95 iterations
##
## Estimator ML
## Optimization method NLMINB
## Number of model parameters 52
## Number of equality constraints 8
##
## Number of observations per group:
## Apex Legends 1092
## Outriders 1488
## Number of missing patterns per group:
## Apex Legends 8
## Outriders 7
##
## Model Test User Model:
##
## Test statistic 55.775
## Degrees of freedom 10
## P-value (Chi-square) 0.000
## Test statistic for each group:
## Apex Legends 3.680
## Outriders 52.095
##
## Parameter Estimates:
##
## Standard errors Standard
## Information Observed
## Observed information based on Hessian
##
##
## Group 1 [Apex Legends]:
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|)
## RIx =~
## x1 1.000
## x2 1.000
## x3 1.000
## RIy =~
## y1 1.000
## y2 1.000
## y3 1.000
## wx1 =~
## x1 1.000
## wx2 =~
## x2 1.000
## wx3 =~
## x3 1.000
## wy1 =~
## y1 1.000
## wy2 =~
## y2 1.000
## wy3 =~
## y3 1.000
##
## Regressions:
## Estimate Std.Err z-value P(>|z|)
## wx2 ~
## wx1 (bxa) 0.457 0.053 8.677 0.000
## wy1 (gxa) 0.027 0.025 1.113 0.266
## wy2 ~
## wx1 (gya) -0.008 0.168 -0.048 0.961
## wy1 (bya) 0.040 0.128 0.310 0.756
## wx3 ~
## wx2 (bxa) 0.457 0.053 8.677 0.000
## wy2 (gxa) 0.027 0.025 1.113 0.266
## wy3 ~
## wx2 (gya) -0.008 0.168 -0.048 0.961
## wy2 (bya) 0.040 0.128 0.310 0.756
##
## Covariances:
## Estimate Std.Err z-value P(>|z|)
## wx1 ~~
## wy1 0.052 0.045 1.158 0.247
## .wx2 ~~
## .wy2 0.018 0.046 0.396 0.692
## .wx3 ~~
## .wy3 0.048 0.040 1.209 0.227
## RIx ~~
## RIy 0.011 0.048 0.232 0.816
##
## Intercepts:
## Estimate Std.Err z-value P(>|z|)
## .x1 0.765 0.025 30.549 0.000
## .x2 0.671 0.026 26.141 0.000
## .x3 0.670 0.025 26.777 0.000
## .y1 3.422 0.048 71.715 0.000
## .y2 3.237 0.069 46.638 0.000
## .y3 3.235 0.092 35.062 0.000
## RIx 0.000
## RIy 0.000
## wx1 0.000
## .wx2 0.000
## .wx3 0.000
## wy1 0.000
## .wy2 0.000
## .wy3 0.000
##
## Variances:
## Estimate Std.Err z-value P(>|z|)
## RIx 0.386 0.035 11.145 0.000
## RIy 1.300 0.155 8.388 0.000
## wx1 0.298 0.030 10.030 0.000
## wy1 1.124 0.150 7.509 0.000
## .wx2 0.269 0.017 15.519 0.000
## .wy2 1.028 0.186 5.522 0.000
## .wx3 0.226 0.013 16.995 0.000
## .wy3 1.107 0.151 7.318 0.000
## .x1 0.000
## .x2 0.000
## .x3 0.000
## .y1 0.000
## .y2 0.000
## .y3 0.000
##
##
## Group 2 [Outriders]:
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|)
## RIx =~
## x1 1.000
## x2 1.000
## x3 1.000
## RIy =~
## y1 1.000
## y2 1.000
## y3 1.000
## wx1 =~
## x1 1.000
## wx2 =~
## x2 1.000
## wx3 =~
## x3 1.000
## wy1 =~
## y1 1.000
## wy2 =~
## y2 1.000
## wy3 =~
## y3 1.000
##
## Regressions:
## Estimate Std.Err z-value P(>|z|)
## wx2 ~
## wx1 (bxo) 0.553 0.042 13.091 0.000
## wy1 (gxo) 0.016 0.027 0.609 0.543
## wy2 ~
## wx1 (gyo) -0.024 0.059 -0.414 0.679
## wy1 (byo) 0.025 0.115 0.220 0.826
## wx3 ~
## wx2 (bxo) 0.553 0.042 13.091 0.000
## wy2 (gxo) 0.016 0.027 0.609 0.543
## wy3 ~
## wx2 (gyo) -0.024 0.059 -0.414 0.679
## wy2 (byo) 0.025 0.115 0.220 0.826
##
## Covariances:
## Estimate Std.Err z-value P(>|z|)
## wx1 ~~
## wy1 0.078 0.075 1.043 0.297
## .wx2 ~~
## .wy2 -0.034 0.040 -0.840 0.401
## .wx3 ~~
## .wy3 0.029 0.028 1.038 0.299
## RIx ~~
## RIy -0.042 0.050 -0.838 0.402
##
## Intercepts:
## Estimate Std.Err z-value P(>|z|)
## .x1 0.666 0.030 22.512 0.000
## .x2 0.373 0.023 16.503 0.000
## .x3 0.207 0.017 12.372 0.000
## .y1 2.831 0.037 76.839 0.000
## .y2 2.838 0.059 47.900 0.000
## .y3 2.782 0.065 42.942 0.000
## RIx 0.000
## RIy 0.000
## wx1 0.000
## .wx2 0.000
## .wx3 0.000
## wy1 0.000
## .wy2 0.000
## .wy3 0.000
##
## Variances:
## Estimate Std.Err z-value P(>|z|)
## RIx -0.179 0.108 -1.659 0.097
## RIy 0.918 0.102 8.963 0.000
## wx1 1.483 0.140 10.570 0.000
## wy1 1.097 0.099 11.081 0.000
## .wx2 0.485 0.021 23.505 0.000
## .wy2 0.750 0.167 4.491 0.000
## .wx3 0.310 0.026 11.985 0.000
## .wy3 0.937 0.100 9.368 0.000
## .x1 0.000
## .x2 0.000
## .x3 0.000
## .y1 0.000
## .y2 0.000
## .y3 0.000
Get parameter estimates for plot. Note: The standardized estimates aren’t the same across waves because we didn’t put equality constraints on the variances – only the unstandardized ones are identical.
params <-
bind_rows(
parameterestimates(lavaan_fit) %>%
mutate(Type = "Unstandardized"),
standardizedsolution(lavaan_fit) %>%
rename(est = est.std) %>%
mutate(Type = "Standardized")
) %>%
as_tibble() %>%
rename(Game = group) %>%
mutate(
Game = factor(if_else(Game == 1, "Apex Legends", "Outriders"))
) %>%
mutate(
label = if_else(lhs == "RIx" & rhs == "RIy", "Covariance", label)
) %>%
mutate(
Outcome = case_when(
str_starts(lhs, "wx") ~ "Hours",
str_starts(lhs, "wy") ~ "Angry",
TRUE ~ NA_character_
),
Predictor = case_when(
str_starts(rhs, "wx") ~ "Hours",
str_starts(rhs, "wy") ~ "Angry",
TRUE ~ NA_character_
)
) %>%
mutate(
`Parameter type` = case_when(
lhs == "RIx" & rhs == "RIy" ~ "Covariance",
Predictor == Outcome ~ "Autoregression",
Predictor != Outcome ~ "Cross-lagged",
TRUE ~ NA_character_
)
) %>%
mutate(
Direction = case_when(
`Parameter type` == "Cross-lagged" ~ str_glue('{Predictor}[plain("[t-1]")]%->%{Outcome}[plain("[t]")]'),
TRUE ~ NA_character_
),
Direction = as.factor(Direction)
) %>%
filter(!label == "") %>%
mutate(
across(
c(est, ci.lower, ci.upper),
~ round(.x, digits = 2)
)
) %>%
select(
Predictor,
Outcome,
`Parameter type`,
Direction,
Type,
Game,
Estimate = est,
`Lower CI` = ci.lower,
`Upper CI` = ci.upper
) %>%
distinct() %>%
mutate(
across(
c(Predictor, Outcome, Direction),
~ as.factor(str_replace(.x, "Angry", "`Aggressive affect`")) # for labelling figure
)
)
params %>%
select(-Direction) %>%
mutate( # temporarily remove backticks (purely cosmetic, for table below)
across(
c(Predictor, Outcome),
~ str_replace(.x, "`Aggressive affect`", "Aggressive affect") # for labelling figure
)
) %>%
kbl(caption = "Parameter from RICLPM") %>%
kable_styling(full_width = FALSE, font_size = 12)
Predictor | Outcome | Parameter type | Type | Game | Estimate | Lower CI | Upper CI |
---|---|---|---|---|---|---|---|
Hours | Hours | Autoregression | Unstandardized | Apex Legends | 0.46 | 0.35 | 0.56 |
Aggressive affect | Hours | Cross-lagged | Unstandardized | Apex Legends | 0.03 | -0.02 | 0.08 |
Hours | Aggressive affect | Cross-lagged | Unstandardized | Apex Legends | -0.01 | -0.34 | 0.32 |
Aggressive affect | Aggressive affect | Autoregression | Unstandardized | Apex Legends | 0.04 | -0.21 | 0.29 |
NA | NA | Covariance | Unstandardized | Apex Legends | 0.01 | -0.08 | 0.10 |
Hours | Hours | Autoregression | Unstandardized | Outriders | 0.55 | 0.47 | 0.64 |
Aggressive affect | Hours | Cross-lagged | Unstandardized | Outriders | 0.02 | -0.04 | 0.07 |
Hours | Aggressive affect | Cross-lagged | Unstandardized | Outriders | -0.02 | -0.14 | 0.09 |
Aggressive affect | Aggressive affect | Autoregression | Unstandardized | Outriders | 0.03 | -0.20 | 0.25 |
NA | NA | Covariance | Unstandardized | Outriders | -0.04 | -0.14 | 0.06 |
Hours | Hours | Autoregression | Standardized | Apex Legends | 0.43 | 0.34 | 0.52 |
Aggressive affect | Hours | Cross-lagged | Standardized | Apex Legends | 0.05 | -0.04 | 0.14 |
Hours | Aggressive affect | Cross-lagged | Standardized | Apex Legends | 0.00 | -0.18 | 0.17 |
Aggressive affect | Aggressive affect | Autoregression | Standardized | Apex Legends | 0.04 | -0.22 | 0.30 |
Hours | Hours | Autoregression | Standardized | Apex Legends | 0.48 | 0.38 | 0.59 |
Hours | Aggressive affect | Cross-lagged | Standardized | Apex Legends | 0.00 | -0.18 | 0.18 |
Aggressive affect | Aggressive affect | Autoregression | Standardized | Apex Legends | 0.04 | -0.21 | 0.28 |
NA | NA | Covariance | Standardized | Apex Legends | 0.02 | -0.12 | 0.15 |
Hours | Hours | Autoregression | Standardized | Outriders | 0.69 | 0.62 | 0.77 |
Aggressive affect | Hours | Cross-lagged | Standardized | Outriders | 0.02 | -0.04 | 0.08 |
Hours | Aggressive affect | Cross-lagged | Standardized | Outriders | -0.03 | -0.20 | 0.13 |
Aggressive affect | Aggressive affect | Autoregression | Standardized | Outriders | 0.03 | -0.24 | 0.30 |
Hours | Aggressive affect | Cross-lagged | Standardized | Outriders | -0.02 | -0.14 | 0.09 |
Aggressive affect | Aggressive affect | Autoregression | Standardized | Outriders | 0.02 | -0.18 | 0.23 |
NA | NA | Covariance | Standardized | Outriders | -0.10 | -0.35 | 0.14 |
4.2 Figure 4
Then a forest plot of the unstandardized cross-lagged effects.
text_estimates <-
params %>%
filter(Type == "Unstandardized", `Parameter type` == "Cross-lagged") %>%
mutate(
label = str_glue("{Estimate} [{`Lower CI`}, {`Upper CI`}]"),
x = rep(0.25, 4),
y = c(rep(2.1, 2), rep(1.1, 2))
)
params %>%
filter(Type == "Unstandardized", `Parameter type` == "Cross-lagged") %>%
mutate(
Direction = fct_rev(Direction), # not in alphabetical order
Game = fct_rev(Game),
estimate_text = str_glue("{Estimate} [{`Lower CI`}, {`Upper CI`}]")
) %>%
ggplot(aes(Estimate, Game)) +
geom_vline(xintercept = 0, lty = 2, size = .25) +
geom_pointrange(
aes(xmin = `Lower CI`, xmax = `Upper CI`),
size = 0.8
) +
scale_x_continuous(
breaks = pretty_breaks()
) +
xlab("Estimated cross-lagged effect [95%CI]") +
xlim(-0.4, 0.4) +
facet_wrap(
~ Direction,
labeller = labeller(.rows = label_parsed)
) +
geom_text(
data = text_estimates,
mapping = aes(x = x, y = y, label = label)
) +
theme(
aspect.ratio = 1,
axis.title.y = element_blank(),
panel.grid.major.y = element_line(color = "lightgrey"),
)