Statistical Analyses

Author

Dylan Carter

Load all required packages

Install if required:

# install.packages("tidyverse")
# install.packages("boot")
# install.packages("bootES")
# install.packages("kableExtra")
# install.packages("ggpubr")
# install.packages("ggpp")
# install.packages("gt")
# install.packages("patchwork")
# install.packages("rsvg")
# install.packages("grid")
# if (!require("devtools")) install.packages("devtools")
# devtools::install_github("psyteachr/introdataviz")
# install.packages("ggtext")
# install.packages("ggbeeswarm")
# install.packages("lme4")
# install.packages("lmerTest")
# install.packages("merTools")
# install.packages("emmeans")
# install.packages("performance")
# install.packages("qqplotr")
# install.packages("ggplotify")
# install.packages("dtwclust")
# install.packages("cluster")
# install.packages("ggh4x")

Load:

library(tidyverse)
library(boot)
library(bootES)
library(kableExtra)
library(ggpubr)
library(introdataviz)
library(ggpp)
library(gt)
library(patchwork)
library(rsvg)
library(grid)
library(ggbeeswarm)
library(lme4)
library(lmerTest)
library(merTools)
library(emmeans)
library(performance)
library(qqplotr)
library(ggtext)
library(ggplotify)
library(gtable)
library(dtwclust)
library(cluster)
library(ggh4x)
set.seed(12765)

Demographic and anthropometric data analysis

Load input data:

demographic_anthropometric_force_data <- 
  read_csv("demographic_anthropometric_force_data.csv") %>%
  mutate(testing_group = as.factor(testing_group))

Descriptives table

Table 1 Anthropometric and demographic data. Shown as means (± standard deviation (SD)).

Show function to create descriptives table
# FUNCTION: create_descriptives_table

# Creates formatted table for several descriptive variables.
# (age, height, mass, hand training time, testing group)

# Inputs:
# - data: "demographic_anthropometric_force_data.csv".

# Outputs:
# - Formatted table of descriptive measures

create_descriptives_table <- function(data) {
  # Step 1: Select and rename relevant columns for analysis
  # Select demographic variables and testing group
  # Rename columns to more readable format for final table
  general_descriptives <- data %>%
    dplyr::select(age, height, mass, specific_training_time, testing_group) %>%
    rename(
      `Hand Training Time` = specific_training_time,
      `Age` = age,
      `Height` = height,
      `Mass` = mass
    ) %>%
    
    # Step 2: Calculate summary statistics by testing group
    # Group data by testing_group and calculate mean and SD for each variable
    # .names parameter creates new column names in format: variable_statistic
    group_by(testing_group) %>%
    summarise(across(everything(), list(
      mean = ~mean(., na.rm = TRUE),  # Calculate mean, removing NA values
      sd = ~sd(., na.rm = TRUE)       # Calculate std dev, removing NA values
    ), .names = "{col}_{fn}")) %>%
    
    # Step 3: Reshape data for presentation
    # First pivot: Transform summary statistics into separate rows
    # Converts from wide to long format
    pivot_longer(
      cols = -testing_group,           # Keep testing_group as is
      names_to = c("Variable", ".value"), # Split column names into variable name and statistic type
      names_pattern = "^(.*)_(mean|sd)$"  # Pattern to match column names
    ) %>%
    
    # Second pivot: Create separate columns for each testing group
    # Transforms data to have mean and SD columns for each group
    pivot_wider(
      names_from = testing_group,      # Create columns based on testing group
      values_from = c(mean, sd)        # Spread mean and SD values
    )
  
  # Step 4: Format table using gt (great table) package
  general_descriptives %>%
    gt() %>%
    # Format all numeric columns to 2 decimal places
    fmt_number(
      columns = everything(),
      decimals = 2
    ) %>%
    
    # Step 5: Rename columns with clear, formatted labels
    cols_label(
      Variable = "Measure",
      mean_dexterity = "Mean",
      sd_dexterity = md("&plusmn;SD"),  # Plus/minus symbol (markdown)
      mean_strength = "Mean",
      sd_strength = md("&plusmn;SD")    # Plus/minus symbol (markdown)
    ) %>%
    
    # Step 6: Add column groupings (spanners)
    # Group strength-related columns
    tab_spanner(
      label = "Strength-trained",
      columns = c(mean_strength, sd_strength)
    ) %>%
    # Group dexterity-related columns
    tab_spanner(
      label = "Dexterity-Trained",
      columns = c(mean_dexterity, sd_dexterity)
    ) %>%
    
    # Step 7: Set column widths for consistent formatting
    cols_width(
      Variable ~ px(250),       # Variable names get more space
      everything() ~ px(100)    # All other columns same width
    ) %>% 
    
    # Step 8: Apply table styling options
    tab_options(
      table.background.color = "white",
      row.striping.background_color = "white",
      table_body.hlines.style = "none",           # Remove horizontal lines in body
      column_labels.border.bottom.color = "black", # Add borders to column labels
      column_labels.border.top.color = "black",
      table_body.border.bottom.color = "black",   # Add border to bottom of table
      table.border.top.color = "black",           # Add border to top of table
      table.border.bottom.color = "black",
      heading.border.bottom.color = "black"
    ) %>% 
    
    # Step 9: Disable row striping
    opt_row_striping(
      row_striping = FALSE
    )
}
descriptive_table <- create_descriptives_table(
  demographic_anthropometric_force_data)
descriptive_table
Measure Dexterity-Trained Strength-trained
Mean

±SD

Mean

±SD

Age 23.00 5.27 23.70 4.06
Height 182.80 7.26 174.04 5.94
Mass 74.99 9.22 67.41 8.66
Hand Training Time 12.10 4.04 13.47 4.55

Calculate bootstrapped means and 95% confidence intervals for each group

  • Each calculation uses 10000 bootstrapped samples (with replacement).

  • The calculations are used later for plotting.

  • NOTE: this script may produce bootstrapped means and 95% CI marginally different from those published. This is common with the bootstrapping approach, as bootstrapping involves random resampling of data. The changes will be minimal, given we have set a seed at the beginning of this script, and will not affect overall conclusions.

Show functions to calculate group mean and 95% CI
# FUNCTION: calculate_group_boot_confint

# Calculates bootstrap confidence intervals separately for dexterity and strength groups

# Inputs:
# - dataframe: "demographic_anthropometric_force_data.csv". 
# - columns to exclude: participant ID and testing group (as these do not need to be analysed).
# - number of bootstrap replicates: R = 10000 is used.

# Outputs:
# - dataframe containing bootstrapped means and 95% confidence intervals for each group/column

calculate_group_boot_confint <- function(data, exclude_columns = c("participant", "testing_group"), R = 10000) {
  # Get columns to analyze (excluding specified ones)
  columns_to_include <- setdiff(names(data), exclude_columns)
  
  # Process each column with the bootstrapping "boot" function.
  ci_means <- lapply(columns_to_include, function(column) {
    # Bootstrap for dexterity group means
    results_dexterity <- boot(data, 
                              \(data, indices) mean_per_group(data, indices, "dexterity", column), 
                              R)
    # Bootstrap for strength group means
    results_strength <- boot(data, 
                             \(data, indices) mean_per_group(data, indices, "strength", column), 
                             R)
    
    # Calculate 95% confidence intervals using percentile method
    ci_dexterity <- boot.ci(results_dexterity, type = "perc")
    ci_strength <- boot.ci(results_strength, type = "perc")
    
    # Combine results into single dataframe with group means and 95% CIs
    data.frame(
      Testing_Variable = column,
      testing_group = c("dexterity", "strength"),
      Mean = c(mean(data[[column]][data$testing_group == "dexterity"]),
               mean(data[[column]][data$testing_group == "strength"])),
      Lower_CI = c(ci_dexterity$percent[4], ci_strength$percent[4]),  # 2.5th percentile
      Upper_CI = c(ci_dexterity$percent[5], ci_strength$percent[5])   # 97.5th percentile
    )
  })
  do.call(rbind, ci_means) %>%   # Combine all results
    mutate(across(c('Mean', 'Lower_CI', 'Upper_CI'), \(x) round(x, 5))) # Round to 5 decimal places
}


# HELPER FUNCTION: mean_per_group

# Helper function to calculate mean for a specific group during bootstrap
# Used by calculate_group_boot_confint

mean_per_group <- function(data, indices, testing_group, column) {
  d <- data[indices, ]  # Get bootstrap sample
  group_data <- d[[column]][d$testing_group == testing_group]  # Extract group data
  return(mean(group_data))  # Calculate mean
}
group_means_and_ci_boot <- calculate_group_boot_confint(
    demographic_anthropometric_force_data)

rounded_group_means_and_ci_boot <- group_means_and_ci_boot %>%
  mutate(across(c('Mean', 'Lower_CI', 'Upper_CI'), \(x) round(x, 2))) # Round to 2dp
  
kable(rounded_group_means_and_ci_boot) %>%
  kable_styling(c("striped", "hover", "condensed")) %>%
  collapse_rows(columns = 1, valign = "middle")
Testing_Variable testing_group Mean Lower_CI Upper_CI
age dexterity 23.00 20.11 26.50
strength 23.70 21.67 26.50
height dexterity 182.80 178.61 187.37
strength 174.04 170.58 177.73
mass dexterity 74.99 69.07 80.33
strength 67.41 62.37 72.79
forearm_circumference dexterity 264.20 256.86 271.73
strength 276.50 262.64 287.22
fds_thickness dexterity 191.65 175.22 206.42
strength 195.11 173.71 217.02
time_taken_to_complete_dexterity dexterity 15.47 14.26 16.64
strength 17.54 16.29 18.93
dexterity_accuracy dexterity 98.45 97.46 99.44
strength 99.38 98.45 100.00
dexterity_attempts dexterity 1.10 1.00 1.33
strength 1.10 1.00 1.33
max_force dexterity 112.64 99.47 124.69
strength 130.02 119.56 139.16
max_force_bodymass_normalised dexterity 1.53 1.30 1.76
strength 1.94 1.80 2.09
max_force_fds_thickness_normalised dexterity 0.59 0.53 0.65
strength 0.68 0.62 0.75
max_force_forearm_circumference_normalised dexterity 0.42 0.38 0.47
strength 0.47 0.45 0.50
specific_training_time dexterity 12.10 9.89 14.67
strength 13.47 10.75 16.25
general_resistance_training_time dexterity 0.75 0.22 1.33
strength 1.10 0.29 2.17
cov_force_15 dexterity 0.04 0.04 0.05
strength 0.04 0.03 0.04
cov_force_35 dexterity 0.03 0.02 0.03
strength 0.02 0.02 0.03
cov_force_55 dexterity 0.03 0.02 0.03
strength 0.02 0.02 0.03
cov_force_70 dexterity 0.03 0.03 0.03
strength 0.02 0.02 0.03

Calculate mean differences and 95% confidence intervals

Each calculation uses the formula

  • \(\bar{x}_{(dexterity-trained)}- \bar{x}_{(strength-trained)}\).

  • Therefore:

    • If CI is positive and doesn’t cross 0: dexterity-trained>strength-trained.

    • If CI is negative and doesn’t cross 0: strength-trained>dexterity-trained.

    • If CI crosses 0: no difference between groups.

  • NOTE: this script may produce bootstrapped mean differences and 95% CI marginally different from those published. This is common with the bootstrapping approach, as bootstrapping involves random resampling. The changes will be incredibly minimal, given we have set a seed at the beginning of this script, and would not affect overall conclusions.

Show functions to calculate mean differences and 95% CI
# FUNCTION: calculate_meandiff_boot_confint

# Calculates bootstrap confidence intervals for differences between groups

# Inputs:
# - dataframe: "demographic_anthropometric_force_data.csv". 
# - columns to exclude: participant ID and testing group (as these do not need to be analysed).
# - number of bootstrap replicates: R = 10000 is used.

# Outputs:
# - dataframe containing bootstrapped group difference of means and their 95% confidence intervals.


calculate_meandiff_boot_confint <- function(data, exclude_columns = c("participant", "testing_group"), R = 10000) {
  columns_to_include <- setdiff(names(data), exclude_columns)
  
  # Process each column with the bootstrapping "boot" function.
  ci_results <- lapply(columns_to_include, function(column) {
    # Calculate bootstrap samples of mean differences
    results <- boot(data, \(data, indices) mean_diff(data, indices, column), R)
    ci <- boot.ci(results, type = "perc")  # Get confidence intervals
    
    # Store results with column name
    data.frame(
      Testing_Variable = column,
      Mean_Diff = mean_diff(data, 1:nrow(data), column),
      Lower_CI = ci$percent[4],    # 2.5th percentile
      Upper_CI = ci$percent[5]     # 97.5th percentile
    )
  })
  # Combine and round results
  do.call(rbind, ci_results) %>% 
    mutate(across(c('Mean_Diff', 'Lower_CI', 'Upper_CI'), \(x) round(x, 5))) # Round to 5 decimal places
}


# HELPER FUNCTION: mean_diff

# Helper function to calculate difference in means between groups
# Used by calculate_meandiff_boot_confint

mean_diff <- function(data, indices, column) {
  d <- data[indices, ]  # Get bootstrap sample
  # Calculate means for each group
  dexterity <- d[[column]][d$testing_group == "dexterity"]
  strength <- d[[column]][d$testing_group == "strength"]
  return(mean(dexterity) - mean(strength))  # Return difference
}
mean_difference_ci_boot <- calculate_meandiff_boot_confint(
  demographic_anthropometric_force_data)
rounded_mean_difference_ci_boot <- mean_difference_ci_boot %>% 
  mutate(across(c('Mean_Diff', 'Lower_CI', 'Upper_CI'), \(x) round(x, 2))) # Round to 2 dp

kable(rounded_mean_difference_ci_boot) %>% 
  kable_styling(c("striped", "hover","condensed"))
Testing_Variable Mean_Diff Lower_CI Upper_CI
age -0.70 -4.50 3.40
height 8.76 3.21 14.59
mass 7.58 -0.47 15.02
forearm_circumference -12.30 -25.90 3.39
fds_thickness -3.46 -30.52 22.19
time_taken_to_complete_dexterity -2.07 -3.87 -0.40
dexterity_accuracy -0.93 -2.17 0.39
dexterity_attempts 0.00 -0.27 0.27
max_force -17.38 -33.26 -1.37
max_force_bodymass_normalised -0.41 -0.68 -0.14
max_force_fds_thickness_normalised -0.09 -0.18 0.00
max_force_forearm_circumference_normalised -0.04 -0.09 0.01
specific_training_time -1.37 -4.93 2.41
general_resistance_training_time -0.35 -1.55 0.67
cov_force_15 0.01 0.00 0.01
cov_force_35 0.00 0.00 0.01
cov_force_55 0.00 0.00 0.01
cov_force_70 0.00 0.00 0.01

Median difference between groups for number of attempts at dexterity task:

medians <- demographic_anthropometric_force_data %>%
  dplyr::filter(testing_group %in% c("dexterity", "strength")) %>%
  dplyr::group_by(testing_group) %>%
  dplyr::summarise(median_attempts = median(dexterity_attempts, na.rm = TRUE))

median_diff <- diff(medians$median_attempts)

cat("dexterity-trained median attempts =", medians$median_attempts[1],
    "\nstrength-trained median attempts =", medians$median_attempts[2],
    "\nmedian difference = ", median_diff)
dexterity-trained median attempts = 1 
strength-trained median attempts = 1 
median difference =  0

Calculate bootstrapped effect sizes and their 95% confidence intervals

  • Calculates bootstrapped between-group effect sizes as Cohen’s \(d\).
  • NOTE: this script may produce bootstrapped effect sizes and their 95% CI marginally different from those published. This is common with the bootstrapping approach, as bootstrapping involves random resampling. The changes will be incredibly minimal, given we have set a seed at the beginning of this script, and would not affect overall conclusions.
Show function to calculate effect sizes
# FUNCTION: apply_bootES

# Calculates Cohen's d effect size using bootES package

# Inputs:
# - dataframe: "demographic_anthropometric_force_data.csv". 
# - column_name: columns for analysis
# - number of bootstrap replicates: R = 10000 is used.

# Outputs:
# - BootES results: Cohen's d effect size and 95% confidence intervals, along with other metrics.

# Note: returns NULL for non-numeric columns

apply_bootES <- function(data, column_name, R = 10000) {
  if (is.numeric(data[[column_name]])) {
    # Prepare data for bootES analysis
    combined_boot <- data %>% 
      dplyr::select(testing_group, all_of(column_name))
    
    # Run bootES with 10000 replicates
    bootES(combined_boot, 
           R, 
           data.col = column_name, 
           group.col = "testing_group",
           contrast = c("strength","dexterity"),
           effect.type = "cohens.d")
  } else {
    return(NULL)
  }
}
columns_to_analyze <- setdiff(names
                              (demographic_anthropometric_force_data), 
                              "testing_group"
                              )

effect_sizes_boot <- setNames(
  lapply(columns_to_analyze, \(x) 
         apply_bootES(demographic_anthropometric_force_data, 
                      x)),columns_to_analyze)

effect_sizes_boot <- Filter(Negate(is.null), 
                            effect_sizes_boot)
  • Effect sizes:

    • Trivial when \(d < 0.2\).

    • Small when \(d = 0.2 - 0.5\).

    • Moderate when \(d = 0.5 - 0.8\).

    • Large when \(d \ge 0.8\).

  • Confidence Intervals:

    • If CI is positive and doesn’t cross 0: dexterity-trained>strength-trained.

    • If CI is negative and doesn’t cross 0: strength-trained>dexterity-trained.

    • If CI crosses 0: no difference between groups.

Show function to convert effect size results to dataframe
# FUNCTION: boot_results_to_df

# Converts bootES results to a df for easier viewing

# Inputs:
# - boot_list: a list of results from several colimns using the apply_bootES function.

# Outputs:
# - dataframe: containing Cohen's d effect size and 95% confidence intervals.

boot_results_to_df <- function(boot_list) {
  data.frame(
    Test_Variable = names(boot_list),
    Cohens_d = sapply(boot_list, function(x) round(x$t0, 2)),        # Round to 2 decimal places
    Lower_CI = sapply(boot_list, function(x) round(x$bounds[1], 2)), # Round to 2 decimal places
    Upper_CI = sapply(boot_list, function(x) round(x$bounds[2], 2))  # Round to 2 decimal places
  )
}
effect_sizes_table <- boot_results_to_df(effect_sizes_boot) %>%
  filter(Test_Variable != "participant") # Remove participant ID effect sizes.

kable(effect_sizes_table, row.names = FALSE) %>%
  kable_styling(c("striped", "hover","condensed"))
Test_Variable Cohens_d Lower_CI Upper_CI
age -0.15 -1.19 0.79
height 1.32 0.22 2.24
mass 0.85 -0.20 1.95
forearm_circumference -0.72 -2.06 0.43
fds_thickness -0.11 -1.05 0.86
time_taken_to_complete_dexterity -1.01 -1.82 -0.11
dexterity_accuracy -0.63 -2.05 0.20
dexterity_attempts 0.00 -0.88 0.67
max_force -0.93 -1.88 0.10
max_force_bodymass_normalised -1.28 -2.27 -0.31
max_force_fds_thickness_normalised -0.83 -1.68 0.16
max_force_forearm_circumference_normalised -0.77 -1.67 0.17
specific_training_time -0.32 -1.28 0.68
general_resistance_training_time -0.27 -1.08 0.77
cov_force_15 0.58 -0.44 1.30
cov_force_35 0.61 -0.55 2.07
cov_force_55 0.60 -0.37 1.76
cov_force_70 0.72 -0.30 1.61

Run linear regression analyses

Equation used for linear regression analyses were:

\[ COV \sim Force_{max} \]

Units:

  • COV: %

  • Force: N

Note: check_model plots are in for models 15%, 35%, 55% and 70% MVC (in that order).

Show functions to calculate linear regression analyses
# FUNCTION: prepare_force_data

# This function prepares force data for plotting

# Inputs:
# - data: "demographic_anthropometric_force_data.csv".

# Outputs:
# - Long data: transformed data for linear regression analyses
prepare_force_data <- function(data) {
  force_levels <- c("15", "35", "55", "70")
  
  # Create long format data
  long_data <- data %>%
    dplyr::select(max_force, testing_group, all_of(paste0("cov_force_", force_levels))) %>%
    pivot_longer(
      cols = starts_with("cov_force_"),
      names_to = "force_level",
      values_to = "cov_force",
      names_prefix = "cov_force_"
    ) %>%
    mutate(
      force_level_label = paste0(force_level, "% MVC"),
      cov_force_pct = cov_force * 100
    )
  
  return(long_data)
}


# FUNCTION: calculate_lm_force_COV
# Calculates linear model statistics and creates performance plots
# Inputs:
# - long_data (from previous function)
# Outputs:
# - Table of statistics from the lm of max force and COV
# - Performance plots for each model
calculate_lm_force_COV <- function(long_data) {
  stats_data <- long_data %>%
    group_by(force_level, force_level_label) %>%
    do({
      model <- lm(cov_force_pct ~ max_force, data = .)
      model_summary <- summary(model)
      
      # # Create performance plot with title
      plot_title <- paste(unique(.$force_level_label))
      check1 <- plot(check_heteroscedasticity(model))
      check2 <- plot(check_normality(model))
      check3 <- plot(check_predictions(model))
      plot_obj <- check1 + check2 + check3 + plot_layout(ncol = 1, guides = "collect") + plot_annotation(title = plot_title) &
      theme(legend.position = "bottom")
      print(plot_obj)
      
      
      
      # Extract coefficients
      intercept <- model_summary$coefficients[1, 1]
      slope <- model_summary$coefficients[2, 1]
      
      # Get confidence intervals for slope
      ci <- confint(model, level = 0.95)
      slope_ci_lower <- round(ci[2, 1], 2)
      slope_ci_upper <- round(ci[2, 2], 2)
      
      data.frame(
        r_squared = round(model_summary$r.squared,2),
        adj_r_squared = round(model_summary$adj.r.squared,2),
        intercept = round(intercept,2),
        slope = round(slope,2),
        slope_ci_lower = slope_ci_lower,
        slope_ci_upper = slope_ci_upper
      )
    }) %>%
    ungroup() %>%
    mutate(
      r2_text = paste0("Adj. R² = ", gsub("-", "−", sprintf("%.2f", adj_r_squared))),
      formula_text = paste0(
        "y = ", 
        sprintf("%.2f", intercept), 
        ifelse(slope >= 0, " + ", " − "), 
        sprintf("%.2f", abs(slope)), "x"
      )
    )
  
  return(stats_data)
}
# Prepare the data
long_data <- prepare_force_data(demographic_anthropometric_force_data)

# Calculate statistics with confidence intervals
stats_data <- calculate_lm_force_COV(long_data)

# Print the statistics data
stats_table_data <- stats_data %>% dplyr::select(-c(force_level, r2_text, formula_text))
kable(stats_table_data, row.names = FALSE) %>%
  kable_styling(c("striped", "hover","condensed"))
force_level_label r_squared adj_r_squared intercept slope slope_ci_lower slope_ci_upper
15% MVC 0.45 0.42 7.39 -0.03 -0.05 -0.01
35% MVC 0.08 0.03 3.64 -0.01 -0.02 0.01
55% MVC 0.10 0.05 3.85 -0.01 -0.03 0.01
70% MVC 0.05 -0.01 3.56 -0.01 -0.02 0.01

Plotting

  • Notes:

    • Female data points were tagged after their creation using an “F” in Inkscape.

    • Some styling and organisation of plots were done in InkScape after their creation. All data elements were kept identical to their output, only aesthetic changes were made.

Maximum force output plot

Show function to create maxmium and nornalised force plots
# FUNCTION: create_force_plot

# Creates raincloud plots for maximum force metrics.

# Inputs:
# - data: "demographic_anthropometric_force_data.csv".
# - ci_data: output from the function "calculate_group_boot_confint" in "calculate_bootstrap_confidence_intervals.R".
# - force_var: column name of the force variable used (i.e. "max_force" or "max_force_forearm_circumference_normalised").
# - y_label: Label of the y-axis (such as "Force Output (N)")
# - strip label: 90 degree rotated plot title in grey shaded box (such as "Gross Force").

# Outputs:
# - Force variable raincloud plots.


create_force_plot <- function(data, ci_data, force_var, y_label, strip_label) {
  # Controls spacing of the rain/jitter plot elements
  rain_height <- 0.07
  
  # Add a dummy faceting variable to the data
  data$facet_var <- strip_label
  
  # Filter confidence interval data and add faceting variable
  ci_data_filtered <- ci_data[ci_data$Testing_Variable == force_var, ]
  ci_data_filtered$facet_var <- strip_label
  
  # Ensure consistent ordering of groups in visualization
  # 'strength' appears before 'dexterity' in the plot
  data$testing_group <- factor(data$testing_group, levels = c("strength", "dexterity"))
  
  # Begin building the plot with multiple layers.
  ggplot() +
    # Layer 1: Violin plots ("clouds").
    # Shows distribution kernel density of the data (uses the introdataviz package).
    introdataviz::geom_flat_violin(
      data = data, 
      aes(x = "", y = .data[[force_var]], fill = testing_group), 
      trim = FALSE,  # Don't trim the tails of the distribution
      alpha = 0.9,   # Slight transparency
      show.legend = FALSE,
      position = position_nudge(x = rain_height + 0.07)  # Shift position slightly
    ) +
    
    # Layer 2: Individual participant data points ("rain")
    # Jittered points showing participant raw data
    geom_point(
      data = data, 
      aes(x = "", y = .data[[force_var]], colour = testing_group, shape = testing_group), 
      size = 2.2,
      stroke = 0.5,
      show.legend = FALSE,  # Hide from legend since shown in violin plot
      position = position_jitter(width = rain_height - 0.055)  # Add random noise
    ) +
    
    # Layer 3: Box plots
    # Show quartiles and median
    geom_boxplot(
      data = data, 
      aes(x = "", y = .data[[force_var]], fill = testing_group), 
      width = 0.07, 
      show.legend = FALSE, # Hide from legend since shown in violin plot
      outlier.shape = NA,  # Hide outliers since shown in rain plot
      position = position_dodgenudge(width = 0.075, x = -rain_height * 1.8) # Change position to be below "rain".
    ) +
    
    # Layer 4: Confidence intervals
    # Show mean and CI from bootstrap analysis positioned inside the "clouds"
    geom_pointrange(
      data = ci_data_filtered, 
      aes(x = "", y = Mean, ymin = Lower_CI, ymax = Upper_CI, 
          color = testing_group, shape = testing_group, fill = testing_group), 
      linewidth = 1, 
      size = 0.6, 
      show.legend = FALSE,
      position = position_dodgenudge(x = rain_height + 0.14, 
                                     width = 0.05) # Position inside the "clouds".
    ) +
    
    # Add faceting with placeholder variable
    facet_grid(rows = vars(facet_var), switch = "y") +
    
    # Configure scales and coordinate system
    scale_x_discrete(name = "", expand = c(rain_height * 3, 0, 0, 0.7)) +
    scale_y_continuous() +
    coord_flip() +  # Flip coordinates for horizontal layout
   theme_bw() + 
    # Apply theme and styling which will be consistent across plots
    theme(
      panel.grid.major = element_blank(),
      panel.grid.minor = element_blank(),
      panel.background = element_blank(),
      panel.border = element_blank(),
      axis.line.y = element_blank(),
      axis.line.x = element_line(color = "grey70", linewidth = 0.5),
      axis.ticks.y = element_blank(),
      axis.ticks.x = element_line(color = "grey70", linewidth = 0.5),
      axis.title.y = element_blank(),
      axis.text.y = element_blank(),
      axis.title.x = element_text(face = "bold", size = 11),
      axis.text.x = element_text(size = 9),
      strip.background = element_rect(fill = "grey90", color = NA),  # Remove the box around facet labels
      legend.position = "none",
      strip.text.y = element_text(
        angle = 90,
        face = "bold",
        size = 12
      )
    ) +
    
    # Add labels and titles
    labs(
      y = y_label,
      fill = "Testing Group"
    ) +
    
    # Define consistent visual encoding for testing groups
    scale_shape_manual(values = c("strength" = 21, "dexterity" = 24),
                       labels = c("strength" = "Strength-Trained", "dexterity" = "Dexterity-Trained")) +
    scale_color_manual(values = c("strength" = "#fe9d5d", "dexterity" = "#365373"),
                       labels = c("strength" = "Strength-Trained", "dexterity" = "Dexterity-Trained")) +
    scale_fill_manual(values = c("strength" = "#fe9d5d", "dexterity" = "#456990"),
                      labels = c("strength" = "Strength-Trained", "dexterity" = "Dexterity-Trained"))
}
gross_force_plot <- create_force_plot(
   demographic_anthropometric_force_data,
   group_means_and_ci_boot,
   "max_force",
   "Force (N)",
   "Gross Force"
 )

Forearm-circumference normalised force output plot

norm_force_plot <- create_force_plot(
  demographic_anthropometric_force_data,
  group_means_and_ci_boot,
  "max_force_forearm_circumference_normalised",
  "Force/unit forearm circumference (N/cm)",
  "Normalized Force"

)

Create coefficient of variation of steady force plot

Show function to create coefficient of variation of steady force plot
# FUNCTION: create_cov_force_plot

# Creates faceted plot that shows 95% CI and raw data points for foce steadiness (coefficient of varation of force)
# during the submaximal force trials

# Inputs:
# - data: "demographic_anthropometric_force_data.csv".
# - ci_data: output from the function "calculate_group_boot_confint" in "calculate_bootstrap_confidence_intervals.R".

# Outputs:
# - Force steadiness plot (coefficient of variation of force).

create_cov_force_plot <- function(data, ci_data) {
  # Prepare coefficient of variation (COV) of force data for plotting
  # Select relevant columns
  plot_cov <- data %>% 
    dplyr::select(participant, testing_group, cov_force_15, cov_force_35, cov_force_55, cov_force_70)
  
  # Convert wide format to long format for plotting
  plot_cov_force_long <- plot_cov %>%
    pivot_longer(cols = -c(testing_group, participant), 
                 names_to = "Variable", 
                 values_to = "Value") %>% 
    # Recode variable names and set factor levels for proper ordering
    mutate(
      # Convert numeric suffixes to percentage labels
      Variable = factor(recode(Variable,
                               "cov_force_15" = "15%",
                               "cov_force_35" = "35%",
                               "cov_force_55" = "55%",
                               "cov_force_70" = "70%"),
                        levels = c("70%", "55%", "35%", "15%")),  # Order from high to low
      testing_group = factor(testing_group, levels = c("strength", "dexterity"))
    )
  
  # Similarly prepare confidence interval data
  plot_cov_force_CI <- ci_data %>% 
    filter(Testing_Variable %in% c("cov_force_15", "cov_force_35", "cov_force_55", "cov_force_70")) %>% 
    mutate(
      Variable = factor(recode(Testing_Variable,
                               "cov_force_15" = "15%",
                               "cov_force_35" = "35%",
                               "cov_force_55" = "55%",
                               "cov_force_70" = "70%"),
                        levels = c("70%", "55%", "35%", "15%")),
      testing_group = factor(testing_group, levels = c("strength", "dexterity"))
    )
  
  # Create multi-panel plot
  ggplot() + 
    # Layer 1: Individual participant data points with jitter.
    geom_jitter(data = plot_cov_force_long, 
                aes(x = Value, y = testing_group, color = testing_group, shape = testing_group), 
                size = 2.5, 
                stroke = 0.5, 
                position = position_jitterdodge(jitter.width = 0.05, dodge.width = 0.5),
                show.legend = FALSE) +
    
    # Layer 2: Bootstrapped mean and 95% confidence intervals
    geom_pointrange(data = plot_cov_force_CI, 
                    aes(x = Mean, y = testing_group, 
                        xmin = Lower_CI, xmax = Upper_CI, 
                        color = testing_group, shape = testing_group, 
                        fill = testing_group),
                    size = 1, 
                    linewidth = 1.7, 
                    position = position_dodge(width = 0.5)) +
    
    # Create separate panels for each force level
    facet_grid(rows = vars(Variable), scales = "free_y", switch = "y") + 
    
    # Apply theme and styling which will be consistent across plots
    theme_bw() +
    theme(
      panel.grid.major = element_blank(),
      panel.grid.minor = element_blank(),
      panel.border = element_rect(colour = "grey70"),
      legend.position = "bottom",
      legend.title = element_text(size = 9, face = "bold"),
      legend.background = element_blank(),
      legend.text = element_text(size = 9),
      axis.title.y = element_text(angle = 90, size = 12, face = "bold", hjust = 0.5),
      axis.text.y = element_blank(), 
      axis.ticks.y = element_blank(),
      axis.line.y = element_blank(),
      strip.text.y = element_text(angle = 90, size = 11, face = "bold"),
      axis.title.x = element_text(size = 11, face = "bold", margin = margin(t = 10)),
      axis.text.x = element_text(size = 10, face = "bold"),
      axis.line.x = element_line(size = 0.3, colour = "grey80"),
      strip.background = element_rect(fill = "grey90", color = NA)
    ) +
    
    # Add labels and configure scales
    labs(x = "COV",
         y = "Percentage of MVC",
         fill = "Testing Group",
         color = "Testing Group", 
         shape = "Testing Group") +
    scale_x_continuous(labels = scales::percent) +  # Format x-axis as force level percentages
    
    # Apply consistent visual encoding for testing groups
    scale_shape_manual(values = c("strength" = 21, "dexterity" = 24),
                       labels = c("strength" = "Strength-Trained", "dexterity" = "Dexterity-Trained")) +
    scale_color_manual(values = c("strength" = "#fe9d5d", "dexterity" = "#456990"),
                       labels = c("strength" = "Strength-Trained", "dexterity" = "Dexterity-Trained")) +
    scale_fill_manual(values = c("strength" = "#fe9d5d", "dexterity" = "#456990"),
                      labels = c("strength" = "Strength-Trained", "dexterity" = "Dexterity-Trained"))
}
cov_plot <- create_cov_force_plot(
  demographic_anthropometric_force_data, 
  group_means_and_ci_boot
  )

Linear regression plots

Show functions to calculate linear regression analyses
# FUNCTION: create_lm_plots

# Calculates 

# Inputs:
# - long_data (from linear regression function above)
# - stats_data (from linear regression function above)

# Outputs:
# - Plot of lm for each force level.

create_lm_plots <- function(long_data, stats_data) {
  p <- ggplot(long_data, aes(x = max_force, y = cov_force_pct, color = testing_group, shape = testing_group)) +
    # Regression line
    geom_smooth(method = "lm", formula = y ~ x, se = TRUE, color = "black", linewidth = 0.8, 
                fill = "grey90", alpha = 1, 
                inherit.aes = FALSE, aes(x = max_force, y = cov_force_pct)) +
    # Participant data points
    geom_point(size = 2, stroke = 0.6) +
    # R² text
    geom_text(data = stats_data,
              aes(x = Inf, y = Inf, label = r2_text),
              hjust = 1.1, vjust = 3.7,
              size = 3.2, color = "black",
              inherit.aes = FALSE) +
    # Slope text
    geom_text(data = stats_data,
              aes(x = Inf, y = Inf, label = formula_text),
              hjust = 1.1, vjust = 1.7,
              size = 3.2, color = "black",
              inherit.aes = FALSE) +
    facet_grid(. ~ force_level_label, scales = "free_x") +
    labs(x = "Max Force (N)", y = "COV Force (%)") +
    scale_y_continuous(breaks = scales::pretty_breaks(n = 3), 
                       labels = function(x) paste0(x, "%")) +
    scale_shape_manual(values = c("strength" = 21, "dexterity" = 24),
                       labels = c("strength" = "Strength-Trained", 
                                  "dexterity" = "Dexterity-Trained")) +
    coord_cartesian(ylim = c(NA, max(long_data$cov_force_pct, na.rm = TRUE) * 1.05)) +
    scale_color_manual(
      name = "Testing Group",
      values = c("strength" = "#fe9d5d", "dexterity" = "#456990"),
      labels = c("strength" = "Strength-Trained", "dexterity" = "Dexterity-Trained")
    ) +
    theme_bw() +
    theme(
      panel.border = element_rect(color = "grey70", fill = NA, linewidth = 0.5),
      panel.grid.major = element_blank(),
      panel.grid.minor = element_blank(),
      panel.background = element_blank(),
      strip.background = element_rect(color = NA, fill = "grey90"),
      strip.text = element_text(face = "bold", size = 12),
      strip.placement = "outside",
      axis.title.x = element_text(face = "bold", margin = margin(t = 10)),
      axis.text.x = element_text(size = 9),
      axis.title.y = element_text(face = "bold"),
      axis.text.y = element_text(size = 9),
      axis.ticks = element_line(color = "grey70", linewidth = 0.3),
      legend.position = "none"
    )
  
  return(p)
}
lm_plot <- create_lm_plots(long_data, stats_data)

Combine plots into final paneled plot

combined_plot <- (gross_force_plot| norm_force_plot) / (cov_plot) / (lm_plot) +
  plot_layout(
    guides = 'collect', # Collect all legends
    heights = c(0.3, 0.6, 0.2) # Adjust height proportions
  ) +
  plot_annotation(tag_levels = 'A',
                  theme = theme(legend.position = "bottom")) &
  theme(plot.tag = element_text(face = "bold"),
        plot.tag.position = c(0.01, 1.01))

combined_plot

Motor unit data analyses

mu_data <- read_csv("mu_data.csv", 
    col_types = cols(testing_group = col_factor(levels = c("dexterity", 
        "strength")), force_level = col_factor(levels = c("15", 
        "35", "55", "70")), trial = col_factor(levels = c("1", 
        "2", "3")), muscle = col_factor(levels = c("apb", 
        "fds"))))

Motor unit table

Show functions to create table of average MUs per participant
# FUNCTION: create_MU_summary_table

# Calculates 

# Inputs:
# - mu_data

# Outputs:
# - Table of identified MUs across the three trials, per participant and for each force level.

create_MU_summary_table <- function(mu_data) {
  # Create summary table
  summary_table <- mu_data %>%
    group_by(force_level, muscle, testing_group) %>%
    summarize(
      n_participants = n_distinct(participant),
      avg_motor_units = n() / n_distinct(participant),
      .groups = 'drop'
    ) %>%
    # Reshape to have separate columns for each measure
    pivot_wider(
      names_from = c(testing_group, muscle),
      values_from = c(n_participants, avg_motor_units),
      names_sep = "_"
    )

  # Reorder columns to group by testing_group first
  desired_order <- c("force_level")
  for(testing_group in c("strength", "dexterity")) {
    for(muscle in c("apb", "fds")) {
      desired_order <- c(desired_order,
                        paste0("n_participants_", testing_group, "_", muscle),
                        paste0("avg_motor_units_", testing_group, "_", muscle))
    }
  }

  summary_table <- summary_table %>%
    dplyr::select(all_of(desired_order))

  # Create GT table
  final_table <- summary_table %>%
    gt()

  # First add the lower level (muscle) spanners
  for(testing_group in c("strength", "dexterity")) {
    # Add APB spanner
    apb_cols <- c(
      paste0("n_participants_", testing_group, "_apb"),
      paste0("avg_motor_units_", testing_group, "_apb")
    )
    
    final_table <- final_table %>%
      tab_spanner(
        label = "APB",
        columns = all_of(apb_cols),
        id = paste0(testing_group, "_apb")
      )
    
    # Add FDS spanner
    fds_cols <- c(
      paste0("n_participants_", testing_group, "_fds"),
      paste0("avg_motor_units_", testing_group, "_fds")
    )
    
    final_table <- final_table %>%
      tab_spanner(
        label = "FDS",
        columns = all_of(fds_cols),
        id = paste0(testing_group, "_fds")
      )
  }

  # Then add the top level (testing_group) spanners
  # Strength-trained columns
  strength_cols <- c(
    "n_participants_strength_apb",
    "avg_motor_units_strength_apb",
    "n_participants_strength_fds",
    "avg_motor_units_strength_fds"
  )

  final_table <- final_table %>%
    tab_spanner(
      label = "Strength-trained",
      columns = all_of(strength_cols),
      id = "strength"
    )

  # Dexterity-trained columns
  dexterity_cols <- c(
    "n_participants_dexterity_apb",
    "avg_motor_units_dexterity_apb",
    "n_participants_dexterity_fds",
    "avg_motor_units_dexterity_fds"
  )

  final_table <- final_table %>%
    tab_spanner(
      label = "Dexterity-trained",
      columns = all_of(dexterity_cols),
      id = "dexterity"
    )

  # Add column labels
  col_labels <- list(force_level = "Force Level")

  # Define column labels systematically
  for(testing_group in c("strength", "dexterity")) {
    for(muscle_name in c("apb", "fds")) {
      col_labels[[paste0("n_participants_", testing_group, "_", muscle_name)]] <- "N"
      col_labels[[paste0("avg_motor_units_", testing_group, "_", muscle_name)]] <- "No. MUs"
    }
  }

  # Apply formatting
  final_table <- final_table %>%
    tab_options(
      column_labels.font.weight = "bold",
      # Table borders
      table.border.top.color = "black",
      table.border.top.width = px(1.5),
      table.border.bottom.color = "black",
      table.border.bottom.width = px(1.5),
      # Column label borders
      column_labels.border.top.color = "black",
      column_labels.border.top.width = px(1.5),
      column_labels.border.bottom.color = "black",
      column_labels.border.bottom.width = px(1.5),
      # Row group borders
      row_group.border.top.color = "black",
      row_group.border.top.width = px(1.5),
      row_group.border.bottom.color = "black",
      row_group.border.bottom.width = px(1.5),
      # Body borders
      table_body.border.top.color = "black",
      table_body.border.top.width = px(1.5),
      table_body.border.bottom.color = "black",
      table_body.border.bottom.width = px(1.5),
      # Spanner borders
      heading.border.bottom.color = "black",
      heading.border.bottom.width = px(1.5)
    ) %>%
    tab_style(
      style = list(
        cell_text(weight = "bold"),
        cell_borders(
          sides = c("top", "bottom"),
          color = "black",
          weight = px(1.5)
        )
      ),
      locations = cells_column_spanners()
    ) %>%
    # Add borders between column labels
    tab_style(
      style = cell_borders(
        sides = c("top", "bottom"),
        color = "black",
        weight = px(1.5)
      ),
      locations = cells_column_labels()
    ) %>%
    # Add borders to data cells - finer and lighter grey
    tab_style(
      style = cell_borders(
        sides = c("top", "bottom"),
        color = "#D3D3D3",
        weight = px(0.5)
      ),
      locations = cells_body()
    ) %>%
    # Keep the bottom border of the last row thick and black
    tab_style(
      style = cell_borders(
        sides = "bottom",
        color = "black",
        weight = px(1.5)
      ),
      locations = cells_body(
        rows = nrow(summary_table)
      )
    ) %>% 
    # Make force_level values bold
    tab_style(
      style = cell_text(weight = "bold"),
      locations = cells_body(columns = force_level)
    ) %>%
    cols_label(!!!col_labels) %>%
    # Transform text to add %
    text_transform(
      locations = cells_body(columns = force_level),
      fn = function(x) paste0(x, "%")
    ) %>%
    fmt_number(
      columns = contains("avg_motor_units"),
      decimals = 1
    ) %>%
    # Center align numeric columns
    cols_align(
      align = "center",
      columns = -force_level
    ) %>% 
    cols_align(
      align = "left",
      columns = force_level
    ) %>% 
    opt_horizontal_padding(scale = 2.6)

  # Return the table
  return(final_table)
}
final_table <- create_MU_summary_table(mu_data)
final_table
Strength-trained Dexterity-trained
Force Level APB FDS APB FDS
N No. MUs N No. MUs N No. MUs N No. MUs
15% 6 4.3 3 3.3 9 4.0 4 2.0
35% 10 8.3 5 7.2 10 9.7 5 4.6
55% 10 18.1 10 8.1 10 15.9 7 7.0
70% 9 18.1 9 11.9 10 19.1 9 6.9

Linear mixed effects model

This is a linear mixed effects model of average firing rate.

  • The fixed effects, which are all interaction terms include:

    • Testing group.

    • Force level.

    • Muscle being tested.

  • The random effects is:

    • Participant.

    • Firing (or recruitment) threshold.

model_participant_firing_threhsold <- lmer(avg_firing_rate 
                                           ~ testing_group 
                                           * force_level 
                                           * muscle
                                           +(1|participant) 
                                           + (1|firing_threshold),
                                           data=mu_data
                                           )

summary(model_participant_firing_threhsold)
Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula: avg_firing_rate ~ testing_group * force_level * muscle + (1 |  
    participant) + (1 | firing_threshold)
   Data: mu_data

REML criterion at convergence: 8652.4

Scaled residuals: 
     Min       1Q   Median       3Q      Max 
-2.18965 -0.46806 -0.00462  0.50564  2.68590 

Random effects:
 Groups           Name        Variance Std.Dev.
 firing_threshold (Intercept) 19.387   4.403   
 participant      (Intercept)  2.378   1.542   
 Residual                     24.558   4.956   
Number of obs: 1312, groups:  firing_threshold, 1233; participant, 20

Fixed effects:
                                               Estimate Std. Error        df
(Intercept)                                      8.9756     1.2206  306.4061
testing_groupstrength                            0.5606     1.8517  380.8512
force_level35                                    8.7190     1.3141 1294.7817
force_level55                                   10.8926     1.2344 1289.9666
force_level70                                   12.8065     1.2230 1289.0139
musclefds                                       -3.5144     2.6246 1279.3408
testing_groupstrength:force_level35             -6.0901     2.0046 1295.7599
testing_groupstrength:force_level55             -2.5711     1.8632 1289.5843
testing_groupstrength:force_level70             -1.3839     1.8591 1287.8217
testing_groupstrength:musclefds                 -0.6201     3.6155 1294.8645
force_level35:musclefds                         -9.4995     3.0297 1283.6760
force_level55:musclefds                         -8.2725     2.8193 1268.3432
force_level70:musclefds                         -9.2254     2.8046 1265.5318
testing_groupstrength:force_level35:musclefds    8.8483     4.1451 1293.0045
testing_groupstrength:force_level55:musclefds    3.7329     3.8653 1291.0599
testing_groupstrength:force_level70:musclefds    3.1491     3.8484 1291.0855
                                              t value Pr(>|t|)    
(Intercept)                                     7.353 1.78e-12 ***
testing_groupstrength                           0.303  0.76226    
force_level35                                   6.635 4.75e-11 ***
force_level55                                   8.824  < 2e-16 ***
force_level70                                  10.472  < 2e-16 ***
musclefds                                      -1.339  0.18080    
testing_groupstrength:force_level35            -3.038  0.00243 ** 
testing_groupstrength:force_level55            -1.380  0.16784    
testing_groupstrength:force_level70            -0.744  0.45680    
testing_groupstrength:musclefds                -0.172  0.86385    
force_level35:musclefds                        -3.135  0.00175 ** 
force_level55:musclefds                        -2.934  0.00340 ** 
force_level70:musclefds                        -3.289  0.00103 ** 
testing_groupstrength:force_level35:musclefds   2.135  0.03298 *  
testing_groupstrength:force_level55:musclefds   0.966  0.33436    
testing_groupstrength:force_level70:musclefds   0.818  0.41335    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Check model assumptions and fit:
check1 <- plot(check_heteroscedasticity(model_participant_firing_threhsold))
check2 <- plot(check_normality(model_participant_firing_threhsold))
check3 <- plot(check_predictions(model_participant_firing_threhsold))
check1 + check2 + check3 + plot_layout(ncol = 1, guides = "collect") &
  theme(legend.position = "bottom")

model_performance(model_participant_firing_threhsold)
# Indices of model performance

AIC      |     AICc |      BIC | R2 (cond.) | R2 (marg.) |   ICC |  RMSE | Sigma
--------------------------------------------------------------------------------
8690.351 | 8690.939 | 8788.758 |      0.674 |      0.385 | 0.470 | 3.719 | 4.956

Average firing rate calculations using estimated marginal means

Here we calculate the average firing rate estimated marginal means and 95% confidence intervals. These were calculated for each testing group and muscle, conditioned on force level.

emm <- emmeans(model_participant_firing_threhsold, 
               pairwise 
               ~ testing_group 
               | force_level 
               + muscle, 
               adjust = 'BH', 
               infer = T)

emm
$emmeans
force_level = 15, muscle = apb:
 testing_group emmean    SE     df lower.CL upper.CL t.ratio p.value
 dexterity       8.98 1.224  312.3    6.568    11.38   7.335  <.0001
 strength        9.54 1.395  462.1    6.794    12.28   6.834  <.0001

force_level = 35, muscle = apb:
 testing_group emmean    SE     df lower.CL upper.CL t.ratio p.value
 dexterity      17.69 0.859   86.2   15.986    19.40  20.589  <.0001
 strength       12.17 0.901  107.5   10.380    13.95  13.507  <.0001

force_level = 55, muscle = apb:
 testing_group emmean    SE     df lower.CL upper.CL t.ratio p.value
 dexterity      19.87 0.736   48.9   18.389    21.35  26.991  <.0001
 strength       17.86 0.696   41.4   16.452    19.26  25.653  <.0001

force_level = 70, muscle = apb:
 testing_group emmean    SE     df lower.CL upper.CL t.ratio p.value
 dexterity      21.78 0.704   41.4   20.362    23.20  30.956  <.0001
 strength       20.96 0.724   46.9   19.503    22.41  28.966  <.0001

force_level = 15, muscle = fds:
 testing_group emmean    SE     df lower.CL upper.CL t.ratio p.value
 dexterity       5.46 2.424 1088.5    0.705    10.22   2.253  0.0245
 strength        5.40 2.165  987.6    1.154     9.65   2.495  0.0127

force_level = 35, muscle = fds:
 testing_group emmean    SE     df lower.CL upper.CL t.ratio p.value
 dexterity       4.68 1.502  500.3    1.730     7.63   3.117  0.0019
 strength        7.38 1.252  308.0    4.916     9.84   5.894  <.0001

force_level = 55, muscle = fds:
 testing_group emmean    SE     df lower.CL upper.CL t.ratio p.value
 dexterity       8.08 1.120  204.2    5.873    10.29   7.214  <.0001
 strength        9.18 0.894  106.0    7.412    10.96  10.277  <.0001

force_level = 70, muscle = fds:
 testing_group emmean    SE     df lower.CL upper.CL t.ratio p.value
 dexterity       9.04 0.996  157.4    7.075    11.01   9.078  <.0001
 strength       10.75 0.829   78.0    9.098    12.40  12.967  <.0001

Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 

$contrasts
force_level = 15, muscle = apb:
 contrast             estimate   SE     df lower.CL upper.CL t.ratio p.value
 dexterity - strength  -0.5606 1.86  387.6  -4.2095    3.088  -0.302  0.7628

force_level = 35, muscle = apb:
 contrast             estimate   SE     df lower.CL upper.CL t.ratio p.value
 dexterity - strength   5.5296 1.24   96.5   3.0587    8.000   4.442  <.0001

force_level = 55, muscle = apb:
 contrast             estimate   SE     df lower.CL upper.CL t.ratio p.value
 dexterity - strength   2.0105 1.01   45.1  -0.0299    4.051   1.984  0.0533

force_level = 70, muscle = apb:
 contrast             estimate   SE     df lower.CL upper.CL t.ratio p.value
 dexterity - strength   0.8233 1.01   44.1  -1.2106    2.857   0.816  0.4190

force_level = 15, muscle = fds:
 contrast             estimate   SE     df lower.CL upper.CL t.ratio p.value
 dexterity - strength   0.0595 3.25 1062.0  -6.3178    6.437   0.018  0.9854

force_level = 35, muscle = fds:
 contrast             estimate   SE     df lower.CL upper.CL t.ratio p.value
 dexterity - strength  -2.6986 1.96  407.0  -6.5422    1.145  -1.380  0.1683

force_level = 55, muscle = fds:
 contrast             estimate   SE     df lower.CL upper.CL t.ratio p.value
 dexterity - strength  -1.1023 1.43  154.7  -3.9330    1.728  -0.769  0.4429

force_level = 70, muscle = fds:
 contrast             estimate   SE     df lower.CL upper.CL t.ratio p.value
 dexterity - strength  -1.7057 1.30  114.9  -4.2725    0.861  -1.316  0.1907

Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 

Plotting

Here the average firing rate is plotted, which each muscle in its own facet.

  • NOTE:

    • 15% MVC in the FDS muscle was not included in the analysis due to the low number of identified motor units.

    • Some styling and organisation of plots were done in InkScape after their creation. All data elements were kept identical to their output, only aesthetic changes were made.

Show function for plotting
# FUNCTION: create_emmeans_plot

# Creates geom_point plot for the estimated marginal mean motor unit firing rate 
# at each force level and for each muscle.

# Inputs:
# - data: output from the EMmeans function used for calculation of estimated marginal means            in linear
#         mixed effects models

# Outputs:
# - Plot of EMM average motor unit firing rate faceted by muscle.


create_emmeans_plot <- function(emm) {
  
  # Process estimated marginal means data:
  # - Rename columns for clarity 
  # - Convert force levels to numeric
  # - Format group names
  # - Filter to include only specific force levels for each muscle
  processed_emm_data <- as.data.frame(emm$emmeans) %>%
    rename(Mean = emmean, 
           Lower_CI = lower.CL, 
           Upper_CI = upper.CL) %>%
    mutate(
      force_level = as.numeric(gsub("MVC", "", as.character(force_level))),
      muscle = case_when(
        muscle == "apb" ~ "Abductor Pollicis Brevis",
        muscle == "fds" ~ "Flexor Digitorum Superficialis",
        TRUE ~ muscle
      ),
      testing_group = case_when(
        testing_group == "strength" ~ "Strength-Trained",
        testing_group == "dexterity" ~ "Dexterity-Trained",
        TRUE ~ testing_group
      )
    ) %>% 
    # Filter data to include:
    # - APB muscle: 15%, 35%, 55%, 70% force levels
    # - FDS muscle: 35%, 55%, 70% force levels
    filter((muscle == "Abductor Pollicis Brevis" & force_level %in% c(15, 35, 55, 70)) |
             (muscle == "Flexor Digitorum Superficialis" & force_level %in% c(35, 55, 70)))
  
  # Create plot showing estimated means and confidence intervals
  ggplot(processed_emm_data, aes(x = force_level, color = testing_group)) +
    # Add fine lines connecting means within each group
    geom_line(aes(y = Mean, group = testing_group, linetype = testing_group), 
              linewidth = 0.2, position = position_dodge(width = 2)) +
    # Add points with error bars showing confidence intervals
    geom_pointrange(aes(y = Mean, ymin = Lower_CI, ymax = Upper_CI, 
                        shape = testing_group, fill = testing_group), 
                    linewidth = 1.5, size = 0.7, position = position_dodge(width = 2)) +
    # Add labels and titles
    labs(
      x = "Percentage of MVC",
      y = "EMM Firing Rate",
      color = "Testing Group",
      shape = "Testing Group",
      fill = "Testing Group",
      linetype = "Testing Group"
    ) +
    # Apply black and white theme and customize appearance
    theme_bw() +
    theme(
      panel.grid.major = element_blank(),
      panel.grid.minor = element_blank(),
      panel.border = element_rect(colour = "grey70"),
      legend.position = "bottom",
      strip.text = element_text(size = 12, face = "bold"),
      axis.title.x = element_text(size = 11, face = "bold", margin = margin(t = 15)),
      axis.text.x = element_text(size = 9, face = "bold"),
      axis.line.x = element_line(size = 0.3, colour = "grey80"),
      axis.title.y.left = element_text(size = 11, face = "bold", margin = margin(r = 10)),
      axis.text.y = element_text(size = 9),
      title = element_blank(),
      strip.background = element_rect(fill = "grey90", color = NA) ,
      legend.title = element_text(size = 9, face = "bold"),
      legend.text = element_text(size = 9)
    ) +
    # Configure x-axis scale and labels
    scale_x_continuous(
      limits = c(10, 75),
      labels = scales::percent_format(scale = 1),
      breaks = c(15, 35, 55, 70)
    ) +
    # Set custom shapes, colors, fills and line types for groups
    scale_shape_manual(values = c("Strength-Trained" = 21, "Dexterity-Trained" = 24)) +
    scale_color_manual(values = c("Strength-Trained" = "#fe9d5d", "Dexterity-Trained" = "#456990")) +
    scale_fill_manual(values = c("Strength-Trained" = "#fe9d5d", "Dexterity-Trained" = "#456990")) +
    scale_linetype_manual(values = c("Strength-Trained" = 5, "Dexterity-Trained" = 1)) +
    # Create separate panels for each muscle
    facet_wrap(~ muscle)
}
mu_avg_firing_rate_plot <- create_emmeans_plot(emm)

mu_avg_firing_rate_plot

Coherence plotting

# Load pooled coherence data
pooled_coherence_data <- read_csv("pooled_coherence_data.csv", 
                                  col_types = cols(
                                    .default = col_double(),
                                    # Specify all chi2_sig columns as logical
                                    strength_chi2_sig_15 = col_logical(),
                                    strength_chi2_sig_35 = col_logical(),
                                    strength_chi2_sig_55 = col_logical(),
                                    strength_chi2_sig_70 = col_logical(),
                                    dexterity_chi2_sig_15 = col_logical(),
                                    dexterity_chi2_sig_35 = col_logical(),
                                    dexterity_chi2_sig_55 = col_logical(),
                                    dexterity_chi2_sig_70 = col_logical()
                                  ),
                                  show_col_types = FALSE)

# Load coherence histogram data
coherence_histogram_data <- read_csv("histogram_coherence_data.csv", show_col_types = FALSE)

# Load comparison of coherence data
comparison_of_coherence_data <- read_csv("comparison_of_coherence_data.csv", show_col_types = FALSE)

Pooled coherence plots

Show function for plotting pooled coherence plots
# FUNCTION: create_faceted_coherence_plots

# Creates NeuroSpec-style pooled coherence plots in a faceted layout

# Inputs:
# - data: pooled coherence data

# Outputs:
# - 2x4 faceted plot with strength/dexterity on rows and force levels on columns

create_faceted_coherence_plots <- function(data) {
  # Prepare data in long format for faceting
  force_levels <- c("15", "35", "55", "70")
  testing_groups <- c("strength", "dexterity")
  
  # Create empty list to store reshaped data
  plot_data_list <- list()
  
  # Reshape data for each combination
  for (group in testing_groups) {
    for (force in force_levels) {
      # Construct column names dynamically
      coh_col <- paste0(group, "_coh_", force)
      ci_col <- paste0(group, "_ci_", force)
      chi2_sig_col <- paste0(group, "_chi2_sig_", force)
      
      # Create subset with necessary columns
      temp_data <- data[, c("freq", coh_col, ci_col, chi2_sig_col)]
      names(temp_data) <- c("freq", "coherence", "ci", "chi2_sig")
      temp_data$testing_group <- group
      temp_data$force_level <- force
      
      plot_data_list <- append(plot_data_list, list(temp_data))
    }
  }
  
  # Combine all data
  long_data <- do.call(rbind, plot_data_list)
  
  # Convert to factors for proper ordering
  long_data$testing_group <- factor(long_data$testing_group, 
                                   levels = c("strength", "dexterity"))
  long_data$force_level <- factor(long_data$force_level, 
                                 levels = c("15", "35", "55", "70"))
  
  # Create significant points subset
  sig_data <- long_data[long_data$chi2_sig == TRUE, ]
  
  # Create the faceted plot
  p <- ggplot(long_data, aes(x = freq)) +
    # Add shaded frequency bands
    annotate("rect", xmin = 8, xmax = 16, ymin = 0, ymax = 0.04,
             fill = "grey90", alpha = 0.5) +
    annotate("rect", xmin = 16, xmax = 30, ymin = 0, ymax = 0.04,
             fill = "grey75", alpha = 0.5) +
    annotate("rect", xmin = 30, xmax = 60, ymin = 0, ymax = 0.04,
             fill = "grey60", alpha = 0.5) +
    # Add symbols (will appear in each facet)
    annotate("text", x = 12, y = 0.035, label = "\u03B1", size = 10/.pt) +
    annotate("text", x = 22, y = 0.035, label = "\u03B2", size = 10/.pt) +
    annotate("text", x = 45, y = 0.035, label = "\u03B3", size = 10/.pt) +
    # Add coherence line with color
    geom_line(aes(y = coherence, color = testing_group), linewidth = 0.8) +
    # Add confidence interval as dashed line
    geom_hline(aes(yintercept = ci), linetype = "dashed", linewidth = 0.4) +
    # Add red X marks for significant chi2 points
    geom_point(data = sig_data, 
               aes(y = coherence), 
               shape = 4, # X shape
               color = "red", 
               size = 2) +
    # Set axis limits and breaks
    scale_x_continuous(
      limits = c(0, 65),
      breaks = c(0, 10, 20, 30, 40, 50, 60),
      name = "Frequency (Hz)",
      expand = expansion(mult = c(0, 0.05))
    ) +
    scale_y_continuous(
      limits = c(0, 0.04), 
      breaks = c(0, 0.01, 0.02, 0.03, 0.04),
      name = "Coherence Estimate",
      expand = expansion(mult = c(0, 0.05))
    ) +
    # Add color scale
    scale_color_manual(
      values = c("strength" = "#fe9d5d", "dexterity" = "#456990"),
      labels = c("strength" = "Strength-Trained", "dexterity" = "Dexterity-Trained")
    ) +
    # Create facets: rows = testing groups, columns = force levels
    facet_grid(testing_group ~ force_level,
               labeller = labeller(
                 testing_group = c("strength" = "Strength-Trained",
                                 "dexterity" = "Dexterity-Trained"),
                 force_level = function(x) paste0(x, "% MVC")
               ),
               switch = "y") +  # Move row strip labels to the left
    # Theme modifications
    theme_bw() +
    theme(
      panel.grid.major = element_blank(),
      panel.grid.minor = element_blank(),
      panel.background = element_blank(),
      strip.background = element_rect(color = NA),
      strip.text.y = element_blank(),
      strip.text.x = element_text(face = "bold", size = 12),
      strip.placement = "none",
      panel.spacing.y = unit(0.8, "lines"),
      panel.border = element_rect(color = "grey50", fill = NA, linewidth = 0.5),
      axis.ticks = element_line(color = "grey30", linewidth = 0.3),
      
      # Change axis fonts
      axis.title.x = element_text(margin = margin(t = 11, r = 0, b = 0, l = 0),
                                  face = "bold"),
      axis.text.x = element_text(size = 9),
      axis.title.y = element_text(face = "bold"),
      axis.text.y = element_text(size = 9),
      
      # No legend since groups are faceted
      legend.position = "none",
    )
  
  return(p)
}
# Create the faceted coherence plot
faceted_coherence_plot <- create_faceted_coherence_plots(pooled_coherence_data)

Pooled histogram plots

Show function for plotting pooled coherence histogram plots
# FUNCTION: create_faceted_histogram_plots

# Creates histogram plots of percentage of trials with significant coherence

# Inputs:
# - data: coherence histogram data

# Outputs:
# - 2x4 faceted plot with strength/dexterity on rows and force levels on columns


create_faceted_histogram_plots <- function(data) {
  # Prepare data in long format for faceting
  force_levels <- c("15", "35", "55", "70")
  testing_groups <- c("strength", "dexterity")
  
  # Create empty list to store reshaped data
  plot_data_list <- list()
  
  # Reshape data for each combination
  for (group in testing_groups) {
    for (force in force_levels) {
      # Construct column name dynamically
      hist_col <- paste0(group, "_MVC", force)
      
      # Create subset with necessary columns
      temp_data <- data[, c("freq", hist_col)]
      names(temp_data) <- c("freq", "value")
      temp_data$value <- temp_data$value * 100  # Convert to percentage
      temp_data$testing_group <- group
      temp_data$force_level <- force
      
      plot_data_list <- append(plot_data_list, list(temp_data))
    }
  }
  
  # Combine all data
  long_data <- do.call(rbind, plot_data_list)
  
  # Convert to factors for proper ordering
  long_data$testing_group <- factor(long_data$testing_group, 
                                   levels = c("strength", "dexterity"))
  long_data$force_level <- factor(long_data$force_level, 
                                 levels = c("15", "35", "55", "70"))
  
  # Create the faceted plot
  p <- ggplot(long_data, aes(x = freq)) +
    # Add shaded frequency bands
    annotate("rect", xmin = 8, xmax = 16, ymin = 0, ymax = 25,
             fill = "grey90", alpha = 0.5) +
    annotate("rect", xmin = 16, xmax = 30, ymin = 0, ymax = 25,
             fill = "grey75", alpha = 0.5) +
    annotate("rect", xmin = 30, xmax = 60, ymin = 0, ymax = 25,
             fill = "grey60", alpha = 0.5) +
    # Add symbols (will appear in each facet)
    annotate("text", x = 12, y = 22, label = "\u03B1", size = 10/.pt) +
    annotate("text", x = 23.5, y = 22, label = "\u03B2", size = 10/.pt) +
    annotate("text", x = 45, y = 22, label = "\u03B3", size = 10/.pt) +
    # Add histogram bars
    geom_col(aes(y = value, fill = testing_group), width = 1.3) +
    # Set axis limits and breaks
    scale_x_continuous(
      limits = c(0, 65),
      breaks = seq(0, 60, by = 10),
      name = "Frequency (Hz)",
      expand = expansion(mult = c(0, 0.05))
    ) +
    scale_y_continuous(
      limits = c(0, 25), 
      breaks = c(0, 5, 10, 15, 20, 25),
      labels = function(x) paste0(x, "%"),
      name = "% of Trials",
      expand = expansion(mult = c(0, 0.05))
    ) +
    # Add fill scale
    scale_fill_manual(
      values = c("strength" = "#fe9d5d", "dexterity" = "#456990"),
      labels = c("strength" = "Strength-Trained", "dexterity" = "Dexterity-Trained")
    ) +
    # Create facets: rows = testing groups, columns = force levels
    facet_grid(testing_group ~ force_level,
               labeller = labeller(
                 testing_group = c("strength" = "Strength-Trained",
                                 "dexterity" = "Dexterity-Trained"),
                 force_level = function(x) paste0(x, "% MVC")
               ),
               switch = "y") +  # Move row strip labels to the left
    # Theme modifications
    theme_bw() +
    theme(
      panel.grid.major = element_blank(),
      panel.grid.minor = element_blank(),
      panel.background = element_blank(),
      strip.background = element_rect(color = NA),
      strip.text.y = element_blank(),
      strip.text.x = element_text(face = "bold", size = 12),
      strip.placement = "none",
      panel.spacing.y = unit(0.8, "lines"),
      panel.border = element_rect(color = "grey50", fill = NA, linewidth = 0.5),
      axis.ticks = element_line(color = "grey30", linewidth = 0.3),
      
      # Change axis fonts
      axis.title.x = element_text(margin = margin(t = 11, r = 0, b = 0, l = 0),
                                  face = "bold"),
      axis.text.x = element_text(size=9),
      axis.title.y = element_text(face = "bold"),
      axis.text.y = element_text(size=9),
      
      # No legend since groups are faceted
      legend.position = "none"
    )
  
  return(p)
}
# Create the faceted histogram plot
faceted_histogram_plot <- create_faceted_histogram_plots(coherence_histogram_data)
# Combine plots
combined_plots <- faceted_coherence_plot / plot_spacer() / faceted_histogram_plot +
  plot_layout(
    guides = "collect",
    axis_titles = "collect",
    heights = c(1,0.03,1)
  ) +
  plot_annotation(
    tag_levels = "A"
  ) & 
  theme(
    legend.position = "none",
    legend.margin = margin(t = 0, r = 0, b = 10, l = 0),
    legend.box.margin = margin(t = -7, r = 0, b = 0, l = 0),
    plot.tag.position = c(0.05, 1),
    plot.tag = element_text(size = 18, face = "bold")
  )

combined_plots

Comparison of coherence plots

Show function for plotting comparison of coherence plots
# FUNCTION: create_faceted_comparison_of_coherence_plots

# Creates NeuroSpec-style plots for comparison of coherence in a faceted layout

# Inputs:
# - data: "comparison_of_coherence_data.csv"

# Outputs:
# - 2x2 faceted plot with force levels

create_faceted_comparison_of_coherence_plots <- function(data) {
  # Force levels to use
  force_levels <- c("15", "35", "55", "70")
  
  # Create empty list to store reshaped data
  plot_data_list <- list()
  
  # Reshape data for each force level
  for (force in force_levels) {
    # Construct column names dynamically
    coh_col <- paste0("compcoh_", force)
    ci_col <- paste0("ci_", force)
    
    # Create subset with necessary columns
    temp_data <- data[, c("freq", coh_col, ci_col)]
    names(temp_data) <- c("freq", "coherence", "ci")
    temp_data$force_level <- force
    
    plot_data_list <- append(plot_data_list, list(temp_data))
  }
  
  # Combine all data
  long_data <- do.call(rbind, plot_data_list)
  
  # Convert to factor for proper ordering
  long_data$force_level <- factor(long_data$force_level, 
                                 levels = c("15", "35", "55", "70"))
  
  # Create the faceted plot
  p <- ggplot(long_data, aes(x = freq)) +
    # Add shaded frequency bands
    annotate("rect", xmin = 8, xmax = 16, ymin = -0.21, ymax = 0.21,
             fill = "grey90", alpha = 0.5) +
    annotate("rect", xmin = 16, xmax = 30, ymin = -0.21, ymax = 0.21,
             fill = "grey75", alpha = 0.5) +
    annotate("rect", xmin = 30, xmax = 60, ymin = -0.21, ymax = 0.21,
             fill = "grey60", alpha = 0.5) +
    # Add symbols for frequency bands
    annotate("text", x = 12, y = 0.18, label = "\u03B1", size = 12/.pt) +
    annotate("text", x = 23, y = 0.18, label = "\u03B2", size = 12/.pt) +
    annotate("text", x = 45, y = 0.18, label = "\u03B3", size = 12/.pt) +
    # Add confidence intervals as horizontal lines
    geom_hline(aes(yintercept = ci), color = "#fe9d5d", linewidth = 1) +
    geom_hline(aes(yintercept = -ci), color = "#456990", linewidth = 1) +
    # Add zero line
    geom_hline(yintercept = 0, linetype = "dashed", linewidth = 0.5) +
    # Add coherence line
    geom_line(aes(y = coherence), linewidth = 0.5) +
    # Set axis limits and breaks
    scale_x_continuous(
      limits = c(0, 65),
      breaks = seq(0, 60, by = 10),
      name = "Frequency (Hz)",
      expand = expansion(mult = c(0, 0.05))
    ) +
    scale_y_continuous(
      limits = c(-0.21, 0.21),
      name = "Difference of Coherence",
      expand = expansion(mult = c(0, 0.05))
    ) +
    # Create 2x2 facets using facet_wrap
    facet_wrap(~ force_level,
               nrow = 2,
               ncol = 2,
               labeller = labeller(
                 force_level = function(x) paste0(x, "% MVC")
               )) +
   # Theme modifications
    theme_bw() +
    theme(
      panel.grid.major = element_blank(),
      panel.grid.minor = element_blank(),
      panel.background = element_blank(),
      #strip.background = element_rect(color = NA),
      strip.text.y = element_blank(),
      strip.text.x = element_text(face = "bold", size = 12),
      strip.placement = "outside",
      panel.spacing = unit(0.8, "lines"),
      panel.border = element_rect(color = "grey70", fill = NA, linewidth = 0.5),
      axis.ticks = element_line(color = "grey30", linewidth = 0.3),
      strip.background = element_rect(color = NA, fill = "grey90"),
      
      # Change Axis Fonts
      axis.title.x = element_text(face = "bold"),
      axis.text.x = element_text(size=9),#face = "bold"),
      axis.title.y = element_text(face = "bold"),
      axis.text.y = element_text(size=9),#face = "bold"),
      
      # Position legend closer to the x-axis
      legend.position = "bottom",
      legend.margin = margin(t = 0, r = 0, b = 10, l = 0),
      legend.box.margin = margin(t = -7, r = 0, b = 0, l = 0)
    )
  
  return(p)
}
# Create the faceted plot
faceted_comparison_coh_plot <- create_faceted_comparison_of_coherence_plots(comparison_of_coherence_data)
Show function for plotting average coherence across entire band
# FUNCTION: average_band_coherence

# Collapses pooled coherence across participants in each group at each force level for each band (alpha, beta and gamma)

# Inputs:
# - data: "pooled_coherence_data.csv"

# Outputs:
# - 3x1 faceted plot with coherence bands and force levels

average_band_coherence <- function(pooled_coherence_data) {
  
  # Get column names containing coherence data
  coh_cols <- grep("coh_", names(pooled_coherence_data), value = TRUE)
  
  # Initialize an empty dataframe for the results
  result_df <- data.frame(matrix(ncol = length(coh_cols), nrow = 3))
  colnames(result_df) <- coh_cols
  rownames(result_df) <- c("alpha", "beta", "gamma")
  
  # Define row ranges for each frequency band
  alpha_rows <- 4:7
  beta_rows <- 8:13
  gamma_rows <- 14:28
  
  # Calculate averages for each column and frequency band
  for (col in coh_cols) {

    col_data <- as.numeric(as.character(pooled_coherence_data[[col]]))
    
    # Calculate average for alpha band (rows 4-7)
    result_df[1, col] <- mean(col_data[alpha_rows], na.rm = TRUE)
    
    # Calculate average for beta band (rows 8-28)
    result_df[2, col] <- mean(col_data[beta_rows], na.rm = TRUE)
    
    # Calculate average for gamma band (rows 14-28)
    result_df[3, col] <- mean(col_data[gamma_rows], na.rm = TRUE)
  }
  
  # Convert result_df to a long format for easier plotting
  # First, add a row identifier for the frequency bands
  long_df <- as.data.frame(result_df)
  long_df$band <- rownames(long_df)
  
  # Reshape the data to long format
  long_df <- pivot_longer(
    long_df,
    cols = -band,
    names_to = "condition",
    values_to = "coherence"
  )
  
  # Extract the group and force level from the condition column
  long_df <- long_df %>%
    mutate(
      group = ifelse(grepl("strength", condition), "Strength", "Dexterity"),
      force = as.numeric(sub(".*_coh_(\\d+)$", "\\1", condition))
    )
  
  # Convert band directly to a factor with capitalized labels
  long_df$band <- factor(long_df$band, 
                        levels = c("alpha", "beta", "gamma"),
                        labels = c("Alpha", "Beta", "Gamma"))
  
  # Convert group to a factor for color mapping
  long_df$group <- factor(long_df$group, levels = c("Strength", "Dexterity"))
  
  # Create your plot with adjusted spacing
  facet_plot <- ggplot(long_df, aes(x = force, y = coherence, color = group, group = group)) +
    geom_line(size = 1) +
    geom_point(size = 3) +
    scale_x_continuous(
      breaks = c(15, 35, 55, 70), 
      labels = c("15%", "35%", "55%", "70%"),
      limits = c(10, 75)
    ) +
    scale_y_continuous(limits = c(0, 0.015)) +
    scale_color_manual(values = c("Strength" = "#fe9d5d", "Dexterity" = "#456990"), 
                       name = "Testing Group") +
    facet_wrap(~ band, ncol = 3) +
    labs(x = "Percentage of MVC",
         y = "Average Coherence") +
    theme_bw() +
    theme(
      panel.grid.major = element_blank(),
      panel.grid.minor = element_blank(),
      panel.background = element_blank(),
      strip.text = element_text(face = "bold", size = 12),
      strip.placement = "outside",
      panel.spacing = unit(0.3, "lines"),
      panel.border = element_rect(color = "grey70", fill = NA, linewidth = 0.5),
      axis.ticks = element_line(color = "grey30", linewidth = 0.3),
      strip.background = element_rect(color = NA, fill = "grey90"),
      
      # Change axis fonts
      axis.title.x = element_text(margin = margin(t = 11, r = 0, b = 0, l = 0), #change spacing
                                  face = "bold"),
      axis.title.y = element_text(face = "bold"),
      axis.text.x = element_text(face = "bold"),
      legend.title = element_text(face = "bold", size = 9),
      
      # Position legend closer to the x-axis
      legend.position = "bottom",
      legend.margin = margin(t = 0, r = 0, b = 10, l = 0),
      legend.box.margin = margin(t = -7, r = 0, b = 0, l = 0)
    )
  
  return(facet_plot)
}
facet_plot <- average_band_coherence(pooled_coherence_data)
Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
ℹ Please use `linewidth` instead.
faceted_comparison_coh_plot <- faceted_comparison_coh_plot +  theme(
    legend.margin = margin(t = 0, r = 0, b = 10, l = 0),
    legend.box.margin = margin(t = -7, r = 0, b = 0, l = 0),
    plot.tag.position = c(0.05, 1),
    plot.tag = element_text(size = 18, face = "bold"))
  
 combined_comparison <- faceted_comparison_coh_plot/ plot_spacer() / facet_plot +
  plot_layout(heights = c(2, 0.05, 0.8)) +
  plot_annotation(tag_levels = "A") +
  theme(
    legend.margin = margin(t = 0, r = 0, b = 10, l = 0),
    legend.box.margin = margin(t = -7, r = 0, b = 0, l = 0),
    plot.tag.position = c(0.05, 1),
    plot.tag = element_text(size = 18, face = "bold")
  )
 
 print(combined_comparison)

Clustering analysis

rms_data_for_R <- read_csv("rms_data_for_R.csv", 
    col_types = cols(group = col_factor(levels = c("strength", 
        "dexterity")), participant = col_factor(levels = c("1", 
        "2", "3", "4", "5", "6", "7", "8", 
        "9", "10", "11", "12", "13", "14", 
        "16", "17", "18", "19", "20", "21")), 
        trial = col_factor(levels = c("1", 
            "2", "3")), forcelevel = col_factor(levels = c("15", 
            "35", "55", "70"))))

Determine whether clustering is appropriate

Show function for calculating optimal clusters
# FUNCTION: determine_optimal_clusters

# Analyzes time series data to determine the optimal number of clusters for muscle activation patterns
# using both the Elbow method (WCSS) and Silhouette analysis.

# Inputs:
# - data: Dataframe containing muscle activation data (must include columns: participant,
#         group, forcelevel, time, force, and the specified muscle_name)
# - muscle_name: Name of the muscle column to analyze ("fds", "apb")
# - group_filter: Vector of group names to include (c("strength", "dexterity"))
# - force_level: The force level to analyze ("15", "35", "55", "70") 
# - max_k: Maximum number of clusters to consider (default = 6)

# Outputs: List containing:
# - elbow_data: Dataframe with WCSS values for each k
# - silhouette_data: Dataframe with silhouette scores for each k
# - optimal_k_elbow: Optimal number of clusters according to Elbow method
# - optimal_k_silhouette: Optimal number of clusters according to Silhouette method
# - plots: List containing the elbow plot
# - muscle_series_list: Preprocessed time series for clustering
# - averaged_data: Processed data with trials averaged by participant
# - participant_groups: Group assignments for each participant

determine_optimal_clusters <- function(data, muscle_name, group_filter, force_level, max_k = 6) {
  
  # Filter data for the specified group and force level
  single_subset <- data %>%
    filter(group %in% group_filter, forcelevel == force_level)
  
  # Average trials for each participant to reduce noise and variability
  averaged_data <- single_subset %>%
    group_by(participant, time) %>%
    summarise(
      # Calculate mean for muscle activation and force across trials
      across(all_of(c(muscle_name, "force")), ~mean(., na.rm = TRUE), .names = "avg_{.col}"),
      .groups = "drop"
    )
  
  # Extract group information for each participant (for later analysis)
  participant_groups <- single_subset %>%
    group_by(participant) %>%
    summarise(group = first(group), .groups = "drop") %>%
    distinct()
  
  # Join group information to the averaged data for complete dataset
  averaged_data <- averaged_data %>%
    left_join(participant_groups, by = "participant")
  
  # Extract time series for each participant (these will be clustered)
  muscle_col <- paste0("avg_", muscle_name)
  muscle_series_list <- averaged_data %>%
    group_by(participant) %>%
    arrange(time) %>%  # Ensure time order is preserved
    summarise(series = list(!!sym(muscle_col)), .groups = "drop") %>%
    pull(series)
  
  # WCSS METHOD
  # Special case: Calculate WCSS for k=1 (not handled by clustering algorithm)
  all_values <- do.call(cbind, muscle_series_list)
  mean_series <- rowMeans(all_values, na.rm = TRUE)
  
  # For k=1, WCSS is sum of squared distances from each point to the overall mean
  wcss_k1 <- sum(apply(all_values, 2, function(x) sum((x - mean_series)^2, na.rm = TRUE)))
  
  # Define range of k values to evaluate (starting from k=2)
  k_range <- 2:max_k
  wcss_values <- numeric(length(k_range))
  
  # SILHOUTTE SCORES
  # Initialize storage for silhouette scores (+1 to include k=1)
  sil_values <- numeric(length(k_range) + 1)
  sil_values[1] <- 0  # Set k=1 silhouette to 0 (not defined for single cluster)
  
  # Create distance matrix for silhouette calculation using DTW
  dist_matrix <- proxy::dist(muscle_series_list, method = "dtw")
  dist_matrix <- as.dist(as.matrix(dist_matrix))
  
  # Loop for 1:k range clusters
  for (i in seq_along(k_range)) {
    k_val <- k_range[i]
    set.seed(123)  # Set seed for reproducibility
    
    # Perform k-shape clustering (shape-based distance, appropriate for time series)
    temp_clust <- tsclust(muscle_series_list, 
                         type = "partitional",
                         k = k_val, 
                         distance = "sbd",  # Shape-based distance
                         centroid = "shape",
                         control = partitional_control(nrep = 1))
    
    # Calculate WCSS
    centroids <- temp_clust@centroids
    clusters <- temp_clust@cluster
    wcss <- 0
    
    # Calculate sum of squared distances from each point to its assigned centroid
    for (j in 1:length(muscle_series_list)) {
      cluster_id <- clusters[j]
      centroid <- centroids[[cluster_id]]
      ts <- muscle_series_list[[j]]
      
      # Use DTW distance (appropriate for time series) and square it
      dist_sq <- proxy::dist(matrix(ts, ncol=1), matrix(centroid, ncol=1), method="dtw")^2
      wcss <- wcss + dist_sq
    }
    
    # Store WCSS for this k value
    wcss_values[i] <- wcss
    
    # Calculate silhoutte scores
    # Only applicable for k > 1 (silhouette not defined for single cluster)
    if (k_val > 1) {
      sil <- silhouette(temp_clust@cluster, dist_matrix)
      sil_values[i+1] <- mean(sil[, "sil_width"])
    }
  }
  
  # Prepare results
  # Combine k=1 with other k values for WCSS plotting
  elbow_df <- data.frame(k = c(1, k_range), wcss = c(wcss_k1, wcss_values))
  
  # Calculate percentage improvement from k to k+1 for WCSS
  # This helps identify the "elbow point" where adding more clusters gives diminishing returns
  elbow_df$improvement <- c(NA, diff(-elbow_df$wcss) / elbow_df$wcss[-length(elbow_df$wcss)])
  
  # Create silhouette dataframe (k=1 to k=max_k, with k=1 having silhouette of 0)
  silhouette_df <- data.frame(
    k = 1:max_k,
    silhouette = sil_values
  )
  
  # Plot elbow plots
  p1 <- ggplot(elbow_df, aes(x = k, y = wcss)) +
    geom_line() +
    geom_point(size = 3) +
    labs(title = paste0("Elbow Method for Optimal Cluster Count: ", toupper(muscle_name), " ", force_level, "% MVC"),
         x = "Number of Clusters (k)",
         y = "Within-Cluster Sum of Squares (WCSS)") +
    theme_minimal()
  print(p1)
  
    # ----- PRINT RESULTS -----
  cat("\nSilhouette Method Results:\n")
  for (i in 2:max_k) {
    cat(sprintf("k=%d, Silhouette = %.4f\n", i, silhouette_df$silhouette[i]))
  }

  
  # ----- RETURN RESULTS -----
  # Return comprehensive results including plots and preprocessed data
  return(list(
    elbow_data = elbow_df,                     # WCSS values for each k
    silhouette_data = silhouette_df,           # Silhouette scores for each k
    plots = list(elbow_plot = p1),             # Elbow plot for visualization
    
    # Include preprocessed data to avoid recalculation in subsequent analysis
    muscle_series_list = muscle_series_list,   # Time series for clustering
    averaged_data = averaged_data,             # Processed data with trial averaging
    participant_groups = participant_groups    # Group assignments
  ))
}
# Run for all force levels
force_levels <- c("15", "35", "55", "70")
optimizations <- list()
results <- list()

# Step 1: Run all optimizations for FDS
for (level in force_levels) {
  cat("\n\nFDS at", level, "%:\n")
  
  optimizations[[level]] <- determine_optimal_clusters(
    data = rms_data_for_R,
    muscle_name = "fds",
    group_filter = c("strength", "dexterity"),
    force_level = level
  )
}


FDS at 15 %:


Silhouette Method Results:
k=2, Silhouette = 0.3124
k=3, Silhouette = 0.2021
k=4, Silhouette = -0.0553
k=5, Silhouette = -0.1472
k=6, Silhouette = -0.3096


FDS at 35 %:


Silhouette Method Results:
k=2, Silhouette = 0.4424
k=3, Silhouette = 0.2997
k=4, Silhouette = 0.0957
k=5, Silhouette = -0.0756
k=6, Silhouette = -0.0517


FDS at 55 %:


Silhouette Method Results:
k=2, Silhouette = 0.1006
k=3, Silhouette = -0.1699
k=4, Silhouette = 0.0637
k=5, Silhouette = -0.0971
k=6, Silhouette = -0.1586


FDS at 70 %:


Silhouette Method Results:
k=2, Silhouette = 0.2064
k=3, Silhouette = 0.2229
k=4, Silhouette = 0.0968
k=5, Silhouette = -0.0698
k=6, Silhouette = -0.0534
# Step 2: Run all optimizations for APB
for (level in force_levels) {
  cat("\n\nRunning optimization for APB muscle at force level", level, "%\n")

  optimizations[[level]] <- determine_optimal_clusters(
    data = rms_data_for_R,
    muscle_name = "apb",
    group_filter = c("strength", "dexterity"),
    force_level = level
  )
}


Running optimization for APB muscle at force level 15 %


Silhouette Method Results:
k=2, Silhouette = 0.2017
k=3, Silhouette = 0.1935
k=4, Silhouette = 0.0783
k=5, Silhouette = 0.0583
k=6, Silhouette = 0.0439


Running optimization for APB muscle at force level 35 %


Silhouette Method Results:
k=2, Silhouette = 0.4821
k=3, Silhouette = -0.0563
k=4, Silhouette = -0.0354
k=5, Silhouette = -0.0686
k=6, Silhouette = -0.1184


Running optimization for APB muscle at force level 55 %


Silhouette Method Results:
k=2, Silhouette = 0.2268
k=3, Silhouette = -0.0006
k=4, Silhouette = 0.0146
k=5, Silhouette = -0.0689
k=6, Silhouette = -0.1540


Running optimization for APB muscle at force level 70 %


Silhouette Method Results:
k=2, Silhouette = 0.0589
k=3, Silhouette = 0.0210
k=4, Silhouette = -0.0058
k=5, Silhouette = -0.0370
k=6, Silhouette = -0.0764

Run clustering analysis for the FDS at 15% and 35% MVC

Show function for running clustering analysis
# FUNCTION: analyze_muscle_clusters
# Performs k-shape clustering analysis on muscle activation data
# 
# Inputs:
# - data: Dataframe containing muscle activation data (must include columns: participant,
#         group, forcelevel, time, force, and the specified muscle_name)
# - muscle_name: Name of the muscle column to analyze ("fds", "apb")
# - group_filter: Vector of group names to include (c("strength", "dexterity"))
# - force_levels: Vector of force levels to analyze (e.g., c("15", "35"))
# - k_values: Number of clusters for k-shape clustering (can be single value or named vector)
#
# Outputs: List containing clustering results for each force level

analyze_muscle_clusters <- function(data, muscle_name, group_filter, force_levels, k_values) {
  
  # Handle k_values input - if single value, apply to all force levels
  if(length(k_values) == 1) {
    k_vals <- rep(k_values, length(force_levels))
    names(k_vals) <- force_levels
  } else if(is.null(names(k_values))) {
    names(k_values) <- force_levels
    k_vals <- k_values
  } else {
    k_vals <- k_values
  }
  
  # Container for results
  results_list <- list()
  
  # Process each force level
  for(level in force_levels) {
    cat("\n=== Analyzing", muscle_name, "at force level", level, "% with k =", k_vals[level], "===\n")
    
    # Step 1: Filter data for the specified group and force level
    single_subset <- data %>%
      filter(group %in% group_filter, forcelevel == level)
    
    # Step 2: Average trials for each participant
    averaged_data <- single_subset %>%
      group_by(participant, time) %>%
      summarise(
        across(all_of(c(muscle_name, "force")), ~mean(., na.rm = TRUE), .names = "avg_{.col}"),
        .groups = "drop"
      )
    
    # Step 3: Get group information for each participant
    participant_groups <- single_subset %>%
      group_by(participant) %>%
      summarise(group = first(group), .groups = "drop") %>%
      distinct()
    
    # Join the group information to the averaged data
    averaged_data <- averaged_data %>%
      left_join(participant_groups, by = "participant")
    
    # Step 4: Extract the muscle time series for clustering
    muscle_col <- paste0("avg_", muscle_name)
    muscle_series_list <- averaged_data %>%
      group_by(participant) %>%
      arrange(time) %>%
      summarise(series = list(!!sym(muscle_col)), .groups = "drop") %>%
      pull(series)
    
    # Step 5: Perform k-shape clustering
    set.seed(123)  # For reproducibility
    kshape_result <- tsclust(muscle_series_list, 
                           type = "partitional",
                           k = k_vals[level], 
                           distance = "sbd",  # Shape-based distance
                           centroid = "shape",
                           control = partitional_control(nrep = 1))
    
    # Step 6: Get cluster assignments and create mapping
    cluster_assignments <- kshape_result@cluster
    participant_df <- data.frame(
      participant = unique(averaged_data$participant)[1:length(cluster_assignments)],
      cluster = cluster_assignments
    ) %>%
      left_join(participant_groups, by = "participant")
    
    # Step 7: Join cluster information back to averaged data
    clustered_data <- averaged_data %>%
      left_join(participant_df, by = c("participant", "group")) %>%
      mutate(forcelevel = level)
    
    # Step 8: Create centroids dataframe
    centroids <- kshape_result@centroids
    time_points <- clustered_data %>%
      arrange(time) %>%
      pull(time) %>%
      unique()
    
    centroids_df <- data.frame()
    for (i in 1:k_vals[level]) {
      centroid <- centroids[[i]]
      
      # Handle length mismatch with interpolation if needed
      if (length(centroid) != length(time_points)) {
        approx_fn <- approx(x = seq_along(centroid), 
                          y = centroid, 
                          xout = seq(1, length(centroid), length.out = length(time_points)))
        centroid_values <- approx_fn$y
      } else {
        centroid_values <- centroid
      }
      
      temp_df <- data.frame(
        time = time_points,
        value = centroid_values,
        cluster = i,
        forcelevel = level
      )
      centroids_df <- rbind(centroids_df, temp_df)
    }
    
    # Step 9: Calculate scaled centroids
    cluster_stats <- clustered_data %>%
      group_by(cluster) %>%
      summarise(
        mean_val = mean(!!sym(muscle_col), na.rm = TRUE),
        sd_val = sd(!!sym(muscle_col), na.rm = TRUE)
      )
    
    centroids_scaled <- data.frame()
    for (i in 1:k_vals[level]) {
      cluster_centroid <- centroids_df %>% filter(cluster == i)
      cluster_stat <- cluster_stats %>% filter(cluster == i)
      
      centroid_mean <- mean(cluster_centroid$value, na.rm = TRUE)
      centroid_sd <- sd(cluster_centroid$value, na.rm = TRUE)
      
      scaled_values <- cluster_centroid$value
      if (centroid_sd > 0) {
        scaled_values <- ((scaled_values - centroid_mean) / centroid_sd) * 
                        cluster_stat$sd_val + cluster_stat$mean_val
      } else {
        scaled_values <- rep(cluster_stat$mean_val, length(scaled_values))
      }
      
      temp_df <- data.frame(
        time = cluster_centroid$time,
        value = scaled_values,
        cluster = i,
        forcelevel = level
      )
      centroids_scaled <- rbind(centroids_scaled, temp_df)
    }
    
    # Step 10: Summarize results
    participant_clusters <- participant_df %>%
      group_by(cluster) %>%
      summarise(
        participants = paste(participant, collapse = ", "),
        count = n()
      )
    
    group_by_cluster <- participant_df %>%
      group_by(cluster, group) %>%
      summarise(count = n(), .groups = "drop") %>%
      pivot_wider(names_from = group, values_from = count, values_fill = 0)
    
    # Print results for this force level
    cat("Cluster assignments:", cluster_assignments, "\n")
    print(participant_clusters)
    print(group_by_cluster)
    
    # Store results
    results_list[[level]] <- list(
      kshape_result = kshape_result,
      clustered_data = clustered_data,
      centroids_original = centroids_df,
      centroids_scaled = centroids_scaled,
      participant_clusters = participant_clusters,
      group_by_cluster = group_by_cluster,
      k_value = k_vals[level]
    )
  }
  
  return(results_list)
}
# Run the clustering analysis
clustering_results <- analyze_muscle_clusters(
  data = rms_data_for_R,
  muscle_name = "fds",
  group_filter = c("strength", "dexterity"),
  force_levels = c("15", "35"),
  k_values = 2
)

=== Analyzing fds at force level 15 % with k = 2 ===
Cluster assignments: 2 2 1 1 2 1 2 1 1 2 2 2 2 2 1 2 2 1 2 1 
# A tibble: 2 × 3
  cluster participants                               count
    <int> <chr>                                      <int>
1       1 3, 4, 6, 8, 9, 16, 19, 21                      8
2       2 1, 2, 5, 7, 10, 11, 12, 13, 14, 17, 18, 20    12
# A tibble: 2 × 3
  cluster strength dexterity
    <int>    <int>     <int>
1       1        6         2
2       2        4         8

=== Analyzing fds at force level 35 % with k = 2 ===
Cluster assignments: 1 1 1 1 2 1 2 1 1 2 2 2 1 2 1 2 1 1 1 1 
# A tibble: 2 × 3
  cluster participants                                count
    <int> <chr>                                       <int>
1       1 1, 2, 3, 4, 6, 8, 9, 13, 16, 18, 19, 20, 21    13
2       2 5, 7, 10, 11, 12, 14, 17                        7
# A tibble: 2 × 3
  cluster strength dexterity
    <int>    <int>     <int>
1       1        7         6
2       2        3         4

Plotting

Show function for plotting clustering analysis for the FDS at 15% and 35% MVC
# FUNCTION: plot_muscle_clusters
# Creates publication-ready plot for FDS muscle clusters at 35% force level
# 
# Inputs:
# - analysis_results: Output from analyze_muscle_clusters()
# - muscle_name: Name of the muscle analyzed (should be "fds")
# - force_level: Force level to plot (should be "35")
#
# Outputs: ggplot object showing 2 clusters faceted by cluster (as rows)

plot_muscle_clusters <- function(analysis_results, muscle_name = "fds", force_level = "35") {
  
  # Define color palette for experimental groups
  group_colors <- c("strength" = "#fe9d5d", "dexterity" = "#456990")
  
  # Validate force level exists in analysis results
  if(!force_level %in% names(analysis_results)) {
    stop(paste("Force level", force_level, "not found in analysis results"))
  }
  
  # Extract results for specified force level
  result <- analysis_results[[force_level]]
  

  # Filter to first 10 seconds
  result$clustered_data <- result$clustered_data %>% filter(time <= 10)
  result$centroids_scaled <- result$centroids_scaled %>% filter(time <= 10)
  
  # Format cluster labels for display
  result$clustered_data$cluster <- factor(result$clustered_data$cluster, 
                                        labels = paste("Cluster", 1:result$k_value))
  result$centroids_scaled$cluster <- factor(result$centroids_scaled$cluster, 
                                           labels = paste("Cluster", 1:result$k_value))
  
  # Create custom strip title combining muscle name and force levels
  strip_title <- "FDS: 15% and 35% MVC"
  result$clustered_data$forcelevel <- factor(result$clustered_data$forcelevel, 
                                           labels = strip_title)
  result$centroids_scaled$forcelevel <- factor(result$centroids_scaled$forcelevel, 
                                             labels = strip_title)
  
  # Calculate cluster-wise average force traces
  avg_force_data <- result$clustered_data %>%
    group_by(cluster, time) %>%
    summarise(avg_force = mean(avg_force, na.rm = TRUE), .groups = "drop")
  
  # Apply consistent force level labeling
  avg_force_data$forcelevel <- factor(force_level, labels = strip_title)
  
  # Generate muscle-specific column name
  muscle_col <- paste0("avg_", muscle_name)
  
  # Determine EMG scaling parameters
  muscle_range <- range(result$clustered_data[[muscle_col]], na.rm = TRUE)
  muscle_min <- muscle_range[1]
  muscle_max <- muscle_range[2]
  
  # Normalize force data within each cluster (0-1 range)
  for(cluster_level in unique(avg_force_data$cluster)) {
    subset_rows <- avg_force_data$cluster == cluster_level
    
    # Calculate cluster-specific force range
    current_force_range <- range(avg_force_data$avg_force[subset_rows], na.rm = TRUE)
    current_force_min <- current_force_range[1]
    current_force_max <- current_force_range[2]
    
    # Apply normalization if range exists
    if(current_force_max > current_force_min) {
      avg_force_data$avg_force[subset_rows] <- 
        (avg_force_data$avg_force[subset_rows] - current_force_min) / 
        (current_force_max - current_force_min)
    }
    
    # Constrain normalized values to [0,1] range
    avg_force_data$avg_force[subset_rows] <- pmin(pmax(avg_force_data$avg_force[subset_rows], 0), 1)
  }
  
  # Scale force traces to match EMG amplitude range (75% of EMG range)
  scale_force <- function(x) {
    reduced_max <- 0.75  # Scale factor to prevent force trace overlap with EMG data
    x_reduced <- x * reduced_max
    scaled <- ((x_reduced - 0) / (1 - 0)) * (muscle_max - muscle_min) + muscle_min
    return(scaled)
  }
  
  avg_force_data$scaled_force <- scale_force(avg_force_data$avg_force)
  
  # Generate publication-ready plot
  pub_plot <- ggplot() +
    # Individual participant EMG time series (colored by experimental group)
    geom_line(data = result$clustered_data, 
            aes(x = time, y = !!sym(muscle_col), 
                group = participant, color = group), size = 0.2) +
    # Cluster centroids (bold black lines)
    geom_line(data = result$centroids_scaled,
              aes(x = time, y = value, group = 1, size = "Centroid"),
              color = "black") +
    # Average force traces (dashed grey lines)
    geom_line(data = avg_force_data,
            aes(x = time, y = scaled_force, linetype = "Force"),
            color = "grey40", size = 1.2) +
    # Configure y-axis
    scale_y_continuous(name = "Normalized EMG RMS") +
    # Set x axis ticks
    scale_x_continuous(breaks = c(0,5,10)) +
    # Create cluster-based faceting
    facet_grid(cluster ~ forcelevel, switch = "y") +
    # Apply group color scheme
    scale_color_manual(values = group_colors, 
                     labels = c("strength" = "Strength", "dexterity" = "Dexterity")) +
    # Set axis labels
    labs(x = "Time (s)", color = "Testing Group") +
    # Apply publication theme
    theme_bw() +
    theme(
      legend.position = "none",                    # Remove legend for cleaner appearance
      panel.grid = element_blank(),                # Remove grid lines
      strip.text = element_text(face = "bold"),    # Bold strip text
      strip.background = element_rect(color = NA), # Transparent strip background
      strip.text.y = element_text(face = "bold", size = 11),  # Y-axis strip formatting
      strip.text.x = element_text(face = "bold", size = 14),  # X-axis strip formatting
      strip.placement = "outside",                 # Place strips outside plot area
      axis.title.x = element_text(margin = margin(t = 10), face = "bold", size = 12),
      axis.title.y = element_text(margin = margin(r = 10), face = "bold", size = 12),
      axis.title.y.right = element_text(margin = margin(l = 10))
    ) +
    # Configure line type and size aesthetics
    scale_linetype_manual(name = "Force", values = "dashed", labels = NULL) +
    scale_size_manual(name = "Cluster Centroid", values = c(1.5, 0.6), 
                     guide = guide_legend(override.aes = list(color = "black", 
                                                              linetype = "solid")),
                     labels = NULL)
  
  return(pub_plot)
}
fds_clusters_15_35_plot <- plot_muscle_clusters(
  analysis_results = clustering_results,
  muscle_name = "fds",
  force_level = "35"
)
Show function for plotting representative trials for APB and FDS that did not need clustering
# FUNCTION: plot_muscle_no_clusters
# Creates publication-ready plot for FDS muscle data without clustering at 35% force level
# 
# Inputs:
# - data: Dataset containing participant EMG data with columns:
#         time, avg_fds, avg_apb, group, participant, forcelevel, avg_force
# - muscle_name: "apb" or "fds"
# - force_level: Force level to filter (should be "35")
#
# Outputs: ggplot object showing all data in single "No Clusters" facet

plot_muscle_no_clusters <- function(data, muscle_name = "fds", force_level = "35", strip_title = NULL) {
  
  # Define color palette for experimental groups
  group_colors <- c("strength" = "#fe9d5d", "dexterity" = "#456990")
  
  # Filter data to specified force level and first 10 seconds
  filtered_data <- data %>%
    filter(forcelevel == force_level, time <= 10)
  
  # Check if data exists for specified force level
  if(nrow(filtered_data) == 0) {
    stop(paste("No data found for force level", force_level))
  }
  
  # Generate muscle-specific column name
  muscle_col <- paste0("avg_", muscle_name)
  
  # Check if muscle column exists
  if(!muscle_col %in% names(filtered_data)) {
    stop(paste("Column", muscle_col, "not found in data"))
  }
  
  # Calculate centroids as average of all participants
  centroids <- filtered_data %>%
    group_by(time) %>%
    summarise(value = mean(!!sym(muscle_col), na.rm = TRUE), .groups = "drop")
  
  # Add cluster label for faceting
  filtered_data$cluster <- factor("No Clusters")
  centroids$cluster <- factor("No Clusters")
  
  # Create custom strip title - use provided title or default
  if(is.null(strip_title)) {
    strip_title <- "FDS: 15% and 35% MVC"
  }
  filtered_data$forcelevel <- factor(filtered_data$forcelevel, 
                                   labels = strip_title)
  centroids$forcelevel <- factor(force_level, labels = strip_title)
  
  # Calculate average force traces
  avg_force_data <- filtered_data %>%
    group_by(time) %>%
    summarise(avg_force = mean(avg_force, na.rm = TRUE), .groups = "drop")
  
  # Add cluster and force level labels
  avg_force_data$cluster <- factor("No Clusters")
  avg_force_data$forcelevel <- factor(force_level, labels = strip_title)
  
  # Determine EMG scaling parameters
  muscle_range <- range(filtered_data[[muscle_col]], na.rm = TRUE)
  muscle_min <- muscle_range[1]
  muscle_max <- muscle_range[2]
  
  # Normalize force data (0-1 range)
  force_range <- range(avg_force_data$avg_force, na.rm = TRUE)
  force_min <- force_range[1]
  force_max <- force_range[2]
  
  # Apply normalization if range exists
  if(force_max > force_min) {
    avg_force_data$avg_force <- 
      (avg_force_data$avg_force - force_min) / (force_max - force_min)
  }
  
  # Constrain normalized values to [0,1] range
  avg_force_data$avg_force <- pmin(pmax(avg_force_data$avg_force, 0), 1)
  
  # Scale force traces to match EMG amplitude range (75% of EMG range)
  scale_force <- function(x) {
    reduced_max <- 0.75  # Scale factor to prevent force trace overlap with EMG data
    x_reduced <- x * reduced_max
    scaled <- ((x_reduced - 0) / (1 - 0)) * (muscle_max - muscle_min) + muscle_min
    return(scaled)
  }
  
  avg_force_data$scaled_force <- scale_force(avg_force_data$avg_force)
  
  # Generate publication-ready plot
  pub_plot <- ggplot() +
    # Individual participant EMG time series (colored by experimental group)
    geom_line(data = filtered_data, 
            aes(x = time, y = !!sym(muscle_col), 
                group = participant, color = group), size = 0.2) +
    # Centroids (bold black lines)
    geom_line(data = centroids,
              aes(x = time, y = value, group = 1, size = "Centroid"),
              color = "black") +
    # Average force traces (dashed grey lines)
    geom_line(data = avg_force_data,
            aes(x = time, y = scaled_force, linetype = "Force"),
            color = "grey40", size = 1.2) +
    # Configure y-axis
    scale_y_continuous(name = "Normalized EMG RMS") +
    # Set x axis ticks
    scale_x_continuous(breaks = c(0,5,10)) +
    # Create cluster-based faceting
    facet_grid(cluster ~ forcelevel, switch = "y") +
    # Apply group color scheme
    scale_color_manual(values = group_colors, 
                     labels = c("strength" = "Strength", "dexterity" = "Dexterity")) +
    # Set axis labels
    labs(x = "Time (s)", color = "Testing Group") +
    # Apply publication theme
    theme_bw() +
    theme(
      legend.position = "none",                    # Remove legend for cleaner appearance
      panel.grid = element_blank(),                # Remove grid lines
      strip.text = element_text(face = "bold"),    # Bold strip text
      strip.background = element_rect(color = NA), # Transparent strip background
      strip.text.y = element_text(face = "bold", size = 11),  # Y-axis strip formatting
      strip.text.x = element_text(face = "bold", size = 14),  # X-axis strip formatting
      strip.placement = "outside",                 # Place strips outside plot area
      axis.title.x = element_text(margin = margin(t = 10), face = "bold", size = 12),
      axis.title.y = element_text(margin = margin(r = 10), face = "bold", size = 12),
      axis.title.y.right = element_text(margin = margin(l = 10))
    ) +
    # Configure line type and size aesthetics
    scale_linetype_manual(name = "Force", values = "dashed", labels = NULL) +
    scale_size_manual(name = "Cluster Centroid", values = c(1.5, 0.6), 
                     guide = guide_legend(override.aes = list(color = "black", 
                                                              linetype = "solid")),
                     labels = NULL)
  
  return(pub_plot)
}
# Filter data for the groups and force levels you want
filtered_data <- rms_data_for_R %>%
  filter(group %in% c("strength", "dexterity"), 
         forcelevel %in% c("15", "35", "55", "70"))

# Average trials for each participant at each force level and time point
averaged_data <- filtered_data %>%
  group_by(participant, forcelevel, time) %>%
  summarise(
    avg_fds = mean(fds, na.rm = TRUE),
    avg_apb = mean(apb, na.rm = TRUE), 
    avg_force = mean(force, na.rm = TRUE),
    .groups = "drop"
  )

# Add group information back
participant_groups <- filtered_data %>%
  group_by(participant) %>%
  summarise(group = first(group), .groups = "drop") %>%
  distinct()

# Join group info to averaged data
averaged_data <- averaged_data %>%
  left_join(participant_groups, by = "participant")



apb_clusters_plot <- plot_muscle_no_clusters(averaged_data, muscle_name = "apb", 
                                        force_level = "70", 
                                        strip_title = "APB: All Force Levels")

fds_clusters_55_70_plot <- plot_muscle_no_clusters(averaged_data, muscle_name = "fds", 
                                        force_level = "70",
                                        strip_title = "FDS: 55% and 70% MVC")
fds_clusters_combined_plot <- fds_clusters_15_35_plot + fds_clusters_55_70_plot + plot_layout(heights = c(2,1), axis_titles = "collect_x")

combined_cluster_plot <- apb_clusters_plot + plot_spacer() + fds_clusters_combined_plot + plot_layout(heights = c(0.8,0.05,3))

print(combined_cluster_plot)

Online Resource 1

Table of significant pooled coherence frequencies

Show function to get all coherence frequencies which cross the CL
# FUNCTION: compare_with_ci
# Creates tables of significant frequencies based on the coherence confidence limits from the Neurospec Toolbox.
# Inputs:
# - data: "pooled_coherence_data"
# - lower_freq_limit: Lower limit for frequencies of interest.
# - upper_freq_limit: Upper limit for frequencies of interest.

# Outputs:
# - Table of significant frequencies.

compare_with_ci <- function(df, lower_freq_limit = 8, upper_freq_limit = 60) {
  # Get the CI reference value from row 1 of strength_ci_15 column
  ci_reference <- df$strength_ci_15[1]
  
  # Find all coherence columns
  coh_cols <- names(df)[grepl("_(coh|cohxy)_", names(df))]
  
  # Initialize empty lists for results
  all_mvcs <- character()
  all_groups <- character()
  all_frequencies <- numeric()
  
  # Process each coherence column
  for(col_name in coh_cols) {
    # Extract testing group and force level from column name
    parts <- strsplit(col_name, "_")[[1]]
    testing_group <- parts[1]  # "strength" or "dexterity"
    force_level <- parts[3]    # "15", "35", "55", or "70"

      # Compare direct values against CL
      comparison_df <- df %>%
        filter(freq >= lower_freq_limit, freq <= upper_freq_limit, 
               .data[[col_name]] > ci_reference)
      
      # Determine group from column name
      group <- if(grepl("strength", testing_group, ignore.case = TRUE)) 
        "Strength" else "Dexterity"
      
      # Add results to vectors
      all_mvcs <- c(all_mvcs, rep(force_level, nrow(comparison_df)))
      all_groups <- c(all_groups, rep(group, nrow(comparison_df)))
      all_frequencies <- c(all_frequencies, comparison_df$freq)
    }
  
  # Create final long format dataframe
  results_df <- data.frame(
    MVC = all_mvcs,
    Group = all_groups,
    Frequencies = round(all_frequencies,2)
  )
  
  # Convert MVC to numeric and sort
  results_df$MVC <- as.numeric(results_df$MVC)
  results_df <- results_df %>%
    arrange(MVC, Group)
  
  return(results_df)
}
# Identify the significant results
significant_coherence_results <- compare_with_ci(pooled_coherence_data) %>% 
  rename("Significant Frequencies" = Frequencies)

# Create table
kable(significant_coherence_results,
      align = c('c', 'c', 'c')) %>% 
  kable_styling(c("striped", "hover", "condensed"),
                full_width = FALSE) %>%
  column_spec(1:3, width = "100px") %>%
  collapse_rows(columns = c(1,2), valign = "middle")
MVC Group Significant Frequencies
15 Dexterity 19.53
21.70
32.55
34.72
36.89
Strength 13.02
41.23
35 Dexterity 10.85
13.02
32.55
47.74
Strength 36.89
45.57
55 Dexterity 13.02
17.36
32.55
36.89
43.40
49.91
Strength 10.85
15.19
23.87
56.42
70 Dexterity 10.85
36.89
43.40
Strength 17.36
19.53
45.57
49.91

Online Resource 2

cluster_coherence <- read_csv("cluster_coherence.csv")
Show function to plot coherence analysis for participant subclusters
# FUNCTION: create_cluster_vs_pooled_ci_scaled
# Function for subcluster coherence analysis, compared with full dataset (scaled based on CI).
# Inputs:
# - cluster_data: "cluster_coherence"
# - pooled_data: "pooled_coherence_data"
# - mvc_level: 15, 35
# - test_type: str or dext
# - ci_position: scales the confidence interval (diff no. of participants)

# Outputs:
# - Plots comparing coherence of expected cluster subgroup vs entire dataset

# 
create_cluster_vs_pooled_ci_scaled <- function(cluster_data, pooled_data, mvc_level, test_type, ci_position = 0.75) {
  # Define data types
  data_types <- c("cluster1", "pooled")
  
  # Create empty list to store reshaped data
  plot_data_list <- list()
  ci_values <- numeric(length(data_types))
  
  # Reshape data
  for (i in seq_along(data_types)) {
    data_type <- data_types[i]
    
    if (data_type == "cluster1") {
      # Construct column names for cluster data
      coh_col <- paste0("clust1_", mvc_level, "_", test_type)
      ci_col <- paste0("ci_clust1_", mvc_level, "_", test_type)
      
      # Create subset with necessary columns
      temp_data <- cluster_data[, c("f", coh_col, ci_col)]
      names(temp_data) <- c("freq", "coherence", "ci")
      temp_data$data_type <- data_type
      
    } else if (data_type == "pooled") {
      # Construct column names for pooled data
      group_name <- ifelse(test_type == "str", "strength", "dexterity")
      # Extract just the number from mvc_level (e.g., "15mvc" -> "15")
      mvc_number <- gsub("mvc", "", mvc_level)
      coh_col <- paste0(group_name, "_coh_", mvc_number)
      ci_col <- paste0(group_name, "_ci_", mvc_number)
      
      # Create subset with necessary columns
      temp_data <- pooled_data[, c("freq", coh_col, ci_col)]
      names(temp_data) <- c("freq", "coherence", "ci")
      temp_data$data_type <- data_type
    }
    
    # Store CI value for scaling
    ci_values[i] <- unique(temp_data$ci)[1]  # Assuming CI is constant within each dataset
    plot_data_list <- append(plot_data_list, list(temp_data))
  }
  
  # Combine all data
  long_data <- do.call(rbind, plot_data_list)
  
  # Convert to factors for proper ordering
  long_data$data_type <- factor(long_data$data_type, 
                               levels = c("cluster1", "pooled"))
  
  # Calculate y-axis limits based on CI position
  # CI should appear at ci_position (e.g., 0.2 = 20% up the y-axis)
  y_max_cluster <- ci_values[1] / ci_position
  y_max_pooled <- ci_values[2] / ci_position
  
  # Define colors based on test type
  line_color <- switch(test_type,
                      "str" = "#fe9d5d",
                      "dext" = "#456990",
                      "#000000")  # default black if neither
  
  # Create the faceted plot
  p <- ggplot(long_data, aes(x = freq)) +
    # Add frequency band annotations scaled to each subplot (behind lines)
    annotate("rect", xmin = 8, xmax = 16, ymin = 0, ymax = y_max_cluster,
             fill = "grey90", alpha = 0.5) +
    annotate("rect", xmin = 16, xmax = 30, ymin = 0, ymax = y_max_cluster,
             fill = "grey75", alpha = 0.5) +
    annotate("rect", xmin = 30, xmax = 60, ymin = 0, ymax = y_max_cluster,
             fill = "grey60", alpha = 0.5) +
    # Add symbols for Cluster 1 (positioned relative to its scale)
    annotate("text", x = 12, y = y_max_cluster * 0.85, label = "\u03B1", size = 12/.pt) +
    annotate("text", x = 22, y = y_max_cluster * 0.85, label = "\u03B2", size = 12/.pt) +
    annotate("text", x = 45, y = y_max_cluster * 0.85, label = "\u03B3", size = 12/.pt) +
    # Add coherence line
    geom_line(aes(y = coherence), color = line_color, linewidth = 0.8) +
    # Add confidence interval as dashed line
    geom_hline(aes(yintercept = ci), linetype = "dashed", linewidth = 0.4) +
    # Set axis limits and breaks
    scale_x_continuous(
      limits = c(0, 65),
      breaks = seq(0, 60, by = 10),
      name = "Frequency (Hz)",
      expand = expansion(mult = c(0, 0.05))
    ) +
    # Create facets with free y scales
    facet_grid(data_type ~ .,
               scales = "free_y",
               switch = "y", 
               labeller = labeller(
                 data_type = c("cluster1" = "Cluster 1 only", "pooled" = "All data")
               )) +
    # Theme modifications
    theme_bw() +
    theme(
      panel.grid.major = element_blank(),
      panel.grid.minor = element_blank(),
      panel.background = element_blank(),
      strip.text.y = element_text(face = "bold", size = 10),
      strip.placement = "outside",
      
      # Change Axis Fonts
      axis.title.x = element_text(face = "bold"),
      axis.title.y = element_text(face = "bold"),
      plot.title = element_text(hjust = 0.5, face = "bold"),
      
      panel.border = element_rect(color = "grey70", fill = NA, linewidth = 0.5),
      axis.ticks = element_line(color = "grey30", linewidth = 0.3),
      strip.background = element_rect(color = NA, fill = "grey90")
    )

  
  # Define custom y scales for each data type
  y_scales <- list(
    # Cluster 1 - scaled based on its CI
    scale_y_continuous(
      limits = c(0, y_max_cluster), 
      breaks = round(seq(0, y_max_cluster, length.out = 3), 2),
      name = "Coherence Estimate", 
     # expand = expansion(mult = c(0, 0.02))
    ),
    # Pooled - scaled based on its CI
    scale_y_continuous(
      limits = c(0, y_max_pooled), 
      breaks = round(seq(0, y_max_pooled, length.out = 3), 2),
      name = "Coherence Estimate", 
     # expand = expansion(mult = c(0, 0.02))
    )
  )
  
  # Apply custom scales
  p <- p + facetted_pos_scales(y = y_scales)
  
  return(p)
}
# Create plots
plot_15mvc_dext_ci <- create_cluster_vs_pooled_ci_scaled(cluster_coherence, pooled_coherence_data, "15mvc", "dext", ci_position = 0.25)
plot_15mvc_str_ci <- create_cluster_vs_pooled_ci_scaled(cluster_coherence, pooled_coherence_data, "15mvc", "str", ci_position = 0.25)
plot_35mvc_str_ci <- create_cluster_vs_pooled_ci_scaled(cluster_coherence, pooled_coherence_data, "35mvc", "str", ci_position = 0.25)
plot_35mvc_dext_ci <- create_cluster_vs_pooled_ci_scaled(cluster_coherence, pooled_coherence_data, "35mvc", "dext", ci_position = 0.25)

# Combine plots with patchwork
# 15% MVC
combined_15_cluster_vs_pooled_ci <- plot_15mvc_str_ci + plot_spacer() + plot_15mvc_dext_ci + 
  plot_layout(widths = c(0.5,0.01,0.5)) + 
  plot_annotation(
    title = "15% MVC", 
    theme = theme(plot.title = element_text(hjust = 0.5, face = "bold"))
  )
# 35% MVC
combined_35_cluster_vs_pooled_ci <- plot_35mvc_str_ci + plot_spacer() + plot_35mvc_dext_ci + 
  plot_layout(widths = c(0.5,0.01,0.5)) + 
  plot_annotation(
    title = "35% MVC", 
    theme = theme(plot.title = element_text(hjust = 0.5, face = "bold"))
  )



# Helper function to create titles
title_plot <- function(title_text) {
  ggplot() +
    # Draw a grey rectangle
    annotate("rect", xmin = -Inf, xmax = Inf, ymin = -Inf, ymax = Inf, fill = "grey90") +
    # Add centered title text
    annotate("text", x = 0, y = 0, label = title_text, size = 5, 
             fontface = "bold", color = "black") +
    # Ensure no axis lines, ticks, or labels
    theme_void() +
    theme(
      plot.margin = margin(5,5,5,5) 
    )
}

# Create titles
title_15 <- title_plot("15% MVC")
title_35 <- title_plot("35% MVC")

# Combine all plots together
cluster_coherence_plot <- free(title_15) / combined_15_cluster_vs_pooled_ci / plot_spacer() /free(title_35) / combined_35_cluster_vs_pooled_ci +
  plot_layout(heights = c(0.07,0.5, 0.001, 0.07, 0.5))

print(cluster_coherence_plot)