Comparison of time to stabilisation and muscle activation during jump landings in individuals with ACL reconstruction versus a healthy control group - A cross-sectional study

MSc Thesis

Author

Syra Naomi Schmid

Published

April 1, 2026

Code
library(pacman)
pacman::p_load(rio,
  readxl, #import excel file
  tidyverse, # data manipulation
  dplyr, # data manipulation
  flextable, #to develop tables for data export
  gtsummary, # summary table
  optmatch, # matching of participants
  ggplot2, # plots
  ggpubr, # ggplots for publications
  janitor, # clean data
  qqplotr, DataExplorer, # qqplot
  mosaic, # fast data summary
  Hmisc, # descriptive statistics
  lmerTest, # model development
  lme4, # model development
  purrr,
  emmeans, # contrasts
  effects, # contrasts plot
  parameters, # model output
  effects) #Plot lmm

1 Research question

  • Research question 1: Are there differences in muscle activation (pre- and post-initial contact) and TTS after a SLHD between the reconstructed leg, the contralateral leg, and healthy control legs?

2 Data import

Code
df.healthy.raw <- read_excel("../data/CRF_digital_healthy_V2.xlsx")
df.acl.raw <- read_excel("../data/CRF_digital_acl_V1.xlsx")
Code
# summary(df.healthy.raw)
# summary(df.acl.raw)
# str(df.healthy.raw)
# str(df.acl.raw)

3 Data preparation

Code
df.healthy.adapted <- df.healthy.raw %>%
  mutate(
    ID = as.factor (ID),
    age = as.numeric(Age),
    gender = recode(Gender,
                    "M" = "Male",
                    "F" = "Female"),
    height = as.numeric(df.healthy.raw$`Height`),
    weight = as.numeric(df.healthy.raw$`Weight`),
    bmi = as.numeric(df.healthy.raw$`Weight`/((df.healthy.raw$`Height`/100)^2)),
    LSI_Qceps = as.numeric(df.healthy.raw$`LSI_Qceps`),
    LSI_Hams = as.numeric(df.healthy.raw$`LSI_Hams`),
    LSI_SLH= as.numeric(df.healthy.raw$`LSI_SLH`),
    KOOS_total = as.numeric(df.healthy.raw$`KOOS_total`),
    Activity_min = as.numeric(df.healthy.raw$`Activity_min`),
    Tegner = as.numeric(df.healthy.raw$Tegner),
    Dominant_leg = as.factor (Dominant_leg))

df.healthy.adapted$Injured_leg <- as.factor ("none")

# select necessary variables
df.healthy.demo <- df.healthy.adapted %>%
  select(ID, age, gender, height, weight, bmi, 
         LSI_Qceps, LSI_Hams, LSI_SLH, KOOS_total, 
         Activity_min, Tegner, Dominant_leg, Injured_leg)
Code
df.acl.adapted <- df.acl.raw %>%
  mutate(
    ID = as.factor (ID),
    age = as.numeric(Age),
    gender = recode(Gender,
                    "m" = "Male",
                    "w" = "Female",
                    "d" = "Diverse"),
    height = as.numeric(df.acl.raw$`Height`),
    weight = as.numeric(df.acl.raw$Weight),
    LSI_Qceps = as.numeric(df.acl.raw$`LSI_Qceps`),
    LSI_Hams = as.numeric(df.acl.raw$`LSI_Hams`),
    LSI_SLH= as.numeric(df.acl.raw$`LSI_SLH`),
    KOOS_total = as.numeric(df.acl.raw$`KOOS_total`),
    Activity_min = as.numeric(df.acl.raw$`Activity_M0`),
    Tegner = as.numeric(df.acl.raw$Tegner_M0),
    Dominant_leg = as.factor(Dominant_leg),
    Injured_leg = as.factor(Injured_leg))

df.acl.adapted <- df.acl.adapted %>%
  mutate(
    bmi = as.numeric(df.acl.adapted$`weight`/((df.acl.adapted$`height`/100)^2)))

# select necessary variables and omit na values (ID 7 and 15)
df.acl.demo <- df.acl.adapted %>%
  select(ID, age, gender, height, weight, bmi, 
         LSI_Qceps, LSI_Hams, LSI_SLH, KOOS_total, 
         Activity_min, Tegner, Dominant_leg, Injured_leg) %>% 
  drop_na()
Code
df.healthy.demo <- df.healthy.demo %>%
  mutate(group= as.factor("Healthy"))
df.acl.demo <- df.acl.demo %>%
  mutate(group= as.factor("ACL"))

df.merge.demo <- bind_rows(df.healthy.demo, df.acl.demo)


# DataExplorer::create_report(df.merge.demo)

4 Demographics

Code
df.merge.table1 <- df.merge.demo %>% 
  select(-ID, -Injured_leg, -Dominant_leg)



tbl1 <- df.merge.table1  %>%
  gtsummary::tbl_summary(
    by = group,
    statistic = list(
      all_continuous() ~ "{mean} ({sd})",
      all_categorical()~ "{n} ({p}%)"),
    label = list(
      group = "Group",
      gender = "Sex",
      age = "Age",
      height = "Height",
      weight = "Weight",
      bmi = "Body mass index",
      LSI_Qceps = "LSI Quadriceps (%)",
      LSI_Hams = "LSI Hamstring (%)",
      LSI_SLH = "LSI Single leg hop (%)",
      KOOS_total = "KOOS total",
      Activity_min = "Activity minutes per week",
      Tegner = "Tegner Activity Scale"
    ),
    missing_text = "(Missing)",
    digits = all_continuous()~ 2
  ) %>%
  gtsummary::add_overall(last = TRUE) %>%
  bold_labels()


tbl1 %>%
  as_flex_table() %>%
  align(j = 1, align = "left", part = "all") %>% 
  flextable::set_table_properties(width = 1, layout = "autofit") %>% 
  flextable::fontsize(size = 12, part = "all") %>% 
  flextable::font(fontname = "Times New Roman", part = "all")

Characteristic

Healthy
N = 121

ACL
N = 131

Overall
N = 251

Age

28.17 (3.95)

27.85 (5.64)

28.00 (4.80)

Sex

Female

5 (42%)

4 (31%)

9 (36%)

Male

7 (58%)

9 (69%)

16 (64%)

Height

175.33 (9.11)

175.62 (5.16)

175.48 (7.17)

Weight

72.62 (11.88)

75.46 (12.88)

74.10 (12.24)

Body mass index

23.49 (2.47)

24.45 (3.89)

23.99 (3.25)

LSI Quadriceps (%)

95.27 (12.37)

78.61 (13.28)

86.61 (15.18)

LSI Hamstring (%)

94.13 (10.92)

100.18 (10.63)

97.28 (10.99)

LSI Single leg hop (%)

100.67 (6.06)

91.64 (5.95)

95.97 (7.47)

KOOS total

97.23 (2.33)

83.11 (6.54)

89.89 (8.70)

Activity minutes per week

429.17 (188.33)

296.63 (171.95)

360.25 (188.70)

Tegner Activity Scale

3

1 (8.3%)

1 (7.7%)

2 (8.0%)

4

3 (25%)

7 (54%)

10 (40%)

5

5 (42%)

2 (15%)

7 (28%)

6

1 (8.3%)

0 (0%)

1 (4.0%)

7

2 (17%)

2 (15%)

4 (16%)

9

0 (0%)

1 (7.7%)

1 (4.0%)

1Mean (SD); n (%)

Code
tbl1 %>%
  as_flex_table() %>%
  set_caption("Table 1. Demographics") %>%
  flextable::set_table_properties(width = 1, layout = "autofit") %>% 
  flextable::fontsize(size = 8, part = "all") %>% 
  flextable::font(fontname = "Times New Roman", part = "all") %>%
  flextable::save_as_docx(path = "../tables/table1.by.group.docx")
Code
df.merge.drop <- df.merge.demo %>% 
  filter(!ID %in% c("SKHG_10", "SKH_03", "SKH_04"))

df.merge.table1.drop <- df.merge.drop %>% 
  select(-ID, -Injured_leg, -Dominant_leg)

tbl1.drop <- df.merge.table1.drop  %>%
  gtsummary::tbl_summary(
    by = group,
    statistic = list(
      all_continuous() ~ "{mean} ({sd})",
      all_categorical()~ "{n} ({p}%)"),
    label = list(
      group = "Group",
      gender = "Sex",
      age = "Age",
      height = "Height",
      weight = "Weight",
      bmi = "Body mass index",
      LSI_Qceps = "LSI Quadriceps (%)",
      LSI_Hams = "LSI Hamstring (%)",
      LSI_SLH = "LSI Single leg hop (%)",
      KOOS_total = "KOOS total",
      Activity_min = "Activity minutes per week",
      Tegner = "Tegner Activity Scale"
    ),
    missing_text = "(Missing)",
    digits = all_continuous()~ 2
  ) %>%
  gtsummary::add_overall(last = TRUE) %>%
  bold_labels()


tbl1.drop %>%
  as_flex_table() %>%
  align(j = 1, align = "left", part = "all") %>% 
  flextable::set_table_properties(width = 1, layout = "autofit") %>% 
  flextable::fontsize(size = 12, part = "all") %>% 
  flextable::font(fontname = "Times New Roman", part = "all")

Characteristic

Healthy
N = 111

ACL
N = 111

Overall
N = 221

Age

27.82 (3.95)

26.09 (3.96)

26.95 (3.96)

Sex

Female

4 (36%)

4 (36%)

8 (36%)

Male

7 (64%)

7 (64%)

14 (64%)

Height

175.36 (9.55)

174.95 (5.16)

175.16 (7.50)

Weight

73.05 (12.36)

71.29 (8.35)

72.17 (10.33)

Body mass index

23.61 (2.55)

23.33 (3.03)

23.47 (2.73)

LSI Quadriceps (%)

96.16 (12.56)

79.81 (13.68)

87.98 (15.31)

LSI Hamstring (%)

94.89 (11.12)

101.10 (11.31)

98.00 (11.40)

LSI Single leg hop (%)

100.17 (6.09)

92.62 (5.96)

96.39 (7.03)

KOOS total

97.22 (2.44)

82.34 (6.14)

89.78 (8.88)

Activity minutes per week

442.27 (191.69)

316.93 (172.89)

379.60 (189.33)

Tegner Activity Scale

3

1 (9.1%)

1 (9.1%)

2 (9.1%)

4

3 (27%)

5 (45%)

8 (36%)

5

5 (45%)

2 (18%)

7 (32%)

7

2 (18%)

2 (18%)

4 (18%)

9

0 (0%)

1 (9.1%)

1 (4.5%)

1Mean (SD); n (%)

Code
tbl1.drop %>%
  as_flex_table() %>%
  set_caption("Table 1. Demographics - post matching") %>%
  flextable::set_table_properties(width = 1, layout = "autofit") %>% 
  flextable::fontsize(size = 8, part = "all") %>% 
  flextable::font(fontname = "Times New Roman", part = "all") %>%
  flextable::save_as_docx(path = "../tables/table1.by.group.matched.docx")

4.1 T-Test age, height, weight

Code
t_test_age <- t.test(age ~ group, data = df.merge.drop)
t_test_age

    Welch Two Sample t-test

data:  age by group
t = 1.0247, df = 20, p-value = 0.3177
alternative hypothesis: true difference in means between group Healthy and group ACL is not equal to 0
95 percent confidence interval:
 -1.788877  5.243423
sample estimates:
mean in group Healthy     mean in group ACL 
             27.81818              26.09091 
Code
ggplot(df.merge.drop, aes(x = group, y = age, fill = group, colour = group)) +
  geom_boxplot(alpha = 0.6, width = 0.5, outlier.shape = NA) +
  geom_jitter(width = 0.1, alpha = 0.5) +
  scale_fill_manual(values = c(
    "ACL" = "black",
    "Healthy" = "grey60"
  )) +
  scale_color_manual(values = c(
    "ACL" = "black",
    "Healthy" = "grey60"
  )) +
  labs(
    title = "Age: ACL vs. Healthy",
    x = "Group",
    y = "Age (years)"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

Code
t_test_height <- t.test(height ~ group, data = df.merge.drop)
t_test_height

    Welch Two Sample t-test

data:  height by group
t = 0.12494, df = 15.386, p-value = 0.9022
alternative hypothesis: true difference in means between group Healthy and group ACL is not equal to 0
95 percent confidence interval:
 -6.554585  7.372767
sample estimates:
mean in group Healthy     mean in group ACL 
             175.3636              174.9545 
Code
ggplot(df.merge.drop, aes(x = group, y = height, fill = group, colour= group)) +
  geom_boxplot(alpha = 0.6, width = 0.5, outlier.shape = NA) +
  geom_jitter(width = 0.1, alpha = 0.5) +
  scale_fill_manual(values = c(
    "ACL" = "black",
    "Healthy" = "grey60"
  )) +
  scale_color_manual(values = c(
    "ACL" = "black",
    "Healthy" = "grey60"
  )) +
  labs(
    title = "Height: ACL vs. Healthy",
    x = "Group",
    y = "Height (cm)"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

Code
t_test_weight <- t.test(weight ~ group, data = df.merge.drop)
t_test_weight

    Welch Two Sample t-test

data:  weight by group
t = 0.39007, df = 17.548, p-value = 0.7012
alternative hypothesis: true difference in means between group Healthy and group ACL is not equal to 0
95 percent confidence interval:
 -7.712823 11.221914
sample estimates:
mean in group Healthy     mean in group ACL 
             73.04545              71.29091 
Code
ggplot(df.merge.drop, aes(x = group, y = weight, fill = group, colour= group)) +
  geom_boxplot(alpha = 0.6, width = 0.5, outlier.shape = NA) +
  geom_jitter(width = 0.1, alpha = 0.5) +
  scale_fill_manual(values = c(
    "ACL" = "black",
    "Healthy" = "grey60"
  )) +
  scale_color_manual(values = c(
    "ACL" = "black",
    "Healthy" = "grey60"
  )) +
  labs(
    title = "Weight: ACL vs. Healthy",
    x = "Group",
    y = "Weight (kg)"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

Code
t_test_age <- t.test(LSI_Qceps ~ group, data = df.merge.drop)
t_test_age

    Welch Two Sample t-test

data:  LSI_Qceps by group
t = 2.9195, df = 19.857, p-value = 0.00852
alternative hypothesis: true difference in means between group Healthy and group ACL is not equal to 0
95 percent confidence interval:
  4.662202 28.035147
sample estimates:
mean in group Healthy     mean in group ACL 
             96.15777              79.80909 
Code
ggplot(df.merge.drop, aes(x = group, y = LSI_Qceps, fill = group, colour = group)) +
  geom_boxplot(alpha = 0.6, width = 0.5, outlier.shape = NA) +
  geom_jitter(width = 0.1, alpha = 0.5) +
  scale_fill_manual(values = c(
    "ACL" = "black",
    "Healthy" = "grey60"
  )) +
  scale_color_manual(values = c(
    "ACL" = "black",
    "Healthy" = "grey60"
  )) +
  labs(
    title = "Age: ACL vs. Healthy",
    x = "Group",
    y = "Age (years)"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

Code
t_test_age <- t.test(LSI_Hams ~ group, data = df.merge.drop)
t_test_age

    Welch Two Sample t-test

data:  LSI_Hams by group
t = -1.2982, df = 19.994, p-value = 0.209
alternative hypothesis: true difference in means between group Healthy and group ACL is not equal to 0
95 percent confidence interval:
 -16.186281   3.768039
sample estimates:
mean in group Healthy     mean in group ACL 
             94.89088             101.10000 
Code
ggplot(df.merge.drop, aes(x = group, y = LSI_Hams, fill = group, colour = group)) +
  geom_boxplot(alpha = 0.6, width = 0.5, outlier.shape = NA) +
  geom_jitter(width = 0.1, alpha = 0.5) +
  scale_fill_manual(values = c(
    "ACL" = "black",
    "Healthy" = "grey60"
  )) +
  scale_color_manual(values = c(
    "ACL" = "black",
    "Healthy" = "grey60"
  )) +
  labs(
    title = "Age: ACL vs. Healthy",
    x = "Group",
    y = "Age (years)"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

Code
t_test_age <- t.test(LSI_SLH ~ group, data = df.merge.drop)
t_test_age

    Welch Two Sample t-test

data:  LSI_SLH by group
t = 2.9408, df = 19.991, p-value = 0.008084
alternative hypothesis: true difference in means between group Healthy and group ACL is not equal to 0
95 percent confidence interval:
  2.19504 12.90850
sample estimates:
mean in group Healthy     mean in group ACL 
            100.16904              92.61727 
Code
ggplot(df.merge.drop, aes(x = group, y = LSI_SLH, fill = group, colour = group)) +
  geom_boxplot(alpha = 0.6, width = 0.5, outlier.shape = NA) +
  geom_jitter(width = 0.1, alpha = 0.5) +
  scale_fill_manual(values = c(
    "ACL" = "black",
    "Healthy" = "grey60"
  )) +
  scale_color_manual(values = c(
    "ACL" = "black",
    "Healthy" = "grey60"
  )) +
  labs(
    title = "Age: ACL vs. Healthy",
    x = "Group",
    y = "Age (years)"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

Code
t_test_age <- t.test(KOOS_total ~ group, data = df.merge.drop)
t_test_age

    Welch Two Sample t-test

data:  KOOS_total by group
t = 7.4719, df = 13.093, p-value = 4.49e-06
alternative hypothesis: true difference in means between group Healthy and group ACL is not equal to 0
95 percent confidence interval:
 10.58396 19.18486
sample estimates:
mean in group Healthy     mean in group ACL 
             97.22426              82.33985 
Code
ggplot(df.merge.drop, aes(x = group, y = KOOS_total, fill = group, colour = group)) +
  geom_boxplot(alpha = 0.6, width = 0.5, outlier.shape = NA) +
  geom_jitter(width = 0.1, alpha = 0.5) +
  scale_fill_manual(values = c(
    "ACL" = "black",
    "Healthy" = "grey60"
  )) +
  scale_color_manual(values = c(
    "ACL" = "black",
    "Healthy" = "grey60"
  )) +
  labs(
    title = "Age: ACL vs. Healthy",
    x = "Group",
    y = "Age (years)"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

Code
t_test_age <- t.test(Activity_min ~ group, data = df.merge.drop)
t_test_age

    Welch Two Sample t-test

data:  Activity_min by group
t = 1.6104, df = 19.791, p-value = 0.1231
alternative hypothesis: true difference in means between group Healthy and group ACL is not equal to 0
95 percent confidence interval:
 -37.12246 287.81337
sample estimates:
mean in group Healthy     mean in group ACL 
             442.2727              316.9273 
Code
ggplot(df.merge.drop, aes(x = group, y = Activity_min, fill = group, colour = group)) +
  geom_boxplot(alpha = 0.6, width = 0.5, outlier.shape = NA) +
  geom_jitter(width = 0.1, alpha = 0.5) +
  scale_fill_manual(values = c(
    "ACL" = "black",
    "Healthy" = "grey60"
  )) +
  scale_color_manual(values = c(
    "ACL" = "black",
    "Healthy" = "grey60"
  )) +
  labs(
    title = "Age: ACL vs. Healthy",
    x = "Group",
    y = "Age (years)"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

5 Matching ACL to Control

Code
df.match.demo <- df.merge.demo %>% 
  filter(!is.na(ID), !is.na(group), !is.na(gender), !is.na(age)) %>%
  mutate(
    treat  = group == "ACL",
    gender = factor(trimws(gender))
  )

df.pairs.1to1 <- df.match.demo %>%
  group_by(gender, treat) %>%
  arrange(age, ID, .by_group = TRUE) %>%  # sort by age within gender+treat
  mutate(rank_in_group = row_number()) %>%
  ungroup() %>%
  group_by(gender, rank_in_group) %>%
  filter(n() == 2, sum(treat) == 1) %>%   # ensures 1 treated + 1 control
  ungroup() %>%
  mutate(subclass = as.integer(factor(paste(gender, rank_in_group)))) %>%
  arrange(subclass, desc(treat)) %>%
  select(subclass, ID, group, treat, gender, age, weight, height, Dominant_leg, Injured_leg)

#View(df.pairs.1to1)

##dropouts AFTER exact gender constraint (i.e., eligible but unmatched)
dropouts_after_gender <- df.match.demo %>%
  anti_join(df.pairs.1to1 %>% 
              distinct(ID), by = "ID") %>%
  group_by(gender) %>%
  mutate(reason = if_else(treat,
                          "Unmatched treated (not enough controls in gender)",
                          "Unmatched control (not enough treated in gender)")) %>%
  ungroup() %>%
  select(ID, group, treat, gender, age, reason)

#View(dropouts_after_gender)

5.1 Selecting Control Leg (CG)

Code
df.matched <- df.pairs.1to1 %>%
  group_by(subclass) %>%
  mutate(
    Injured_leg  = as.character(Injured_leg),
    Dominant_leg = as.character(Dominant_leg),
    acl_injured_leg  = Injured_leg[group == "ACL"][1],
    acl_dominant_leg = Dominant_leg[group == "ACL"][1],
    matched_leg = case_when(
      group == "ACL" ~ Injured_leg,
      group == "Healthy" & acl_injured_leg == acl_dominant_leg ~ Dominant_leg,
      group == "Healthy" ~ acl_injured_leg,
      TRUE ~ NA_character_)) %>%
  ungroup() %>%
  select(-acl_injured_leg, -acl_dominant_leg)

5.2 Prepare for merge (EMG and TTS)

Code
df.matched.long <- df.matched %>%
  crossing(Leg = c("L", "R")) %>%   # jede ID bekommt L und R
  mutate(
    group_ad = case_when(
      group == "ACL"     & Leg == Injured_leg  ~ "ACL-I",
      group == "ACL"     & Leg != Injured_leg  ~ "ACL-C",
      group == "Healthy" & Leg == matched_leg  ~ "CG-M",
      TRUE ~ NA_character_))

df.matched.long$ID <- sub("^SKH_", "SKH", df.matched.long$ID)
df.matched.long <- df.matched.long %>%
  rename(Side = Leg)
Code
save(df.matched.long, file="../data/df.demo.matched.rda")
write.csv(df.matched.long, "../data/df.demo.matched.csv")

6 Prepare data EMG and TTS

Code
# EMG average data
df.emg1davg <- rio::import("../../data/df.emg1davg.csv")
df.emgavg.long <- rio::import("../../data/df.emgavg.long.csv") # long

# TTS average data
df.ttsavg <- rio::import("../../data/df.ttsavg.csv")
df.tts1davg <- rio::import("../../data/df.tts1davg.csv")
Code
df.ttsavg.group <- df.ttsavg %>%
  mutate(
    Side = if_else(Condition == "SLHDL_M0", "L", "R"))

df.ttsavg.group <- df.ttsavg.group %>%
  mutate(
    ID   = str_trim(as.character(ID)),
    Side = str_to_upper(str_trim(as.character(Side)))) %>%
  left_join(df.matched.long%>%
      mutate(
        ID  = str_trim(as.character(ID)),
        Leg = str_to_upper(str_trim(as.character(Side))) # must be "L"/"R" to match Side
      ) %>%
      select(ID, Side, group_ad, gender, age),
    by = c("ID" = "ID", "Side" = "Side"))
                                                                                                
# remove not matched ID and not matched leg of CG
df.ttsavg.group <- df.ttsavg.group %>%
  filter(!is.na(group_ad))

##same for df.tts1davg
df.tts1davg.group <- df.tts1davg %>%
  mutate(
    Side = if_else(Condition == "SLHDL_M0", "L", "R"))

df.tts1davg.group <- df.tts1davg.group %>%
  mutate(
    ID   = str_trim(as.character(ID)),
    Side = str_to_upper(str_trim(as.character(Side)))) %>%
  left_join(
     df.matched.long%>%
      mutate(
        ID  = str_trim(as.character(ID)),
        Leg = str_to_upper(str_trim(as.character(Side))) # must be "L"/"R" to match Side
      ) %>%
      select(ID, Side, group_ad, gender, age),
    by = c("ID" = "ID", "Side" = "Side")
  )
# remove not matched ID and not matched leg of CG
df.tts1davg.group <- df.tts1davg.group %>%
  filter(!is.na(group_ad))
Code
df.emg.group <- df.emgavg.long %>%
  mutate(
    ID   = str_trim(as.character(ID)),
    Side = str_to_upper(str_trim(as.character(Side)))) %>%
  left_join(
     df.matched.long%>%
      mutate(
        ID  = str_trim(as.character(ID)),
        Leg = str_to_upper(str_trim(as.character(Side))) # must be "L"/"R" to match Side
      ) %>%
      select(ID, Side, group_ad, gender, age),
    by = c("ID" = "ID", "Side" = "Side")
  ) %>% 
  mutate(
    age = as.numeric(age),
    # age 18 as reference
    age_c = age - 18,
    gender = relevel(factor(gender), ref = "Male"))

# remove not matched ID and not matched leg of CG
df.emg <- df.emg.group %>%
  filter(!is.na(group_ad))

#same for df.emg1davg
df.emg1davg.group <- df.emg1davg %>%
  mutate(
    ID   = str_trim(as.character(ID)),
    Side = str_to_upper(str_trim(as.character(Side)))) %>%
  left_join(
     df.matched.long%>%
      mutate(
        ID  = str_trim(as.character(ID)),
        Leg = str_to_upper(str_trim(as.character(Side))) # must be "L"/"R" to match Side
      ) %>%
      select(ID, Side, group_ad),
    by = c("ID" = "ID", "Side" = "Side"))

#remove not matched ID and not matched leg of CG
df.emg1davg.group <- df.emg1davg.group%>%
  filter(!is.na(group_ad))

df.emg1davg.group <- df.emg1davg.group %>%
  mutate(group_ad = factor(group_ad, levels = c("CG-M", "ACL-C", "ACL-I")))

#remove EMG of the not jumping leg and window t0_20
df.emg.ad<- df.emg %>%
  filter(
    (Condition == "SLHDL_M0" & Side == "L") |
    (Condition == "SLHDR_M0" & Side == "R"),
    Window != "t0_20")

#remove EMG of the not jumping leg
df.emg1davg.group <- df.emg1davg.group %>%
  filter(
    (Condition == "SLHDL_M0" & Side == "L") |
    (Condition == "SLHDR_M0" & Side == "R")) %>% 
  mutate(
    group_ad = factor(group_ad, levels = c("CG-M", "ACL-I", "ACL-C")))

7 TTS

Code
fig.qqplot.tts <- df.tts1davg.group %>%
  select(-age, -Time) %>%
  DataExplorer::plot_qq(by = "group_ad", nrow = 3L, ncol = 3L)

Code
fig.qqplot.tts
$page_1

Code
#QQ-Plot for SAfilt_norm
df_tts_clean <- df.tts1davg.group %>%
  filter(!is.na(SAfilt_norm), SAfilt_norm > 0)
model.tts <- lmer(
  log(SAfilt_norm) ~ group_ad + age + gender + (1 | ID),
  data = df_tts_clean,
  na.action = na.exclude)

residuals_tts <- residuals(model.tts)
qqnorm(residuals_tts)
qqline(residuals_tts)

Code
df_res <- data.frame(residuals = residuals_tts)

ggplot(df_res, aes(sample = residuals)) +
  stat_qq() +
  stat_qq_line() +
  theme_classic() +
  labs(
    title = "Q–Q Plot of Model Residuals",
    x = "Theoretical Quantiles",
    y = "Sample Quantiles")

Code
#separate per group_ad GG-Plot
df.tts1davg.group %>%
  filter(Condition %in% c("SLHDR_M0", "SLHDL_M0")) %>%
  ggplot(aes(
    x = Time,
    y = SAfilt_norm,
    color = Condition,
    fill  = Condition)) +
  geom_line(aes(group = interaction(ID, Condition)), alpha = 0.15) +
  stat_summary(aes(group = Condition), fun = mean, geom = "line", linewidth = 1) +
  stat_summary(aes(group = Condition),
               fun.data = mean_sd,
               geom = "ribbon",
               alpha = 0.2,
               color = NA) +
  theme_minimal() +
  labs(y = "SA filtered (N)", x = "Time (s)", color = "Condition", fill = "Condition") +
  facet_wrap(~ group_ad)

Code
#separate per Condition GG-Plot
df.tts1davg.group%>%
  ggplot(aes(x = Time, y = SAfilt_norm, color = group_ad)) +
  stat_summary(geom = "line", fun = mean) +
  stat_summary(geom = "ribbon", fun.data = "mean_sd", alpha = 0.3) +
  labs(y = "SA filtered (N)", x = "Time (s)") +
  theme_minimal() +
  facet_grid(cols = vars(Condition))

Code
#GG-Plot TTS
df.ttsavg.group %>%
  ggplot(aes(x = group_ad, y = Norm_TTSuniform)) +
  geom_boxplot() +
  geom_point(
    position = position_jitter(width = 0.2),  # jitter for visibility
    size = 2) +
  ylab("TTS (s)")

Code
#GG Plot with the IDs, by group_ad
df.ttsavg.group %>%
  ggplot(aes(x = group_ad, y = Norm_TTSuniform)) +
  geom_boxplot() +
  geom_point(
    position = position_jitter(width = 0.2),
    size = 2
  ) +
  geom_text(
    aes(label = ID),
    position = position_jitter(width = 0.2),
    vjust = -0.5,
    size = 3
  ) +
  ylab("TTS (s)")

Code
df.ttsavg.group %>% ggplot(aes(x=group_ad, y = Norm_TTSuniform)) +
  geom_boxplot() +
  geom_point(
    position = position_jitter(width = 0.2), 
    size = 2) +
  ylab("TTS Uniform (s)")

Code
ggsave(
  filename = "../figures/fig.qqplot.tts.png",
  plot = fig.qqplot.tts,
  width = 20,
  height = 10,
  units = "cm",
  dpi = 600)
Code
df.ttsavg.group <- df.ttsavg.group %>%
  mutate(group_ad = relevel(factor(group_ad), ref = "CG-M"),
         age_c = age - 18)

# unadjusted model
model.tts.lmm.unadj <- lmer(
  log(Norm_TTSuniform) ~ group_ad + (1 | ID),
  data = df.ttsavg.group)

# adjusted model
model.tts.lmm <- lmer(
  log(Norm_TTSuniform) ~ group_ad + age_c + gender + (1 | ID),
  data = df.ttsavg.group)

anova(model.tts.lmm.unadj, model.tts.lmm) # use only unadj model
Data: df.ttsavg.group
Models:
model.tts.lmm.unadj: log(Norm_TTSuniform) ~ group_ad + (1 | ID)
model.tts.lmm: log(Norm_TTSuniform) ~ group_ad + age_c + gender + (1 | ID)
                    npar    AIC    BIC  logLik -2*log(L) Chisq Df Pr(>Chisq)
model.tts.lmm.unadj    5 41.971 49.453 -15.985    31.971                    
model.tts.lmm          7 43.059 53.534 -14.529    29.059 2.912  2     0.2332
Code
model.tts <- model.tts.lmm.unadj

# unadjusted table
tbl.model.tts <- tbl_regression(
  model.tts,
  conf.int = TRUE,
  label = list(group_ad ~ "Group")) %>%
  modify_header(
    estimate ~ "**Coefficient**",
    ci ~ "**95% CI**",
    p.value ~ "**p-value**") %>%
  modify_table_body(~ .x %>% dplyr::select(-conf.low, -conf.high)) %>%
  bold_p(t = 0.05) %>%
  bold_labels()

tbl.model.tts # not log transformed!!!
Characteristic Coefficient 95% CI p-value
Group


    CG-M
    ACL-C -0.38 -0.75, -0.01 0.046
    ACL-I -0.29 -0.66, 0.08 0.12
Code
tbl.model.tts %>%
  as_flex_table() %>%
  set_caption("LMM Model TTS (log TTS)") %>%
  flextable::set_table_properties(width = 1, layout = "autofit") %>% 
  flextable::fontsize(size = 8, part = "all") %>% 
  flextable::font(fontname = "Times New Roman", part = "all") %>%
  flextable::save_as_docx(path = "../tables/tbl.model.tts.docx")
Code
emmeans.tts.lmm <- emmeans::emmeans(model.tts.lmm, revpairwise ~ group_ad, type = "response")
summary(emmeans.tts.lmm)
$emmeans
 group_ad response    SE   df lower.CL upper.CL
 CG-M         3.12 0.407 24.5     2.38     4.08
 ACL-C        2.09 0.270 24.4     1.60     2.72
 ACL-I        2.27 0.294 24.4     1.74     2.97

Results are averaged over the levels of: gender 
Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 
Intervals are back-transformed from the log scale 

$contrasts
 contrast          ratio    SE   df null t.ratio p.value
 (ACL-C) / (CG-M)  0.669 0.122 24.5    1  -2.200  0.0911
 (ACL-I) / (CG-M)  0.729 0.133 24.5    1  -1.733  0.2135
 (ACL-I) / (ACL-C) 1.089 0.152 10.5    1   0.610  0.8176

Results are averaged over the levels of: gender 
Degrees-of-freedom method: kenward-roger 
P value adjustment: tukey method for comparing a family of 3 estimates 
Tests are performed on the log scale 
Code
df.tts.emmeans.lmm <- as.data.frame(confint(emmeans.tts.lmm$contrasts, level = 0.95))

tbl.tts.contr <-df.tts.emmeans.lmm %>%
  dplyr::select(contrast, ratio, lower.CL, upper.CL) %>%
  mutate(`Ratio (95% CI)` = sprintf("%.2f [%.2f, %.2f]", ratio, lower.CL, upper.CL)) %>%
  rename(Contrast = contrast) %>%
  select(Contrast, `Ratio (95% CI)`)

fltbl.tts.contr <-
  flextable::flextable(tbl.tts.contr) %>%
  flextable::set_caption(caption = "TTS contrasts (ratios with 95% CI)") %>%
  flextable::align(j = 1, align = "left", part = "all") %>%
  flextable::set_table_properties(width = 1, layout = "autofit") %>%
  flextable::fontsize(size = 12, part = "all") %>%
  flextable::font(fontname = "Times New Roman", part = "all")

fltbl.tts.contr %>% 
  flextable::save_as_docx(path = "../tables/tbl.tts.contrasts.docx")
Code
xcol <- if ("ratio" %in% names(df.tts.emmeans.lmm)) "ratio" else "estimate"
lcl  <- if ("lower.CL" %in% names(df.tts.emmeans.lmm)) "lower.CL" else "asymp.LCL"
ucl  <- if ("upper.CL" %in% names(df.tts.emmeans.lmm)) "upper.CL" else "asymp.UCL"

plot.tts.lmm.contrasts <- ggplot(df.tts.emmeans.lmm, aes(x = .data[[xcol]], y = contrast)) +
  geom_vline(xintercept = 1, linetype = "dashed") +
  geom_point(size = 3) +
  geom_errorbarh(aes(xmin = .data[[lcl]], xmax = .data[[ucl]]), height = 0.2) +
  labs(x = "TTS ratio (Group A / Group B)", 
       y = "Group contrast") +
  theme_classic(base_size = 12) +
  scale_x_continuous(expand = expansion(mult = c(0.02, 0.05))) +
  ggpubr::theme_pubr(base_size = 14) +
  theme(
    axis.title = element_text(face = "bold"),
    axis.text  = element_text(color = "black"),
    panel.border = element_rect(fill = NA, linewidth = 0.8),
    plot.margin = margin(10, 12, 10, 10))

plot.tts.lmm.contrasts

Code
ggsave(
  filename = "../figures/plot.tts.contrasts.png",
  plot = plot.tts.lmm.contrasts,
  width = 20,
  height = 10,
  units = "cm",
  dpi = 600)

8 EMG

8.1 EMG: Normality check

Code
#DataExplorer::create_report(df.emg)
# favstats(EMGnorm~Group + Side + Muscle + Window, data = df.emg.ad)
fig.qqplot.emg <- df.emg1davg.group %>%
  select(
    group_ad,
    emgHP_LP,
    emgHP_LP_movRMS,
    emgHP_LP_movRMS_norm,
    emgHP_LP_norm) %>%
  DataExplorer::plot_qq(by = "group_ad", nrow = 3L, ncol = 3L)

Code
fig.qqplot.emg
$page_1

Code
#EMG Residual QQ (Per Muscle)
muscles <- c("VM", "VL", "BF", "ST")

df_res_emg <- bind_rows(lapply(muscles, function(m) {
 df_m <- df.emg1davg.group %>%
    filter(Muscle == m) %>%
    filter(!is.na(emgHP_LP_movRMS_norm),
           emgHP_LP_movRMS_norm > 0)
  model_m <- lmer(
    log(emgHP_LP_movRMS_norm) ~ group_ad +  (1 | ID),
    data = df_m,
    na.action = na.exclude)
  tibble(
    Muscle = m,
    residuals = residuals(model_m)
  ) %>%
    filter(is.finite(residuals))}))

ggplot(df_res_emg, aes(sample = residuals)) +
  stat_qq() +
  stat_qq_line() +
  facet_wrap(~ Muscle, ncol = 2) +
  theme_classic(base_size = 14) +
  labs(
    title = "Q–Q Plots of EMG Model Residuals",
    x = "Theoretical Quantiles",
    y = "Sample Quantiles"
  )

Code
#group-ad and SD
df.emg1davg.group %>%
  filter(Muscle %in% c("VM","VL","BF","ST")) %>%
  ggplot(aes(x = Time, y = emgHP_LP_movRMS_norm, color = group_ad, fill = group_ad)) +
  stat_summary(fun = mean, geom = "line", linewidth = 1) +
  stat_summary(fun.data = mean_se, geom = "ribbon", alpha = 0.25, color = NA) +
  facet_wrap(~ Muscle, ncol = 2) +
  labs(x = "Time (s)", y = "Normalized EMG", color = "Group", fill = "Group") +
  theme_classic(base_size = 14)

Code
#
df.emg1davg.group %>%
  filter(Muscle %in% c("VM","VL","BF","ST")) %>%
  ggplot(aes(Time, emgHP_LP_movRMS_norm)) +
  geom_line(aes(group = interaction(ID, group_ad)), alpha = 0.12) +
  stat_summary(aes(color = group_ad), fun = mean, geom = "line", linewidth = 0.5) +
  facet_wrap(~ Muscle, ncol = 2) +
  labs(x = "Time (s)", y = "Normalized EMG", color = "Group") +
  theme_classic(base_size = 14)

Code
ggsave(
  filename = "../figures/fig.qqplot.emg.png",
  plot = fig.qqplot.emg,
  width = 20,
  height = 10,
  units = "cm",
  dpi = 600)

8.2 EMG: Analysis (log scale, lmer function)

Code
df.emg.relevel <- df.emg.ad %>%
  mutate(log_EMG = log(EMGnorm),
    group_ad = relevel(factor(group_ad), ref = "CG-M"),
    ID = factor(ID),
    Muscle   = factor(Muscle),
    age_c = age - 18,
    Window = relevel(factor(Window), ref = "tm50_0"))

df.VM <- df.emg.relevel %>%
  dplyr::filter(Muscle == "VM")

mlogU.VM <- lmer(log(EMGnorm) ~ group_ad + Window + gender + age_c + (1 | ID), data = df.VM)
mlogG.VM <- lmer(log(EMGnorm) ~ group_ad + (1 | ID), data = df.VM)
mlogM.VM <- lmer(log(EMGnorm) ~ group_ad + Window + (1 | ID), data = df.VM)
mlogI.VM <- lmer(log(EMGnorm) ~ group_ad * Window + (1 | ID), data = df.VM)

anova(mlogU.VM, mlogG.VM, mlogM.VM, mlogI.VM) # use mlogI.VM model as window variation is of interest for research question
Data: df.VM
Models:
mlogG.VM: log(EMGnorm) ~ group_ad + (1 | ID)
mlogM.VM: log(EMGnorm) ~ group_ad + Window + (1 | ID)
mlogU.VM: log(EMGnorm) ~ group_ad + Window + gender + age_c + (1 | ID)
mlogI.VM: log(EMGnorm) ~ group_ad * Window + (1 | ID)
         npar    AIC    BIC   logLik -2*log(L)   Chisq Df Pr(>Chisq)    
mlogG.VM    5 236.44 250.86 -113.221    226.44                          
mlogM.VM    8 218.76 241.82 -101.380    202.76 23.6830  3  2.909e-05 ***
mlogU.VM   10 221.22 250.04 -100.608    201.22  1.5445  2     0.4620    
mlogI.VM   14 224.36 264.72  -98.182    196.36  4.8518  4     0.3028    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Code
model.emg.VM <- mlogI.VM

tbl.model.emg.VM <-
  tbl_regression(
    model.emg.VM,
    exponentiate = FALSE,
    conf.int = TRUE,
    label = list(
      group_ad ~ "Group",
      Window   ~ "Window")) %>%
  modify_header(
    estimate ~ "**Coefficient**",
    ci ~ "**95% CI**",
    p.value ~ "**p-value**") %>%
  modify_table_body(~ .x %>% select(-conf.low, -conf.high)) %>%
  bold_p(t = 0.05) %>%
  bold_labels() %>%
  modify_caption("EMG - VM (log scale)")
tbl.model.emg.VM
EMG - VM (log scale)
Characteristic Coefficient 95% CI p-value
Group


    CG-M
    ACL-C -0.22 -0.80, 0.36 0.4
    ACL-I -0.05 -0.63, 0.52 0.8
Window


    tm50_0
    t20_40 0.54 0.16, 0.92 0.006
    t40_60 0.04 -0.34, 0.42 0.8
    t60_95 0.27 -0.11, 0.65 0.2
Group * Window


    ACL-C * t20_40 0.11 -0.43, 0.64 0.7
    ACL-I * t20_40 -0.21 -0.75, 0.33 0.4
    ACL-C * t40_60 0.40 -0.13, 0.94 0.14
    ACL-I * t40_60 0.30 -0.24, 0.83 0.3
    ACL-C * t60_95 0.40 -0.14, 0.94 0.14
    ACL-I * t60_95 0.13 -0.40, 0.67 0.6
Code
df.VL <- df.emg.relevel %>%
  dplyr::filter(Muscle == "VL")

mlogU.VL <- lmer(log(EMGnorm) ~ group_ad + Window + gender + age_c + (1 | ID), data = df.VL)
mlogG.VL <- lmer(log(EMGnorm) ~ group_ad + (1 | ID), data = df.VL)
mlogM.VL <- lmer(log(EMGnorm) ~ group_ad + Window + (1 | ID), data = df.VL)
mlogI.VL <- lmer(log(EMGnorm) ~ group_ad * Window + (1 | ID), data = df.VL)

anova(mlogU.VL, mlogG.VL, mlogM.VL, mlogI.VL) 
Data: df.VL
Models:
mlogG.VL: log(EMGnorm) ~ group_ad + (1 | ID)
mlogM.VL: log(EMGnorm) ~ group_ad + Window + (1 | ID)
mlogU.VL: log(EMGnorm) ~ group_ad + Window + gender + age_c + (1 | ID)
mlogI.VL: log(EMGnorm) ~ group_ad * Window + (1 | ID)
         npar    AIC    BIC  logLik -2*log(L)  Chisq Df Pr(>Chisq)    
mlogG.VL    5 169.61 184.03 -79.807    159.61                         
mlogM.VL    8 154.60 177.66 -69.299    138.60 21.015  3  0.0001045 ***
mlogU.VL   10 157.72 186.55 -68.859    137.72  0.880  2  0.6440358    
mlogI.VL   14 159.39 199.75 -65.694    131.39  6.330  4  0.1758195    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Code
# use mlogI.VL model as window variation is of interest for research question

model.emg.VL <- mlogI.VL

tbl.model.emg.VL <-
  tbl_regression(
    model.emg.VL,
    exponentiate = FALSE,
    conf.int = TRUE,
    label = list(
      group_ad ~ "Group",
      Window   ~ "Window")) %>%
  modify_header(
    estimate ~ "**Coefficient**",
    ci ~ "**95% CI**",
    p.value ~ "**p-value**") %>%
  modify_table_body(~ .x %>% select(-conf.low, -conf.high)) %>%
  bold_p(t = 0.05) %>%
  bold_labels() %>%
  modify_caption("EMG - VL (log-scale)")
tbl.model.emg.VL
EMG - VL (log-scale)
Characteristic Coefficient 95% CI p-value
Group


    CG-M
    ACL-C -0.16 -0.63, 0.30 0.5
    ACL-I -0.11 -0.57, 0.36 0.7
Window


    tm50_0
    t20_40 0.13 -0.17, 0.42 0.4
    t40_60 -0.18 -0.47, 0.12 0.2
    t60_95 0.25 -0.05, 0.54 0.10
Group * Window


    ACL-C * t20_40 0.25 -0.16, 0.66 0.2
    ACL-I * t20_40 0.11 -0.30, 0.53 0.6
    ACL-C * t40_60 0.49 0.07, 0.90 0.021
    ACL-I * t40_60 0.41 0.00, 0.83 0.050
    ACL-C * t60_95 0.25 -0.16, 0.66 0.2
    ACL-I * t60_95 0.16 -0.25, 0.57 0.4
Code
df.BF <- df.emg.relevel %>%
  dplyr::filter(Muscle == "BF")

mlogU.BF <- lmer(log(EMGnorm) ~ group_ad + Window + gender + age_c + (1 | ID), data = df.BF)
mlogG.BF <- lmer(log(EMGnorm) ~ group_ad + (1 | ID), data = df.BF)
mlogM.BF <- lmer(log(EMGnorm) ~ group_ad + Window + (1 | ID), data = df.BF)
mlogI.BF <- lmer(log(EMGnorm) ~ group_ad * Window + (1 | ID), data = df.BF)

anova(mlogU.BF, mlogG.BF, mlogM.BF, mlogI.BF) 
Data: df.BF
Models:
mlogG.BF: log(EMGnorm) ~ group_ad + (1 | ID)
mlogM.BF: log(EMGnorm) ~ group_ad + Window + (1 | ID)
mlogU.BF: log(EMGnorm) ~ group_ad + Window + gender + age_c + (1 | ID)
mlogI.BF: log(EMGnorm) ~ group_ad * Window + (1 | ID)
         npar    AIC    BIC  logLik -2*log(L)   Chisq Df Pr(>Chisq)    
mlogG.BF    5 307.70 322.11 -148.85    297.70                          
mlogM.BF    8 267.07 290.13 -125.54    251.07 46.6261  3  4.174e-10 ***
mlogU.BF   10 269.89 298.72 -124.95    249.90  1.1773  2    0.55508    
mlogI.BF   14 269.57 309.93 -120.78    241.57  8.3267  4    0.08032 .  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Code
# use mlogI.BF model as window variation is of interest for research question

model.emg.BF <- mlogI.BF

tbl.model.emg.BF <-
  tbl_regression(
    model.emg.BF,
    exponentiate = FALSE,
    conf.int = TRUE,
    label = list(
      group_ad ~ "Group",
      Window   ~ "Window")) %>%
  modify_header(
    estimate ~ "**Coefficient**",
    ci ~ "**95% CI**",
    p.value ~ "**p-value**") %>%
  modify_table_body(~ .x %>% select(-conf.low, -conf.high)) %>%
  bold_p(t = 0.05) %>%
  bold_labels() %>%
  modify_caption("EMG - BF (log-scale)")
tbl.model.emg.BF
EMG - BF (log-scale)
Characteristic Coefficient 95% CI p-value
Group


    CG-M
    ACL-C -0.07 -0.81, 0.66 0.8
    ACL-I -0.09 -0.83, 0.65 0.8
Window


    tm50_0
    t20_40 -0.57 -1.0, -0.13 0.012
    t40_60 -0.22 -0.66, 0.22 0.3
    t60_95 -0.26 -0.70, 0.18 0.2
Group * Window


    ACL-C * t20_40 -0.45 -1.1, 0.17 0.2
    ACL-I * t20_40 -0.65 -1.3, -0.03 0.042
    ACL-C * t40_60 -0.75 -1.4, -0.13 0.019
    ACL-I * t40_60 -0.64 -1.3, -0.02 0.043
    ACL-C * t60_95 -0.35 -0.97, 0.27 0.3
    ACL-I * t60_95 -0.21 -0.84, 0.41 0.5
Code
df.ST <- df.emg.relevel %>%
  dplyr::filter(Muscle == "ST")

mlogU.ST <- lmer(log(EMGnorm) ~ group_ad + Window + gender + age_c + (1 | ID), data = df.ST)
mlogG.ST <- lmer(log(EMGnorm) ~ group_ad + (1 | ID), data = df.ST)
mlogM.ST <- lmer(log(EMGnorm) ~ group_ad + Window + (1 | ID), data = df.ST)
mlogI.ST <- lmer(log(EMGnorm) ~ group_ad * Window + (1 | ID), data = df.ST)

anova(mlogU.ST, mlogG.ST, mlogM.ST, mlogI.ST)
Data: df.ST
Models:
mlogG.ST: log(EMGnorm) ~ group_ad + (1 | ID)
mlogM.ST: log(EMGnorm) ~ group_ad + Window + (1 | ID)
mlogU.ST: log(EMGnorm) ~ group_ad + Window + gender + age_c + (1 | ID)
mlogI.ST: log(EMGnorm) ~ group_ad * Window + (1 | ID)
         npar    AIC    BIC  logLik -2*log(L)   Chisq Df Pr(>Chisq)    
mlogG.ST    5 322.49 336.91 -156.25    312.49                          
mlogM.ST    8 248.62 271.68 -116.31    232.62 79.8757  3    < 2e-16 ***
mlogU.ST   10 247.36 276.18 -113.68    227.36  5.2590  2    0.07212 .  
mlogI.ST   14 253.37 293.73 -112.69    225.37  1.9836  4    0.73878    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Code
# use mlogI.ST model as window variation is of interest for research question

model.emg.ST <- mlogI.ST

tbl.model.emg.ST <-
  tbl_regression(
    model.emg.ST,
    exponentiate = FALSE,
    conf.int = TRUE,
    label = list(
      group_ad ~ "Group",
      Window   ~ "Window")) %>%
  modify_header(
    estimate ~ "**Coefficient**",
    ci ~ "**95% CI**",
    p.value ~ "**p-value**") %>%
  modify_table_body(~ .x %>% select(-conf.low, -conf.high)) %>%
  bold_p(t = 0.05) %>%
  bold_labels() %>%
  modify_caption("ST (log-scale)")
tbl.model.emg.ST
ST (log-scale)
Characteristic Coefficient 95% CI p-value
Group


    CG-M
    ACL-C -0.07 -0.85, 0.70 0.8
    ACL-I -0.07 -0.84, 0.71 0.9
Window


    tm50_0
    t20_40 -0.90 -1.3, -0.50 <0.001
    t40_60 -0.54 -0.94, -0.14 0.009
    t60_95 -0.65 -1.0, -0.25 0.002
Group * Window


    ACL-C * t20_40 -0.39 -0.96, 0.18 0.2
    ACL-I * t20_40 -0.45 -1.0, 0.11 0.12
    ACL-C * t40_60 -0.63 -1.2, -0.06 0.030
    ACL-I * t40_60 -0.49 -1.1, 0.08 0.092
    ACL-C * t60_95 -0.33 -0.89, 0.24 0.3
    ACL-I * t60_95 -0.13 -0.70, 0.43 0.6
Code
tbl.model.emg <-
  tbl_stack(
    list(tbl.model.emg.VM, tbl.model.emg.VL, tbl.model.emg.BF, tbl.model.emg.ST),
    group_header = c("VM", "VL", "BF", "ST")) %>% 
  modify_caption("**Model EMG – log scale**")

tbl.model.emg
Model EMG – log scale
Characteristic Coefficient 95% CI p-value
VM
Group


    CG-M
    ACL-C -0.22 -0.80, 0.36 0.4
    ACL-I -0.05 -0.63, 0.52 0.8
Window


    tm50_0
    t20_40 0.54 0.16, 0.92 0.006
    t40_60 0.04 -0.34, 0.42 0.8
    t60_95 0.27 -0.11, 0.65 0.2
Group * Window


    ACL-C * t20_40 0.11 -0.43, 0.64 0.7
    ACL-I * t20_40 -0.21 -0.75, 0.33 0.4
    ACL-C * t40_60 0.40 -0.13, 0.94 0.14
    ACL-I * t40_60 0.30 -0.24, 0.83 0.3
    ACL-C * t60_95 0.40 -0.14, 0.94 0.14
    ACL-I * t60_95 0.13 -0.40, 0.67 0.6
VL
Group


    CG-M
    ACL-C -0.16 -0.63, 0.30 0.5
    ACL-I -0.11 -0.57, 0.36 0.7
Window


    tm50_0
    t20_40 0.13 -0.17, 0.42 0.4
    t40_60 -0.18 -0.47, 0.12 0.2
    t60_95 0.25 -0.05, 0.54 0.10
Group * Window


    ACL-C * t20_40 0.25 -0.16, 0.66 0.2
    ACL-I * t20_40 0.11 -0.30, 0.53 0.6
    ACL-C * t40_60 0.49 0.07, 0.90 0.021
    ACL-I * t40_60 0.41 0.00, 0.83 0.050
    ACL-C * t60_95 0.25 -0.16, 0.66 0.2
    ACL-I * t60_95 0.16 -0.25, 0.57 0.4
BF
Group


    CG-M
    ACL-C -0.07 -0.81, 0.66 0.8
    ACL-I -0.09 -0.83, 0.65 0.8
Window


    tm50_0
    t20_40 -0.57 -1.0, -0.13 0.012
    t40_60 -0.22 -0.66, 0.22 0.3
    t60_95 -0.26 -0.70, 0.18 0.2
Group * Window


    ACL-C * t20_40 -0.45 -1.1, 0.17 0.2
    ACL-I * t20_40 -0.65 -1.3, -0.03 0.042
    ACL-C * t40_60 -0.75 -1.4, -0.13 0.019
    ACL-I * t40_60 -0.64 -1.3, -0.02 0.043
    ACL-C * t60_95 -0.35 -0.97, 0.27 0.3
    ACL-I * t60_95 -0.21 -0.84, 0.41 0.5
ST
Group


    CG-M
    ACL-C -0.07 -0.85, 0.70 0.8
    ACL-I -0.07 -0.84, 0.71 0.9
Window


    tm50_0
    t20_40 -0.90 -1.3, -0.50 <0.001
    t40_60 -0.54 -0.94, -0.14 0.009
    t60_95 -0.65 -1.0, -0.25 0.002
Group * Window


    ACL-C * t20_40 -0.39 -0.96, 0.18 0.2
    ACL-I * t20_40 -0.45 -1.0, 0.11 0.12
    ACL-C * t40_60 -0.63 -1.2, -0.06 0.030
    ACL-I * t40_60 -0.49 -1.1, 0.08 0.092
    ACL-C * t60_95 -0.33 -0.89, 0.24 0.3
    ACL-I * t60_95 -0.13 -0.70, 0.43 0.6
Code
tbl.model.emg %>%
  as_flex_table() %>%
  set_caption("LMM Model EMG (log TTS)") %>%
  flextable::set_table_properties(width = 1, layout = "autofit") %>% 
  flextable::fontsize(size = 8, part = "all") %>% 
  flextable::font(fontname = "Times New Roman", part = "all") %>%
  flextable::save_as_docx(path = "../tables/tbl.model.emg.docx")

8.3 EMG Contrasts

Code
emmeans(model.emg.VM, revpairwise ~ Window | group_ad)
$emmeans
group_ad = CG-M:
 Window emmean    SE   df lower.CL upper.CL
 tm50_0   6.75 0.203 45.9     6.34     7.15
 t20_40   7.28 0.203 45.9     6.88     7.69
 t40_60   6.78 0.203 45.9     6.38     7.19
 t60_95   7.02 0.203 45.9     6.61     7.43

group_ad = ACL-C:
 Window emmean    SE   df lower.CL upper.CL
 tm50_0   6.52 0.203 45.9     6.11     6.93
 t20_40   7.17 0.203 45.9     6.76     7.58
 t40_60   6.97 0.203 45.9     6.56     7.38
 t60_95   7.20 0.203 45.9     6.79     7.60

group_ad = ACL-I:
 Window emmean    SE   df lower.CL upper.CL
 tm50_0   6.69 0.203 45.9     6.28     7.10
 t20_40   7.02 0.203 45.9     6.61     7.43
 t40_60   7.02 0.203 45.9     6.62     7.43
 t60_95   7.09 0.203 45.9     6.68     7.50

Degrees-of-freedom method: kenward-roger 
Results are given on the log (not the response) scale. 
Confidence level used: 0.95 

$contrasts
group_ad = CG-M:
 contrast        estimate    SE  df t.ratio p.value
 t20_40 - tm50_0  0.53932 0.191 100   2.823  0.0288
 t40_60 - tm50_0  0.03935 0.191 100   0.206  0.9969
 t40_60 - t20_40 -0.49997 0.191 100  -2.617  0.0494
 t60_95 - tm50_0  0.27167 0.191 100   1.422  0.4886
 t60_95 - t20_40 -0.26766 0.191 100  -1.401  0.5016
 t60_95 - t40_60  0.23232 0.191 100   1.216  0.6181

group_ad = ACL-C:
 contrast        estimate    SE  df t.ratio p.value
 t20_40 - tm50_0  0.64634 0.191 100   3.384  0.0056
 t40_60 - tm50_0  0.44165 0.191 100   2.312  0.1021
 t40_60 - t20_40 -0.20469 0.191 100  -1.072  0.7076
 t60_95 - tm50_0  0.67118 0.191 100   3.514  0.0037
 t60_95 - t20_40  0.02484 0.191 100   0.130  0.9992
 t60_95 - t40_60  0.22953 0.191 100   1.202  0.6273

group_ad = ACL-I:
 contrast        estimate    SE  df t.ratio p.value
 t20_40 - tm50_0  0.32893 0.191 100   1.722  0.3178
 t40_60 - tm50_0  0.33472 0.191 100   1.752  0.3025
 t40_60 - t20_40  0.00579 0.191 100   0.030  1.0000
 t60_95 - tm50_0  0.40376 0.191 100   2.114  0.1558
 t60_95 - t20_40  0.07483 0.191 100   0.392  0.9795
 t60_95 - t40_60  0.06904 0.191 100   0.361  0.9838

Degrees-of-freedom method: kenward-roger 
Results are given on the log (not the response) scale. 
P value adjustment: tukey method for comparing a family of 4 estimates 
Code
# summary(emmeans(model.emg.VM, revpairwise ~ Window | group_ad), type = "response")
summary(emmeans(model.emg.VM, revpairwise ~ group_ad | Window), type = "response")  
$emmeans
Window = tm50_0:
 group_ad response  SE   df lower.CL upper.CL
 CG-M          850 173 45.9      564     1280
 ACL-C         681 139 45.9      453     1026
 ACL-I         805 164 45.9      534     1211

Window = t20_40:
 group_ad response  SE   df lower.CL upper.CL
 CG-M         1458 296 45.9      968     2195
 ACL-C        1301 264 45.9      864     1958
 ACL-I        1118 227 45.9      742     1683

Window = t40_60:
 group_ad response  SE   df lower.CL upper.CL
 CG-M          884 180 45.9      587     1331
 ACL-C        1060 216 45.9      704     1596
 ACL-I        1124 229 45.9      747     1693

Window = t60_95:
 group_ad response  SE   df lower.CL upper.CL
 CG-M         1115 227 45.9      741     1680
 ACL-C        1333 271 45.9      885     2008
 ACL-I        1205 245 45.9      800     1814

Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 
Intervals are back-transformed from the log scale 

$contrasts
Window = tm50_0:
 contrast          ratio    SE    df null t.ratio p.value
 (ACL-C) / (CG-M)  0.802 0.231  45.9    1  -0.768  0.7240
 (ACL-I) / (CG-M)  0.947 0.272  45.9    1  -0.191  0.9801
 (ACL-I) / (ACL-C) 1.181 0.226 100.0    1   0.869  0.6609

Window = t20_40:
 contrast          ratio    SE    df null t.ratio p.value
 (ACL-C) / (CG-M)  0.892 0.257  45.9    1  -0.396  0.9172
 (ACL-I) / (CG-M)  0.767 0.221  45.9    1  -0.923  0.6289
 (ACL-I) / (ACL-C) 0.860 0.164 100.0    1  -0.792  0.7085

Window = t40_60:
 contrast          ratio    SE    df null t.ratio p.value
 (ACL-C) / (CG-M)  1.199 0.345  45.9    1   0.630  0.8043
 (ACL-I) / (CG-M)  1.272 0.366  45.9    1   0.836  0.6829
 (ACL-I) / (ACL-C) 1.061 0.203 100.0    1   0.309  0.9486

Window = t60_95:
 contrast          ratio    SE    df null t.ratio p.value
 (ACL-C) / (CG-M)  1.195 0.344  45.9    1   0.621  0.8096
 (ACL-I) / (CG-M)  1.080 0.311  45.9    1   0.268  0.9612
 (ACL-I) / (ACL-C) 0.904 0.173 100.0    1  -0.531  0.8564

Degrees-of-freedom method: kenward-roger 
P value adjustment: tukey method for comparing a family of 3 estimates 
Tests are performed on the log scale 
Code
# plot of log EMG per group
# plot(predictorEffect(mod = model.emg.VM, predictor = "group_ad"))
plot(predictorEffect(mod = model.emg.VM, predictor = "group_ad", partial.residuals = TRUE))

Code
plot(predictorEffects(model.emg.VM, ~Window), axes = list(y = list(transform = exp, lab = "EMGnorm")))

Code
emmeans(model.emg.VL, revpairwise ~ Window | group_ad)
$emmeans
group_ad = CG-M:
 Window emmean    SE   df lower.CL upper.CL
 tm50_0   6.50 0.164 42.5     6.17     6.84
 t20_40   6.63 0.164 42.5     6.30     6.96
 t40_60   6.33 0.164 42.5     6.00     6.66
 t60_95   6.75 0.164 42.5     6.42     7.08

group_ad = ACL-C:
 Window emmean    SE   df lower.CL upper.CL
 tm50_0   6.34 0.164 42.5     6.01     6.67
 t20_40   6.72 0.164 42.5     6.39     7.05
 t40_60   6.65 0.164 42.5     6.32     6.99
 t60_95   6.84 0.164 42.5     6.51     7.17

group_ad = ACL-I:
 Window emmean    SE   df lower.CL upper.CL
 tm50_0   6.40 0.164 42.5     6.07     6.73
 t20_40   6.64 0.164 42.5     6.31     6.97
 t40_60   6.64 0.164 42.5     6.31     6.97
 t60_95   6.81 0.164 42.5     6.48     7.14

Degrees-of-freedom method: kenward-roger 
Results are given on the log (not the response) scale. 
Confidence level used: 0.95 

$contrasts
group_ad = CG-M:
 contrast        estimate    SE  df t.ratio p.value
 t20_40 - tm50_0  0.12742 0.148 100   0.863  0.8239
 t40_60 - tm50_0 -0.17579 0.148 100  -1.190  0.6344
 t40_60 - t20_40 -0.30321 0.148 100  -2.053  0.1758
 t60_95 - tm50_0  0.24773 0.148 100   1.677  0.3410
 t60_95 - t20_40  0.12031 0.148 100   0.815  0.8474
 t60_95 - t40_60  0.42352 0.148 100   2.868  0.0256

group_ad = ACL-C:
 contrast        estimate    SE  df t.ratio p.value
 t20_40 - tm50_0  0.37747 0.148 100   2.556  0.0576
 t40_60 - tm50_0  0.31334 0.148 100   2.122  0.1533
 t40_60 - t20_40 -0.06413 0.148 100  -0.434  0.9724
 t60_95 - tm50_0  0.49833 0.148 100   3.374  0.0057
 t60_95 - t20_40  0.12085 0.148 100   0.818  0.8457
 t60_95 - t40_60  0.18499 0.148 100   1.253  0.5951

group_ad = ACL-I:
 contrast        estimate    SE  df t.ratio p.value
 t20_40 - tm50_0  0.24005 0.148 100   1.625  0.3691
 t40_60 - tm50_0  0.23890 0.148 100   1.618  0.3734
 t40_60 - t20_40 -0.00116 0.148 100  -0.008  1.0000
 t60_95 - tm50_0  0.40810 0.148 100   2.763  0.0339
 t60_95 - t20_40  0.16804 0.148 100   1.138  0.6671
 t60_95 - t40_60  0.16920 0.148 100   1.146  0.6622

Degrees-of-freedom method: kenward-roger 
Results are given on the log (not the response) scale. 
P value adjustment: tukey method for comparing a family of 4 estimates 
Code
# summary(emmeans(model.emg.VL, revpairwise ~ Window | group_ad), type = "response")
summary(emmeans(model.emg.VL, revpairwise ~ group_ad | Window), type = "response")  
$emmeans
Window = tm50_0:
 group_ad response    SE   df lower.CL upper.CL
 CG-M          668 110.0 42.5      480      931
 ACL-C         567  93.2 42.5      407      790
 ACL-I         601  98.8 42.5      432      837

Window = t20_40:
 group_ad response    SE   df lower.CL upper.CL
 CG-M          759 125.0 42.5      545     1057
 ACL-C         827 136.0 42.5      594     1153
 ACL-I         764 126.0 42.5      549     1065

Window = t40_60:
 group_ad response    SE   df lower.CL upper.CL
 CG-M          560  92.1 42.5      402      781
 ACL-C         776 128.0 42.5      557     1081
 ACL-I         763 125.0 42.5      548     1063

Window = t60_95:
 group_ad response    SE   df lower.CL upper.CL
 CG-M          856 141.0 42.5      614     1192
 ACL-C         934 153.0 42.5      670     1301
 ACL-I         904 149.0 42.5      649     1259

Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 
Intervals are back-transformed from the log scale 

$contrasts
Window = tm50_0:
 contrast          ratio    SE    df null t.ratio p.value
 (ACL-C) / (CG-M)  0.849 0.197  42.5    1  -0.704  0.7624
 (ACL-I) / (CG-M)  0.900 0.209  42.5    1  -0.455  0.8925
 (ACL-I) / (ACL-C) 1.060 0.156 100.0    1   0.392  0.9189

Window = t20_40:
 contrast          ratio    SE    df null t.ratio p.value
 (ACL-C) / (CG-M)  1.090 0.253  42.5    1   0.372  0.9267
 (ACL-I) / (CG-M)  1.007 0.234  42.5    1   0.030  0.9995
 (ACL-I) / (ACL-C) 0.924 0.136 100.0    1  -0.539  0.8525

Window = t40_60:
 contrast          ratio    SE    df null t.ratio p.value
 (ACL-C) / (CG-M)  1.385 0.322  42.5    1   1.401  0.3495
 (ACL-I) / (CG-M)  1.362 0.316  42.5    1   1.330  0.3868
 (ACL-I) / (ACL-C) 0.984 0.145 100.0    1  -0.112  0.9931

Window = t60_95:
 contrast          ratio    SE    df null t.ratio p.value
 (ACL-C) / (CG-M)  1.091 0.253  42.5    1   0.374  0.9258
 (ACL-I) / (CG-M)  1.056 0.245  42.5    1   0.235  0.9700
 (ACL-I) / (ACL-C) 0.968 0.143 100.0    1  -0.219  0.9739

Degrees-of-freedom method: kenward-roger 
P value adjustment: tukey method for comparing a family of 3 estimates 
Tests are performed on the log scale 
Code
# plot of log EMG per group
# plot(predictorEffect(mod = model.emg.VL, predictor = "group_ad"))
plot(predictorEffect(mod = model.emg.VL, predictor = "group_ad", partial.residuals = TRUE))

Code
plot(predictorEffects(model.emg.VL, ~Window), axes = list(y = list(transform = exp, lab = "EMGnorm")))

Code
emmeans(model.emg.BF, revpairwise ~ Window | group_ad)
$emmeans
group_ad = CG-M:
 Window emmean    SE   df lower.CL upper.CL
 tm50_0   6.95 0.259 39.3     6.43     7.47
 t20_40   6.38 0.259 39.3     5.86     6.91
 t40_60   6.73 0.259 39.3     6.21     7.26
 t60_95   6.69 0.259 39.3     6.17     7.22

group_ad = ACL-C:
 Window emmean    SE   df lower.CL upper.CL
 tm50_0   6.88 0.259 39.3     6.35     7.40
 t20_40   5.86 0.259 39.3     5.34     6.38
 t40_60   5.91 0.259 39.3     5.39     6.43
 t60_95   6.27 0.259 39.3     5.74     6.79

group_ad = ACL-I:
 Window emmean    SE   df lower.CL upper.CL
 tm50_0   6.86 0.259 39.3     6.33     7.38
 t20_40   5.64 0.259 39.3     5.12     6.17
 t40_60   6.00 0.259 39.3     5.47     6.52
 t60_95   6.39 0.259 39.3     5.86     6.91

Degrees-of-freedom method: kenward-roger 
Results are given on the log (not the response) scale. 
Confidence level used: 0.95 

$contrasts
group_ad = CG-M:
 contrast        estimate    SE  df t.ratio p.value
 t20_40 - tm50_0  -0.5657 0.222 100  -2.553  0.0580
 t40_60 - tm50_0  -0.2178 0.222 100  -0.983  0.7594
 t40_60 - t20_40   0.3479 0.222 100   1.570  0.4002
 t60_95 - tm50_0  -0.2570 0.222 100  -1.160  0.6534
 t60_95 - t20_40   0.3087 0.222 100   1.393  0.5065
 t60_95 - t40_60  -0.0392 0.222 100  -0.177  0.9980

group_ad = ACL-C:
 contrast        estimate    SE  df t.ratio p.value
 t20_40 - tm50_0  -1.0150 0.222 100  -4.581 <0.0001
 t40_60 - tm50_0  -0.9650 0.222 100  -4.355  0.0002
 t40_60 - t20_40   0.0500 0.222 100   0.226  0.9959
 t60_95 - tm50_0  -0.6084 0.222 100  -2.746  0.0355
 t60_95 - t20_40   0.4067 0.222 100   1.835  0.2631
 t60_95 - t40_60   0.3566 0.222 100   1.609  0.3780

group_ad = ACL-I:
 contrast        estimate    SE  df t.ratio p.value
 t20_40 - tm50_0  -1.2127 0.222 100  -5.473 <0.0001
 t40_60 - tm50_0  -0.8586 0.222 100  -3.875  0.0011
 t40_60 - t20_40   0.3541 0.222 100   1.598  0.3845
 t60_95 - tm50_0  -0.4709 0.222 100  -2.125  0.1522
 t60_95 - t20_40   0.7417 0.222 100   3.348  0.0062
 t60_95 - t40_60   0.3877 0.222 100   1.750  0.3039

Degrees-of-freedom method: kenward-roger 
Results are given on the log (not the response) scale. 
P value adjustment: tukey method for comparing a family of 4 estimates 
Code
# summary(emmeans(model.emg.BF, revpairwise ~ Window | group_ad), type = "response")
summary(emmeans(model.emg.BF, revpairwise ~ group_ad | Window), type = "response")
$emmeans
Window = tm50_0:
 group_ad response    SE   df lower.CL upper.CL
 CG-M         1043 270.0 39.3      618     1760
 ACL-C         968 250.0 39.3      574     1633
 ACL-I         950 246.0 39.3      563     1602

Window = t20_40:
 group_ad response    SE   df lower.CL upper.CL
 CG-M          592 153.0 39.3      351      999
 ACL-C         351  90.7 39.3      208      592
 ACL-I         283  73.1 39.3      167      477

Window = t40_60:
 group_ad response    SE   df lower.CL upper.CL
 CG-M          839 217.0 39.3      497     1415
 ACL-C         369  95.4 39.3      219      622
 ACL-I         403 104.0 39.3      239      679

Window = t60_95:
 group_ad response    SE   df lower.CL upper.CL
 CG-M          807 209.0 39.3      478     1361
 ACL-C         527 136.0 39.3      312      889
 ACL-I         593 153.0 39.3      352     1001

Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 
Intervals are back-transformed from the log scale 

$contrasts
Window = tm50_0:
 contrast          ratio    SE    df null t.ratio p.value
 (ACL-C) / (CG-M)  0.928 0.339  39.3    1  -0.204  0.9772
 (ACL-I) / (CG-M)  0.911 0.333  39.3    1  -0.256  0.9646
 (ACL-I) / (ACL-C) 0.981 0.217 100.0    1  -0.085  0.9960

Window = t20_40:
 contrast          ratio    SE    df null t.ratio p.value
 (ACL-C) / (CG-M)  0.592 0.217  39.3    1  -1.433  0.3342
 (ACL-I) / (CG-M)  0.477 0.174  39.3    1  -2.025  0.1196
 (ACL-I) / (ACL-C) 0.805 0.178 100.0    1  -0.977  0.5930

Window = t40_60:
 contrast          ratio    SE    df null t.ratio p.value
 (ACL-C) / (CG-M)  0.440 0.161  39.3    1  -2.247  0.0757
 (ACL-I) / (CG-M)  0.480 0.175  39.3    1  -2.008  0.1236
 (ACL-I) / (ACL-C) 1.091 0.242 100.0    1   0.395  0.9177

Window = t60_95:
 contrast          ratio    SE    df null t.ratio p.value
 (ACL-C) / (CG-M)  0.653 0.239  39.3    1  -1.165  0.4807
 (ACL-I) / (CG-M)  0.735 0.269  39.3    1  -0.841  0.6802
 (ACL-I) / (ACL-C) 1.126 0.249 100.0    1   0.535  0.8542

Degrees-of-freedom method: kenward-roger 
P value adjustment: tukey method for comparing a family of 3 estimates 
Tests are performed on the log scale 
Code
# plot of log EMG per group
# plot(predictorEffect(mod = model.emg.BF, predictor = "group_ad"))
plot(predictorEffect(mod = model.emg.BF, predictor = "group_ad", partial.residuals = TRUE))

Code
plot(predictorEffects(model.emg.BF, ~Window), axes = list(y = list(transform = exp, lab = "EMGnorm")))

Code
emmeans(model.emg.ST, revpairwise ~ Window | group_ad)
$emmeans
group_ad = CG-M:
 Window emmean    SE   df lower.CL upper.CL
 tm50_0   7.17 0.269 33.2     6.62     7.72
 t20_40   6.27 0.269 33.2     5.72     6.82
 t40_60   6.63 0.269 33.2     6.08     7.18
 t60_95   6.52 0.269 33.2     5.98     7.07

group_ad = ACL-C:
 Window emmean    SE   df lower.CL upper.CL
 tm50_0   7.10 0.269 33.2     6.55     7.64
 t20_40   5.81 0.269 33.2     5.26     6.35
 t40_60   5.93 0.269 33.2     5.38     6.47
 t60_95   6.12 0.269 33.2     5.57     6.67

group_ad = ACL-I:
 Window emmean    SE   df lower.CL upper.CL
 tm50_0   7.10 0.269 33.2     6.56     7.65
 t20_40   5.74 0.269 33.2     5.20     6.29
 t40_60   6.08 0.269 33.2     5.53     6.62
 t60_95   6.32 0.269 33.2     5.77     6.87

Degrees-of-freedom method: kenward-roger 
Results are given on the log (not the response) scale. 
Confidence level used: 0.95 

$contrasts
group_ad = CG-M:
 contrast        estimate    SE  df t.ratio p.value
 t20_40 - tm50_0   -0.903 0.202 100  -4.465  0.0001
 t40_60 - tm50_0   -0.541 0.202 100  -2.673  0.0429
 t40_60 - t20_40    0.363 0.202 100   1.792  0.2833
 t60_95 - tm50_0   -0.648 0.202 100  -3.205  0.0097
 t60_95 - t20_40    0.255 0.202 100   1.260  0.5907
 t60_95 - t40_60   -0.108 0.202 100  -0.532  0.9510

group_ad = ACL-C:
 contrast        estimate    SE  df t.ratio p.value
 t20_40 - tm50_0   -1.292 0.202 100  -6.387 <0.0001
 t40_60 - tm50_0   -1.170 0.202 100  -5.785 <0.0001
 t40_60 - t20_40    0.122 0.202 100   0.602  0.9312
 t60_95 - tm50_0   -0.975 0.202 100  -4.820 <0.0001
 t60_95 - t20_40    0.317 0.202 100   1.567  0.4020
 t60_95 - t40_60    0.195 0.202 100   0.965  0.7694

group_ad = ACL-I:
 contrast        estimate    SE  df t.ratio p.value
 t20_40 - tm50_0   -1.358 0.202 100  -6.712 <0.0001
 t40_60 - tm50_0   -1.028 0.202 100  -5.079 <0.0001
 t40_60 - t20_40    0.330 0.202 100   1.633  0.3651
 t60_95 - tm50_0   -0.783 0.202 100  -3.870  0.0011
 t60_95 - t20_40    0.575 0.202 100   2.842  0.0274
 t60_95 - t40_60    0.245 0.202 100   1.209  0.6227

Degrees-of-freedom method: kenward-roger 
Results are given on the log (not the response) scale. 
P value adjustment: tukey method for comparing a family of 4 estimates 
Code
# summary(emmeans(model.emg.ST, revpairwise ~ Window | group_ad), type = "response")
summary(emmeans(model.emg.ST, revpairwise ~ group_ad | Window), type = "response")  
$emmeans
Window = tm50_0:
 group_ad response    SE   df lower.CL upper.CL
 CG-M         1303 351.0 33.2      753     2252
 ACL-C        1209 325.0 33.2      699     2090
 ACL-I        1215 327.0 33.2      703     2101

Window = t20_40:
 group_ad response    SE   df lower.CL upper.CL
 CG-M          528 142.0 33.2      305      913
 ACL-C         332  89.4 33.2      192      574
 ACL-I         313  84.1 33.2      181      540

Window = t40_60:
 group_ad response    SE   df lower.CL upper.CL
 CG-M          759 204.0 33.2      439     1311
 ACL-C         375 101.0 33.2      217      648
 ACL-I         435 117.0 33.2      252      752

Window = t60_95:
 group_ad response    SE   df lower.CL upper.CL
 CG-M          681 183.0 33.2      394     1177
 ACL-C         456 123.0 33.2      264      788
 ACL-I         555 149.0 33.2      321      960

Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 
Intervals are back-transformed from the log scale 

$contrasts
Window = tm50_0:
 contrast          ratio    SE    df null t.ratio p.value
 (ACL-C) / (CG-M)  0.928 0.353  33.2    1  -0.196  0.9790
 (ACL-I) / (CG-M)  0.933 0.355  33.2    1  -0.182  0.9819
 (ACL-I) / (ACL-C) 1.005 0.203 100.0    1   0.027  0.9996

Window = t20_40:
 contrast          ratio    SE    df null t.ratio p.value
 (ACL-C) / (CG-M)  0.629 0.239  33.2    1  -1.218  0.4511
 (ACL-I) / (CG-M)  0.592 0.225  33.2    1  -1.377  0.3644
 (ACL-I) / (ACL-C) 0.941 0.190 100.0    1  -0.298  0.9521

Window = t40_60:
 contrast          ratio    SE    df null t.ratio p.value
 (ACL-C) / (CG-M)  0.494 0.188  33.2    1  -1.851  0.1691
 (ACL-I) / (CG-M)  0.573 0.218  33.2    1  -1.461  0.3222
 (ACL-I) / (ACL-C) 1.160 0.235 100.0    1   0.732  0.7449

Window = t60_95:
 contrast          ratio    SE    df null t.ratio p.value
 (ACL-C) / (CG-M)  0.669 0.255  33.2    1  -1.055  0.5484
 (ACL-I) / (CG-M)  0.816 0.310  33.2    1  -0.536  0.8544
 (ACL-I) / (ACL-C) 1.218 0.246 100.0    1   0.976  0.5937

Degrees-of-freedom method: kenward-roger 
P value adjustment: tukey method for comparing a family of 3 estimates 
Tests are performed on the log scale 
Code
# plot of log EMG per group
# plot(predictorEffect(mod = model.emg.ST, predictor = "group_ad"))
plot(predictorEffect(mod = model.emg.ST, predictor = "group_ad", partial.residuals = TRUE))

Code
plot(predictorEffects(model.emg.ST, ~Window), axes = list(y = list(transform = exp, lab = "EMGnorm")))

8.4 EMG Contrast plot of all muscles

Code
models.emg <- list(
  VM = model.emg.VM,
  VL = model.emg.VL,
  BF = model.emg.BF,
  ST = model.emg.ST)

contr_by_muscle <- imap(models.emg, function(mod, mus) {
  # EMMs for group within each Window, back-transformed (ratios)
  emm <- emmeans(mod, ~ group_ad | Window, type = "response")
  # Reverse pairwise contrasts to match revpairwise
  ctr <- contrast(emm, method = "revpairwise", by = "Window", adjust = "holm") %>%
    confint(level = 0.95) %>%
    as.data.frame() %>%
    mutate(Muscle = mus)
  ctr})

df.emg.contr.lmm <- bind_rows(contr_by_muscle)

tbl.emg.contr <- df.emg.contr.lmm %>%
  select(Muscle, Window, contrast, ratio, lower.CL, upper.CL) %>%
  mutate(`Ratio (95% CI)` = sprintf("%.2f [%.2f, %.2f]", ratio, lower.CL, upper.CL)) %>%
  rename(Contrast = contrast) %>%
  select(Muscle, Window, Contrast, `Ratio (95% CI)`) 


fltbl.emg.contr <- flextable(tbl.emg.contr) %>% 
  set_caption("EMG contrasts (ratios with 95% CI)") %>%
  align(j = 1, align = "left", part = "all") %>%
  flextable::set_table_properties(width = 1, layout = "autofit") %>% 
  flextable::fontsize(size = 8, part = "all") %>% 
  flextable::font(fontname = "Times New Roman", part = "all") 

fltbl.emg.contr

Muscle

Window

Contrast

Ratio (95% CI)

VM

tm50_0

(ACL-C) / (CG-M)

0.80 [0.39, 1.64]

VM

tm50_0

(ACL-I) / (CG-M)

0.95 [0.46, 1.93]

VM

tm50_0

(ACL-I) / (ACL-C)

1.18 [0.74, 1.88]

VM

t20_40

(ACL-C) / (CG-M)

0.89 [0.44, 1.82]

VM

t20_40

(ACL-I) / (CG-M)

0.77 [0.38, 1.57]

VM

t20_40

(ACL-I) / (ACL-C)

0.86 [0.54, 1.37]

VM

t40_60

(ACL-C) / (CG-M)

1.20 [0.59, 2.45]

VM

t40_60

(ACL-I) / (CG-M)

1.27 [0.62, 2.60]

VM

t40_60

(ACL-I) / (ACL-C)

1.06 [0.67, 1.69]

VM

t60_95

(ACL-C) / (CG-M)

1.20 [0.58, 2.44]

VM

t60_95

(ACL-I) / (CG-M)

1.08 [0.53, 2.21]

VM

t60_95

(ACL-I) / (ACL-C)

0.90 [0.57, 1.44]

VL

tm50_0

(ACL-C) / (CG-M)

0.85 [0.48, 1.52]

VL

tm50_0

(ACL-I) / (CG-M)

0.90 [0.50, 1.61]

VL

tm50_0

(ACL-I) / (ACL-C)

1.06 [0.74, 1.52]

VL

t20_40

(ACL-C) / (CG-M)

1.09 [0.61, 1.95]

VL

t20_40

(ACL-I) / (CG-M)

1.01 [0.56, 1.80]

VL

t20_40

(ACL-I) / (ACL-C)

0.92 [0.64, 1.32]

VL

t40_60

(ACL-C) / (CG-M)

1.38 [0.78, 2.47]

VL

t40_60

(ACL-I) / (CG-M)

1.36 [0.76, 2.43]

VL

t40_60

(ACL-I) / (ACL-C)

0.98 [0.69, 1.41]

VL

t60_95

(ACL-C) / (CG-M)

1.09 [0.61, 1.95]

VL

t60_95

(ACL-I) / (CG-M)

1.06 [0.59, 1.88]

VL

t60_95

(ACL-I) / (ACL-C)

0.97 [0.68, 1.39]

BF

tm50_0

(ACL-C) / (CG-M)

0.93 [0.37, 2.32]

BF

tm50_0

(ACL-I) / (CG-M)

0.91 [0.36, 2.27]

BF

tm50_0

(ACL-I) / (ACL-C)

0.98 [0.57, 1.68]

BF

t20_40

(ACL-C) / (CG-M)

0.59 [0.24, 1.48]

BF

t20_40

(ACL-I) / (CG-M)

0.48 [0.19, 1.19]

BF

t20_40

(ACL-I) / (ACL-C)

0.81 [0.47, 1.38]

BF

t40_60

(ACL-C) / (CG-M)

0.44 [0.18, 1.10]

BF

t40_60

(ACL-I) / (CG-M)

0.48 [0.19, 1.20]

BF

t40_60

(ACL-I) / (ACL-C)

1.09 [0.64, 1.87]

BF

t60_95

(ACL-C) / (CG-M)

0.65 [0.26, 1.63]

BF

t60_95

(ACL-I) / (CG-M)

0.74 [0.29, 1.84]

BF

t60_95

(ACL-I) / (ACL-C)

1.13 [0.66, 1.93]

ST

tm50_0

(ACL-C) / (CG-M)

0.93 [0.36, 2.42]

ST

tm50_0

(ACL-I) / (CG-M)

0.93 [0.36, 2.44]

ST

tm50_0

(ACL-I) / (ACL-C)

1.01 [0.61, 1.65]

ST

t20_40

(ACL-C) / (CG-M)

0.63 [0.24, 1.64]

ST

t20_40

(ACL-I) / (CG-M)

0.59 [0.23, 1.55]

ST

t20_40

(ACL-I) / (ACL-C)

0.94 [0.58, 1.54]

ST

t40_60

(ACL-C) / (CG-M)

0.49 [0.19, 1.29]

ST

t40_60

(ACL-I) / (CG-M)

0.57 [0.22, 1.50]

ST

t40_60

(ACL-I) / (ACL-C)

1.16 [0.71, 1.90]

ST

t60_95

(ACL-C) / (CG-M)

0.67 [0.26, 1.75]

ST

t60_95

(ACL-I) / (CG-M)

0.82 [0.31, 2.13]

ST

t60_95

(ACL-I) / (ACL-C)

1.22 [0.74, 1.99]

Code
fltbl.emg.contr %>%
  flextable::save_as_docx(path = "../tables/tbl.emg.contrasts.all.muscles.docx")
Code
df.emg.contr.lmm$Window <- factor(
  df.emg.contr.lmm$Window,
  levels = c("tm50_0", "t20_40", "t40_60", "t60_95"),
  labels = c("Pre-activation", "SLR", "MLR", "LLR"))

plot.emg.contrasts <- ggplot(df.emg.contr.lmm,
                                aes(x = ratio, y = contrast)) +
  geom_vline(xintercept = 1, linetype = "dashed") +
  geom_point(size = 2.5) +
  geom_errorbarh(aes(xmin = lower.CL, xmax = upper.CL), height = 0.2) +
  facet_grid(Muscle ~ Window) +
  scale_x_log10() +
  labs(
    x = "EMG ratio (Group A / Group B)",
    y = "Group contrast") +
  theme_classic(base_size = 12) +
  scale_x_continuous(expand = expansion(mult = c(0.02, 0.05))) +
  ggpubr::theme_pubr(base_size = 14) +
  theme(
    axis.title = element_text(face = "bold"),
    axis.text  = element_text(color = "black"),
    panel.border = element_rect(fill = NA, linewidth = 0.8),
    plot.margin = margin(10, 12, 10, 10))

plot.emg.contrasts

Code
ggsave(
  filename = "../figures/plot.emg.contrasts.png",
  plot = plot.emg.contrasts,
  width = 30,
  height = 30,
  units = "cm",
  dpi = 600)

9 Session info

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

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

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

time zone: Europe/Zurich
tzcode source: internal

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

other attached packages:
 [1] parameters_0.28.3  effects_4.2-5      carData_3.0-6      emmeans_2.0.1     
 [5] lmerTest_3.2-0     lme4_1.1-38        Hmisc_5.2-5        mosaic_1.9.2      
 [9] mosaicData_0.20.4  ggformula_1.0.1    Matrix_1.7-4       lattice_0.22-7    
[13] DataExplorer_0.8.4 qqplotr_0.0.7      janitor_2.2.1      ggpubr_0.6.2      
[17] optmatch_0.10.8    gtsummary_2.5.0    flextable_0.9.10   lubridate_1.9.4   
[21] forcats_1.0.1      stringr_1.6.0      dplyr_1.1.4        purrr_1.2.1       
[25] readr_2.1.6        tidyr_1.3.2        tibble_3.3.1       ggplot2_4.0.1     
[29] tidyverse_2.0.0    readxl_1.4.5       rio_1.2.4          pacman_0.5.1      

loaded via a namespace (and not attached):
  [1] splines_4.5.2           bitops_1.0-9            R.oo_1.27.1            
  [4] cellranger_1.1.0        datawizard_1.3.0        rpart_4.1.24           
  [7] lifecycle_1.0.5         Rdpack_2.6.6            rstatix_0.7.3          
 [10] pbmcapply_1.5.1         doParallel_1.0.17       globals_0.19.0         
 [13] MASS_7.3-65             insight_1.4.6           backports_1.5.0        
 [16] survey_4.5              magrittr_2.0.4          sass_0.4.10            
 [19] rmarkdown_2.30          yaml_2.3.12             qqconf_1.3.2           
 [22] otel_0.2.0              zip_2.3.3               askpass_1.2.1          
 [25] DBI_1.2.3               minqa_1.2.8             RColorBrewer_1.1-3     
 [28] abind_1.4-8             R.utils_2.13.0          nnet_7.3-20            
 [31] pracma_2.4.6            gdtools_0.4.4           labelled_2.16.0        
 [34] pbkrtest_0.5.5          data.tree_1.2.0         listenv_0.10.0         
 [37] cards_0.7.1             parallelly_1.46.1       commonmark_2.0.0       
 [40] codetools_0.2-20        xml2_1.5.2              tidyselect_1.2.1       
 [43] farver_2.1.2            broom.mixed_0.2.9.6     base64enc_0.1-3        
 [46] broom.helpers_1.22.0    jsonlite_2.0.0          opdisDownsampling_1.0.1
 [49] Formula_1.2-5           ggridges_0.5.7          survival_3.8-3         
 [52] iterators_1.0.14        systemfonts_1.3.1       foreach_1.5.2          
 [55] tools_4.5.2             twosamples_2.0.1        ragg_1.5.0             
 [58] Rcpp_1.1.1              glue_1.8.0              gridExtra_2.3          
 [61] xfun_0.56               withr_3.0.2             numDeriv_2016.8-1.1    
 [64] fastmap_1.2.0           mitools_2.4             boot_1.3-32            
 [67] openssl_2.3.4           litedown_0.9            caTools_1.18.3         
 [70] digest_0.6.39           timechange_0.3.0        R6_2.6.1               
 [73] estimability_1.5.1      textshaping_1.0.4       colorspace_2.1-2       
 [76] networkD3_0.4.1         markdown_2.0            R.methodsS3_1.8.2      
 [79] generics_0.1.4          fontLiberation_0.1.0    data.table_1.18.0      
 [82] robustbase_0.99-7       htmlwidgets_1.6.4       pkgconfig_2.0.3        
 [85] gtable_0.3.6            S7_0.2.1                furrr_0.3.1            
 [88] htmltools_0.5.9         fontBitstreamVera_0.1.1 scales_1.4.0           
 [91] reformulas_0.4.4        snakecase_0.11.1        knitr_1.51             
 [94] rstudioapi_0.18.0       tzdb_0.5.0              uuid_1.2-2             
 [97] coda_0.19-4.1           checkmate_2.3.4         nlme_3.1-168           
[100] nloptr_2.2.1            parallel_4.5.2          foreign_0.8-90         
[103] pillar_1.11.1           grid_4.5.2              vctrs_0.7.1            
[106] car_3.1-5               cluster_2.1.8.1         htmlTable_2.4.3        
[109] evaluate_1.0.5          mvtnorm_1.3-3           cli_3.6.5              
[112] compiler_4.5.2          rlang_1.1.7             ggsignif_0.6.4         
[115] labeling_0.4.3          fs_1.6.6                ggiraph_0.9.4          
[118] stringi_1.8.7           bayestestR_0.17.0       mosaicCore_0.9.5       
[121] fontquiver_0.2.1        hms_1.1.4               future_1.69.0          
[124] haven_2.5.5             rbibutils_2.4.1         igraph_2.2.2           
[127] gt_1.3.0                broom_1.0.11            DEoptimR_1.1-4         
[130] officer_0.7.3