OSCR

Measuring naturalistic speech comprehension in real time.

Code ↔ Paper

16 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 16 matches · 2 of them tie a paragraph to a whole file without given lines: weak matches, whose lines are not tinted
  1. [1] § Experiment 1 › Results › Characterizing the comprehension decline ↔ experiment_1/analysis/linear_vs_categorical.ipynb, lines 128 to 220 · score 0.73 · curve_fit, sigmoid curve, Sigmoidal model, Linear model, speech rate, predicted · Report a mismatch
  2. [2] § Experiment 1 › Results › Characterizing the comprehension decline ↔ experiment_1/analysis/linear_vs_categorical.ipynb, lines 222 to 268 · score 0.71 · Pearson correlation, sigmoidal model, linear model, Wilcoxon, fold, predicted · Report a mismatch
  3. [3] § Experiment 1 › Results ↔ experiment_1/supplamentary/analysis/supplamentary_analysis.Rmd, lines 659 to 733 · score 0.59 · random slopes, digit span score, noise scores, comprehension score, regression, model · Report a mismatch
  4. [4] § Experiment 3 › Results ↔ experiment_1/supplamentary/analysis/supplamentary_analysis.Rmd, lines 659 to 733 · score 0.59 · random slopes, digit span score, noise scores, comprehension score, regression, model · Report a mismatch
  5. [5] § Experiment 1 › Results ↔ experiment_1/analysis/data_analysis.Rmd, lines 1359 to 1429 · score 0.59 · random slopes, digit span score, noise scores, comprehension score, regression, model · Report a mismatch
  6. [6] § Experiment 3 › Results ↔ experiment_1/analysis/data_analysis.Rmd, lines 1359 to 1429 · score 0.59 · random slopes, digit span score, noise scores, comprehension score, regression, model · Report a mismatch
  7. [7] § Experiment 1 › Results › Quantifying summary performance ↔ experiment_1/analysis/semantic_similarity.ipynb, lines 482 to 550 · score 0.58 · GloVe Heard Segment, summary word, dictionary, embedding, cosine, semantic · Report a mismatch
  8. [8] § Experiment 1 › Results › Characterizing the comprehension decline ↔ experiment_1/analysis/linear_vs_categorical.ipynb, lines 128 to 220 · score 0.58 · sigmoidal models predicting, linear model, Folds, fit, speech rate · Report a mismatch
  9. [9] § Experiment 1 › Speech comprehension measures › Real-time speech comprehension measure ↔ experiment_1/experiment_files/lib/psychojs-2023.2.3.iife.js, the whole file · a weak match · score 0.55 · minimizing, compatibility, head, tools, max, segments · Report a mismatch
  10. [10] § Experiment 1 › Speech comprehension measures › Real-time speech comprehension measure ↔ experiment_1/experiment_files/lib/psychojs-2023.2.3.js, the whole file · a weak match · score 0.55 · minimizing, compatibility, head, tools, max, segments · Report a mismatch
  11. [11] § Experiment 1 › Speech comprehension measures › Post-hoc comprehension measures › Summary task ↔ experiment_1/analysis/semantic_similarity.ipynb, lines 368 to 444 · score 0.55 · summary word, GLoVe, Written Summary, embedding, cosine, summed · Report a mismatch
  12. [12] § Experiment 2 › Methods › Participants ↔ experiment_2/analysis/exp2_data_analysis.Rmd, lines 506 to 539 · score 0.55 · standard deviations, slider movements, outliers · Report a mismatch
  13. [13] § Experiment 2 › Methods › Participants ↔ experiment_1/analysis/data_analysis.Rmd, lines 552 to 583 · score 0.54 · standard deviations, slider movements, outliers · Report a mismatch
  14. [14] § Experiment 2 › Results › Slider scores ↔ experiment_2/analysis/exp2_data_analysis.Rmd, lines 555 to 571 · score 0.51 · pairwise comparisons, Tukey, intercepts, slider responses, fit, digit · Report a mismatch
  15. [15] § Experiment 1 › Results › Validating the slider measure ↔ experiment_1/supplamentary/analysis/supplamentary_analysis.Rmd, lines 1892 to 1953 · score 0.50 · confidence intervals, Regression coefficient, speech rate, slider · Report a mismatch
  16. [16] § Experiment 1 › Results › Validating the slider measure ↔ experiment_1/analysis/data_analysis.Rmd, lines 1556 to 1614 · score 0.50 · confidence intervals, Regression coefficient, speech rate · Report a mismatch

Paper

Loaded from Europe PMC by your browser; OSCR does not store it: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

R Markdown · 1,686 lines · 59 KB · no license · 4 matches

  1. ---
  2. title: "Measuring Naturalistic Speech Comprehension in Real Time"
  3. output: html_document
  4. ---
  5. # DATA WRANGLING
  6. ```{r setup, echo=FALSE, message=FALSE, warning=FALSE}
  7. # Load libraries
  8. library(stringi)
  9. library(lme4)
  10. library(MuMIn)
  11. library(stringr)
  12. library(fs)
  13. library(readxl)
  14. library(readr)
  15. library(simr)
  16. library(lmerTest)
  17. library(patchwork)
  18. library(RColorBrewer)
  19. library(cowplot)
  20. library(scales)
  21. library(gridExtra)
  22. library(PerformanceAnalytics)
  23. library(corrplot)
  24. library(Hmisc)
  25. library(GGally)
  26. library(visreg)
  27. library(broom.mixed)
  28. library(sjPlot)
  29. library(gridExtra)
  30. library(grid)
  31. library(rempsyc)
  32. library(flextable)
  33. library(officer)
  34. library(ggsignif)
  35. library(MuMIn)
  36. library(rstatix)
  37. library(caret)
  38. library(modelr)
  39. library(tidyr)
  40. library(purrr)
  41. library(broom)
  42. library(dplyr)
  43. library(ggplot2)
  44. library(rsample)
  45. library(corrplot)
  46. # set the default ggplot theme
  47. theme_set(theme_classic())
  48. ```
  49. ```{r}
  50. # Set path and participant ids
  51. # Change the folder path to data folder relative to the wd
  52. folder_path <- '/Users/irmakergin/Desktop/measuring_naturalistic_speech_comprehension_2024/data/raw_data'
  53. # Define the list of participant IDs
  54. participant_ids <- c("p_1","p_2","p_3","p_4","p_5", "p_6", "p_7", "p_8", "p_9","p_10",
  55. "p_11", "p_12", "p_13", "p_14","p_15","p_16","p_17","p_18","p_19","p_20",
  56. "p_21", "p_22", "p_23", "p_24","p_25","p_26","p_27","p_28","p_29","p_30")
  57. ```
  58. ```{r, echo=FALSE, message=FALSE, warning=FALSE}
  59. # Organize the data csv files
  60. # Get a list of all files in the folder
  61. files <- dir(folder_path, full.names = TRUE)
  62. # Set a path to save the organized files
  63. organized_data_path<-"/Users/irmakergin/Desktop/measuring_naturalistic_speech_comprehension_2024/data/organized_data"
  64. # Find files by participant ID
  65. for(part_id in participant_ids) {
  66. # Match the slider files that involve the participant ID
  67. slider_pattern <- paste0("slider_", part_id, ".*\\.csv$")
  68. # Match data (csv) files that start with the participant ID
  69. additional_csv_pattern <- paste0("^", part_id, "_.*\\.csv$")
  70. # Find slider files that match the pattern
  71. slider_matched_files <- files[str_detect(basename(files), slider_pattern)]
  72. # Find data CSV files that match the pattern
  73. additional_csv_matched_files <- files[str_detect(basename(files),
  74. additional_csv_pattern)]
  75. # Create a directory for the current participant on the organized data path
  76. participant_folder <- file.path(organized_data_path, part_id)
  77. if(!dir.exists(participant_folder)) {
  78. dir.create(participant_folder, recursive = TRUE, showWarnings = TRUE)
  79. }
  80. # Create a subdirectory for slider files within the participant folder
  81. slider_folder <- file.path(participant_folder, paste0("slider_", part_id))
  82. if(!dir.exists(slider_folder)) {
  83. dir.create(slider_folder, recursive = TRUE, showWarnings = TRUE)
  84. }
  85. # Move slider matched files to the slider subfolder
  86. if(length(slider_matched_files) > 0) {
  87. for(file in slider_matched_files) {
  88. file_copy(file, file.path(slider_folder, basename(file)), overwrite = TRUE)
  89. }
  90. }
  91. # Move additional CSV matched files (experiment data) to the main participant folder
  92. # and rename the participant folders
  93. if(length(additional_csv_matched_files) > 0) {
  94. # Loop to rename the file during the copy process
  95. for(file in additional_csv_matched_files) {
  96. # New filename with '_raw_data'
  97. new_name <- paste0(part_id, "_raw_data.csv")
  98. # Copy and rename the file to the new name in the participant folder
  99. file_copy(file, file.path(participant_folder, new_name), overwrite = TRUE)
  100. }
  101. }
  102. # check
  103. if(length(slider_matched_files) > 0 || length(additional_csv_matched_files) > 0) {
  104. cat("Files for participant ID '", part_id,
  105. "' have been organized successfully. Slider files in: ", slider_folder,
  106. ", other CSVs in: ", participant_folder, "\n")
  107. } else {
  108. cat("No files found for participant ID '", part_id, "'.\n")
  109. }
  110. }
  111. # Commented out because they are already there
  112. # # Put excel files (of digit in noise and digit span) in the organized data folder
  113. #
  114. #source_folder<-"/Users/irmakergin/Desktop/measuring_naturalistic_speech_comprehension_2024/data"
  115. # destination_folder<-"/Users/irmakergin/Desktop/measuring_naturalistic_speech_comprehension_2024/data/organized_data"
  116. #
  117. # List all Excel files in the source folder
  118. #excel_files <- dir_ls(path = source_folder, glob = "*.xlsx")
  119. # Copy each Excel file to the destination folder- commented out to re-run
  120. #for (file_path in excel_files) {
  121. #dest_file_path <- file.path(destination_folder)
  122. #file_copy(file_path, dest_file_path)
  123. # # Check
  124. # cat("Copied:", file_path, "to", dest_file_path, "\n")
  125. #}
  126. # Loop through each participant ID
  127. for (part_id in participant_ids) {
  128. # Define the path to the participant's raw data CSV
  129. raw_data_path <- file.path(organized_data_path,
  130. part_id,
  131. paste0(part_id, "_raw_data.csv"))
  132. # Read the CSV for each participant
  133. if (file.exists(raw_data_path)) {
  134. df.raw <- read_csv(raw_data_path) %>%
  135. filter(!wav_file %in% c("Someday_trial_condition.wav",
  136. "Someday_trial_condition-5x.wav")) %>%
  137. mutate(index = row_number()) %>%
  138. select(index, participant, wav_file, old_name, segment_text, question,
  139. C1, C2, C3, C4, C_correct, duration,
  140. A.numClicks, B.numClicks, C.numClicks, D.numClicks, slider.response,
  141. summary, `textbox.text`) %>%
  142. group_by(wav_file) %>%
  143. summarise(
  144. index = first(index),
  145. participant = first(participant),
  146. old_name = first(old_name),
  147. segment_text = first(segment_text),
  148. question = first(question),
  149. C1 = first(C1),
  150. C2 = first(C2),
  151. C3 = first(C3),
  152. C4 = first(C4),
  153. C_correct = first(C_correct),
  154. duration = first(duration),
  155. summary = first(summary),
  156. textbox.text = first(textbox.text),
  157. A.numClicks = max(A.numClicks, na.rm = TRUE),
  158. B.numClicks = max(B.numClicks, na.rm = TRUE),
  159. C.numClicks = max(C.numClicks, na.rm = TRUE),
  160. D.numClicks = max(D.numClicks, na.rm = TRUE),
  161. likert_response = max(slider.response, na.rm = TRUE),
  162. .groups = 'drop'
  163. ) %>%
  164. arrange(index) %>%
  165. mutate(index = row_number())
  166. # Fill NA values in participant_id column with part_id for participant p_3
  167. if (part_id == "p_3") {
  168. df.raw <- df.raw %>%
  169. mutate(participant = replace_na(participant, part_id))
  170. }
  171. if (part_id == "p_1") {
  172. df.raw <- df.raw %>%
  173. mutate(participant = replace_na(participant, part_id))
  174. }
  175. if (part_id == "p_2") {
  176. df.raw <- df.raw %>%
  177. mutate(participant = replace_na(participant, part_id))
  178. }
  179. if (part_id == "p_14") {
  180. df.raw <- df.raw %>%
  181. mutate(participant = replace_na(participant, part_id))
  182. }
  183. if (part_id == "p_13") {
  184. df.raw <- df.raw %>%
  185. mutate(participant = replace_na(participant, part_id))
  186. }
  187. if (part_id == "p_26") {
  188. df.raw <- df.raw %>%
  189. mutate(participant = replace_na(participant, part_id))
  190. }
  191. # Remove the extra last row from df.raw
  192. df.raw <- head(df.raw, n = -1)
  193. #Assign processed data of each participant to a new df
  194. assign(paste0("df.raw.merged_", part_id), df.raw, envir = .GlobalEnv)
  195. }
  196. }
  197. ```
  198. ```{r}
  199. # Merge all participant dfs into one df
  200. # Create an empty data frame to store merged data
  201. df.raw <- data.frame()
  202. # Loop through each participant ID and merge their df
  203. for (part_id in participant_ids) {
  204. # Construct the name of the data frame variable
  205. df_name <- paste0("df.raw.merged_", part_id)
  206. # Check if the data frame exists in the global environment
  207. if (exists(df_name, envir = .GlobalEnv)) {
  208. # Get the data frame from its name
  209. df_participant <- get(df_name, envir = .GlobalEnv)
  210. # Merge the data frame with the main merged data frame
  211. df.raw <- rbind(df.raw, df_participant)
  212. }
  213. }
  214. ```
  215. ```{r}
  216. # Add accuracy columns for multiple choice questions
  217. # Calculate the multiple choice accuracy
  218. df.raw<- df.raw %>%
  219. mutate(
  220. multiple_choice_accuracy = apply(., 1, function(row) {
  221. # Extract the first character from C_correct - it is the correct letter to click on
  222. correct_answer <- substr(row[["C_correct"]], 1, 1)
  223. # Determine which of the click columns matches the correct answer
  224. clicked_column <- ifelse(row[["A.numClicks"]] == 1, "A",
  225. ifelse(row[["B.numClicks"]] == 1, "B",
  226. ifelse(row[["C.numClicks"]] == 1, "C",
  227. ifelse(row[["D.numClicks"]] == 1, "D", NA))))
  228. # Return 1 if the clicked column matches the correct answer, 0 otherwise
  229. if (!is.na(clicked_column) && clicked_column == correct_answer) {
  230. return(1)
  231. } else {
  232. return(0)
  233. }
  234. })
  235. )
  236. # Change the column the order
  237. df.raw<- df.raw[c("index", "participant", "wav_file", "old_name",
  238. "duration","segment_text",
  239. "question", "C1", "C2", "C3", "C4", "C_correct",
  240. "A.numClicks", "B.numClicks", "C.numClicks",
  241. "D.numClicks", "multiple_choice_accuracy",
  242. "likert_response", "summary","textbox.text")]
  243. ```
  244. ```{r}
  245. # Extract the speech rate information from the trial name and create a speech_rate column
  246. df.raw<- df.raw %>%
  247. mutate(speech_rate = case_when(
  248. str_detect(wav_file, "2x") ~ 2,
  249. str_detect(wav_file, "3x") ~ 3,
  250. str_detect(wav_file, "4x") ~ 4,
  251. str_detect(wav_file, "5x") ~ 5,
  252. TRUE ~ 1 # not sped up if there are no numbers + x in the name
  253. ))
  254. ```
  255. ```{r}
  256. # Add the Digit Span (working memory) scores of each participant
  257. # Create an empty digit_span_score column
  258. df.raw$digit_span_score <- NA
  259. for (part_id in unique(df.raw$participant)) {
  260. # Construct the filename based on the participant ID
  261. filename <- paste0(organized_data_path, "/digit_span_", part_id, ".xlsx")
  262. # Check if the file exists
  263. if (file.exists(filename)) {
  264. # Correctly read the file using read_excel
  265. digit_span_data <- read_excel(filename)
  266. # The score is located in the first row of the 'score' column
  267. score <- digit_span_data$score[1]
  268. df.raw<- df.raw%>%
  269. mutate(digit_span_score = ifelse(participant == part_id, score, digit_span_score))
  270. } else {
  271. cat("File not found for participant", part_id, "\n")
  272. }
  273. }
  274. ```
  275. ```{r}
  276. # Add the hearWHO (digit-in-noise task for hearing in noise) scores of each participants
  277. hearwho_pilot_data <- read_excel(paste0(organized_data_path,
  278. "/hearwho_experiment.xlsx"))
  279. # Excel file has columns named 'participant' and 'score'
  280. # Merge the scores into df.raw based on participant ID
  281. df.raw <- merge(df.raw, hearwho_pilot_data[, c("participant", "score")],
  282. by.x = "participant", by.y = "participant", all.x = TRUE)
  283. # Rename the 'score' column to 'digit_in_noise_score'
  284. names(df.raw)[names(df.raw) == "score"] <- "digit_in_noise_score"
  285. ```
  286. ```{r}
  287. # Add slider values and time points
  288. # Check if the 'slider_values' column exists, if not, create it
  289. if (!"slider_values" %in% names(df.raw)) {
  290. df.raw$slider_values <- NA
  291. }
  292. # Check if the 'slider_time' column exists, if not, create it
  293. if (!"slider_time" %in% names(df.raw)) {
  294. df.raw$slider_time <- NA
  295. }
  296. # Define a function to extract and return slider values and time as strings
  297. get_slider_values_and_time <- function(wav_file_name, part_id) {
  298. # Extract the trial number from wav_file name
  299. matches <- regmatches(wav_file_name, regexec("Someday_([0-9]+)", wav_file_name))
  300. if (length(matches[[1]]) < 2) { # If no match or match does not have a capture group
  301. return(list(values = NA, time = NA))
  302. }
  303. trial_number <- matches[[1]][2]
  304. # Construct the path to the slider file
  305. slider_file_path <- sprintf("%s/%s/slider_%s/slider_%s_001_Someday_%s_condition*.csv",
  306. organized_data_path,
  307. part_id, part_id, part_id,
  308. trial_number)
  309. # Find slider files matching the pattern
  310. slider_files <- Sys.glob(slider_file_path)
  311. if (length(slider_files) == 0) {
  312. return(list(values = NA, time = NA)) # if no matching file found
  313. }
  314. slider_data <- read.csv(slider_files[1], skip = 1)
  315. # Prepare output for values
  316. slider_values_str <- if ("value" %in% colnames(slider_data)) {
  317. paste(slider_data$value, collapse = ",")
  318. } else {
  319. NA
  320. }
  321. # Prepare output for time
  322. slider_time_str <- if ("time" %in% colnames(slider_data)) {
  323. rounded_times <- round(slider_data$time, 2)
  324. paste(rounded_times, collapse = ",")
  325. } else {
  326. NA
  327. }
  328. return(list(values = slider_values_str, time = slider_time_str))
  329. }
  330. # Apply the function to each row of df.raw and extract results into separate columns
  331. slider_info <- mapply(get_slider_values_and_time,
  332. df.raw$wav_file,
  333. df.raw$participant,
  334. SIMPLIFY = FALSE) # Ensure output is a list
  335. # Assign values and time from the list output to respective columns
  336. df.raw$slider_values <- sapply(slider_info, '[[', "values")
  337. df.raw$slider_time <- sapply(slider_info, '[[', "time")
  338. ```
  339. ```{r}
  340. # Re-scale slider values to be between 0-1
  341. # Convert slider_values from string to numeric lists
  342. df.raw$slider_values <- strsplit(as.character(df.raw$slider_values),
  343. ",\\s*")
  344. df.raw$slider_values <- lapply(df.raw$slider_values,
  345. function(x) as.numeric(x))
  346. rescale_values <- function(values) {
  347. sapply(values, function(x) x / 255)
  348. }
  349. # Apply the rescaling function to each row's slider_values
  350. df.raw$slider_values_rescaled <- lapply(df.raw$slider_values,
  351. rescale_values)
  352. ```
  353. ```{r}
  354. # Create a column showing number of slider values
  355. df.raw <- df.raw %>%
  356. mutate(slider_values_number = lengths(slider_values_rescaled))
  357. ```
  358. ```{r}
  359. # Rescale the 10-point-scale values to be between 0-1
  360. df.raw <- df.raw %>%
  361. mutate(likert_rescaled = likert_response / 10)
  362. ```
  363. ## Calculations for the data exclusion criteria
  364. For the post-hoc comprehension measures, to ensure that we are validating the novel measure against actual comprehension, following exclusion criteria will be applied:
  365. ### 1. Absolute threshold for comprehension
  366. 75% correctness on the multiple choice questions for the slowest (easiest to comprehend) speech rate.
  367. ```{r}
  368. # Find trials that do not have a number right before .wav in the wav_file column
  369. # (slowest condition)
  370. attention_check_trials <- df.raw %>%
  371. filter(!grepl("x\\.wav$", wav_file))
  372. # Calculate the accuracy
  373. accuracy_results <- attention_check_trials %>%
  374. group_by(participant) %>%
  375. summarise(
  376. accuracy = mean(multiple_choice_accuracy == 1, na.rm = TRUE),
  377. n = n()
  378. ) %>%
  379. mutate(passed = if_else(accuracy >= 0.75, TRUE, FALSE))
  380. # Print out participants who passed or failed
  381. passed_participants <- accuracy_results %>%
  382. filter(passed) %>%
  383. pull(participant)
  384. failed_participants <- accuracy_results %>%
  385. filter(!passed) %>%
  386. select(participant, accuracy)
  387. if(length(passed_participants) > 0) {
  388. cat(paste(passed_participants, collapse = ", "),
  389. "passed the question accuracy criterion\n")
  390. }
  391. if(nrow(failed_participants) > 0) {
  392. cat(paste(failed_participants$participant, "with accuracy:", failed_participants$accuracy, collapse = ", "),
  393. "failed the question accuracy criterion\n")
  394. }
  395. ```
  396. #### Figure: failed vs accepted participants
  397. ```{r figure-size, fig.width=6, fig.height=4}
  398. # Histogram with all participants accuracies for speech rate 1
  399. p_mult <- ggplot(accuracy_results, aes(x = accuracy)) +
  400. geom_histogram(binwidth = 0.04, fill = "violetred", alpha = 0.7, color = "black") +
  401. labs(title = "All Participants' Multiple Choice Question Accuracy for Speech Rate x1",
  402. x = "Accuracy",
  403. y = "Number of Participants")
  404. # Failed vs passed
  405. p_mult +
  406. geom_histogram(data = failed_participants, aes(x = accuracy),
  407. binwidth = 0.04, fill = "pink", alpha = 0.7, color = "black") +
  408. labs(title = "",
  409. x = "Accuracy",
  410. y = "Number of Participants") +
  411. scale_x_continuous(breaks = c(0.6, 0.8, 1), labels= c('0.6', '0.8', '1')) +
  412. theme(axis.title.x = element_text(size = 14),
  413. axis.title.y = element_text(size = 14),
  414. plot.title = element_text(hjust = 0.5, size = 15, margin = margin(b = 40)), =
  415. axis.text = element_text(size = 12))
  416. ```
  417. ### 2. Ratings on the 10-point scale
  418. They should be significantly different for the slowest and fastest speech rates.
  419. ```{r}
  420. # t-test
  421. participants <- unique(df.raw$participant)
  422. # Create a vector to store results
  423. rating_criteria_results <- character(length(participants))
  424. # Loop through each participant
  425. for (i in seq_along(participants)) {
  426. part_id <- participants[i]
  427. # Subset data for the current participant for slowest and fastest speech rates
  428. subset_data <- df.raw %>%
  429. filter(participant == part_id & (speech_rate == 1 | speech_rate == 4)) %>%
  430. select(likert_response, speech_rate)
  431. # Perform a t-test comparing likert_response for speech_rate 1 vs 4
  432. test_result <- t.test(likert_response ~ speech_rate, data = subset_data)
  433. # Check if the difference is significant (p-value < 0.05)
  434. if (test_result$p.value < 0.05) {
  435. rating_criteria_results[i] <- paste(part_id, "passed the rating criterion")
  436. } else {
  437. rating_criteria_results[i] <- paste(part_id, "failed the rating criterion")
  438. }
  439. }
  440. # Print the results
  441. cat(rating_criteria_results, sep = "\n")
  442. ```
  443. ### 3. Summaries
  444. Participants shouldn't leave all summaries empty
  445. ```{r}
  446. # If all rows under textbox.tex are empty
  447. # Check if all 'textbox.text' entries are empty or NA for each participant
  448. summary_criterion <- df.raw %>%
  449. group_by(participant) %>%
  450. summarise(all_empty = all(textbox.text == "" | is.na(textbox.text))) %>%
  451. ungroup()
  452. # Print the results for participants where all textbox.text are empty
  453. summary_criterion %>%
  454. filter(all_empty) %>%
  455. pull(participant) %>%
  456. walk(~ cat(.x, "failed summary criterion\n"))
  457. # Print the results for participants where not all textbox.text are empty
  458. summary_criterion %>%
  459. filter(!all_empty) %>%
  460. pull(participant) %>%
  461. walk(~ cat(.x, "passed summary criterion\n"))
  462. ```
  463. ### 4. Slider movements
  464. For the physical slider to ensure that participants actually used it actively: Distribution of the amount of slider movements across trials and participants- If a participant's slider movements exceeds ±3.5 standard deviations from the mean, we will remove those participants' data.
  465. ```{r}
  466. # Calculate movement score with magnitude for each trial
  467. df.raw <- df.raw %>%
  468. rowwise() %>%
  469. mutate(trial_movement_score_magnitude =
  470. ifelse(slider_values[[1]][1] == 0, sum(abs(diff(unlist(slider_values)))), NA))
  471. # Aggregate these scores for each participant
  472. participant_movement_scores <- df.raw %>%
  473. group_by(participant) %>%
  474. summarise(movement_score_all = sum(trial_movement_score_magnitude, na.rm = TRUE))
  475. # Add the movement scores back to the df
  476. df.raw <- left_join(df.raw, participant_movement_scores, by = "participant")
  477. # Calculate mean and standard deviation for movement scores, identify outliers
  478. mean_movement_score <- mean(participant_movement_scores$movement_score_all, na.rm = TRUE)
  479. sd_movement_score <- sd(participant_movement_scores$movement_score_all, na.rm = TRUE)
  480. cutoff_upper <- mean_movement_score + 3.5 * sd_movement_score
  481. cutoff_lower <- mean_movement_score - 3.5 * sd_movement_score
  482. outliers <- participant_movement_scores %>%
  483. filter(movement_score_all < cutoff_lower | movement_score_all > cutoff_upper) %>%
  484. pull(participant)
  485. # Print participant IDs for those outside the ±3.5 SD range
  486. cat("Participants outside the ±3.5 SD range:", paste(outliers, collapse = ", "), "\n")
  487. ```
  488. ### Remove the participants that failed the criteria.
  489. p_12, p_16, p_2, p_20, p_23, p_26, p_27, p_6 failed the question accuracy criterion.
  490. ```{r}
  491. # Remove failed participants
  492. df.raw_success <- df.raw %>%
  493. filter(participant != "p_12")
  494. df.raw_success <- df.raw_success %>%
  495. filter(participant != "p_16")
  496. df.raw_success <- df.raw_success %>%
  497. filter(participant != "p_2")
  498. df.raw_success <- df.raw_success %>%
  499. filter(participant != "p_20")
  500. df.raw_success <- df.raw_success %>%
  501. filter(participant != "p_23")
  502. df.raw_success <- df.raw_success %>%
  503. filter(participant != "p_26")
  504. df.raw_success <- df.raw_success %>%
  505. filter(participant != "p_27")
  506. df.raw_success <- df.raw_success %>%
  507. filter(participant != "p_6")
  508. # check
  509. count(df.raw_success, participant)
  510. ```
  511. ## Save the df
  512. ```{r}
  513. # Convert list to string for slider values
  514. df.raw_success$slider_values <- sapply(df.raw_success$slider_values,
  515. function(x) paste(x, collapse = ","))
  516. output_file_path <- file.path(organized_data_path, "organized_data.csv")
  517. write.csv(df.raw_success, output_file_path, row.names = FALSE)
  518. ```
  519. # ANALYSIS
  520. Typos in the summaries participants wrote are corrected first using this code and then manually: *analysis/correct_typos.ipynb*
  521. Semantic similarity between the summaries participants wrote and segments they heard in each trial are calculated using the code in: *analysis/semantic_similarity.ipynb*
  522. data file with semantic similarity scores are in: *data/organized_data/df_semantic.csv*
  523. ```{r}
  524. # upload data after semantic similarity calculations
  525. df.data <- read.csv(file.path(organized_data_path, "df_semantic.csv"))
  526. ```
  527. ```{r}
  528. # Calculate median trial movements for each trial
  529. df.data <- df.data %>%
  530. mutate(
  531. slider_values_numeric = str_split(slider_values_rescaled, ",") %>%
  532. map(~ as.numeric(.x))
  533. )
  534. # Mean and median slider values for each trial
  535. df.data <- df.data %>%
  536. rowwise() %>%
  537. mutate(trial_mean_rescaled = mean(unlist(slider_values_numeric), na.rm = TRUE),
  538. trial_median_rescaled = median(unlist(slider_values_numeric), na.rm = TRUE)) %>%
  539. ungroup()
  540. ```
  541. ## I. Semantic Similarity
  542. ### Average word cound per second for each speech rate
  543. ```{r}
  544. # per-trial word counts and words/min
  545. df_wpm <- df.data %>%
  546. mutate(
  547. text_chr = as.character(segment_text),
  548. word_count = if_else(
  549. is.na(text_chr) | stri_trim_both(text_chr) == "",
  550. 0L,
  551. lengths(stri_extract_all_words(text_chr))
  552. ),
  553. wpm = if_else(duration > 0, 60 * word_count / duration, NA_real_) # words per minute
  554. )
  555. # summaries by speech_rate (both unweighted and duration-weighted)
  556. wpm_by_rate <- df_wpm %>%
  557. group_by(speech_rate) %>%
  558. summarise(
  559. mean_wpm_unweighted = mean(wpm, na.rm = TRUE),
  560. sd_wpm_unweighted = sd(wpm, na.rm = TRUE),
  561. mean_wpm_weighted = 60 * sum(word_count, na.rm = TRUE) / sum(duration, na.rm = TRUE),
  562. n_trials = sum(!is.na(wpm)),
  563. .groups = "drop"
  564. ) %>%
  565. mutate(across(c(mean_wpm_unweighted, sd_wpm_unweighted, mean_wpm_weighted), ~ round(., 2)))
  566. wpm_by_rate
  567. ```
  568. Which semantic similarity analysis explains comprehension better?
  569. ### Visualization
  570. First, let's visualize all 3 to see if they show the same pattern:
  571. ```{r figure-size, fig.width=9, fig.height=5}
  572. # GloVe - Written Summary
  573. average_similarity_summary <- df.data %>%
  574. group_by(speech_rate) %>%
  575. summarise(
  576. average_similarity = mean(glove_sum_max_similarity_rescaled, na.rm = TRUE),
  577. sd = sd(glove_sum_max_similarity_rescaled, na.rm = TRUE),
  578. n = n(),
  579. se = sd / sqrt(n) # standard error
  580. )
  581. # GloVe - Heard Segment
  582. average_similarity_text <- df.data %>%
  583. group_by(speech_rate) %>%
  584. summarise(
  585. average_similarity = mean(glove_text_average_max_similarity, na.rm = TRUE),
  586. sd = sd(glove_text_average_max_similarity, na.rm = TRUE),
  587. n = n(),
  588. se = sd / sqrt(n) # standard error
  589. )
  590. # BERT
  591. average_similarity_bert <- df.data %>%
  592. group_by(speech_rate) %>%
  593. summarise(
  594. average_similarity = mean(bert_cosine_similarity, na.rm = TRUE),
  595. sd = sd(bert_cosine_similarity, na.rm = TRUE),
  596. n = n(),
  597. se = sd / sqrt(n) # standard error
  598. )
  599. # Create the plots without x and y labels
  600. plot_glove_summary <- ggplot(average_similarity_summary, aes(x = factor(speech_rate),
  601. y = average_similarity,
  602. fill = as.factor(speech_rate))) +
  603. geom_bar(stat = "identity", position = "dodge", width = 0.7, fill = "green3") +
  604. scale_x_discrete(labels = function(x) paste0("x ", x)) +
  605. labs(x = NULL, y= NULL,
  606. title = "GloVe- Written Summary") +
  607. geom_text(aes(label = round(average_similarity, 2)),
  608. vjust = -1, position = position_dodge(width = 0.7), size = 3) +
  609. geom_errorbar(aes(ymin = average_similarity - se, ymax = average_similarity + se),
  610. width = 0, position = position_dodge(width = 0.7)) +
  611. theme(axis.title.x = element_text(size = 14),
  612. axis.title.y = element_text(size = 14),
  613. plot.title = element_text(size = 13, hjust = 0.5))
  614. plot_glove_text <- ggplot(average_similarity_text, aes(x = factor(speech_rate),
  615. y = average_similarity,
  616. fill = as.factor(speech_rate))) +
  617. geom_bar(stat = "identity", position = "dodge", width = 0.7, fill = "darkseagreen2") +
  618. scale_x_discrete(labels = function(x) paste0("x ", x)) +
  619. labs(x = NULL, y = NULL,
  620. title = "GloVe - Heard Segment") +
  621. geom_text(aes(label = round(average_similarity, 2)),
  622. vjust = -1, position = position_dodge(width = 0.7), size = 3) +
  623. geom_errorbar(aes(ymin = average_similarity - se, ymax = average_similarity + se),
  624. width = 0,position = position_dodge(width = 0.7)) +
  625. theme(plot.title = element_text(size = 13, hjust = 0.5))
  626. # Adjust the plot for BERT without individual axis labels
  627. plot_bert <- ggplot(average_similarity_bert, aes(x = factor(speech_rate),
  628. y = average_similarity,
  629. fill = as.factor(speech_rate))) +
  630. geom_bar(stat = "identity", position = "dodge", width = 0.7, fill = "darkseagreen3") +
  631. scale_x_discrete(labels = function(x) paste0("x ", x)) +
  632. labs(x = NULL, y = NULL,
  633. title = "BERT") +
  634. geom_text(aes(label = round(average_similarity, 2)),
  635. vjust = -1.2, position = position_dodge(width = 0.7), size = 3) +
  636. geom_errorbar(aes(ymin = average_similarity - se, ymax = average_similarity + se),
  637. width = 0, position = position_dodge(width = 0.7)) +
  638. theme(plot.title = element_text(size = 13, hjust = 0.5))
  639. #put them in a grid
  640. grid.arrange(
  641. arrangeGrob(
  642. plot_glove_summary,plot_glove_text, plot_bert,
  643. ncol = 3,
  644. left = textGrob("Average Normalized Cosine Similarity Value",
  645. rot = 90, vjust = 0.5),
  646. bottom = textGrob("Speech Rate",
  647. rot = 0, vjust = 0.5)
  648. )
  649. )
  650. ```
  651. ### Correlation
  652. Do 3 different semantic similarity measures correlate with each other?
  653. ```{r}
  654. # Subset the relevant columns
  655. semantic_columns <- df.data[, c("bert_cosine_similarity",
  656. "glove_text_average_max_similarity",
  657. "glove_sum_max_similarity_rescaled")]
  658. # Calculate the correlation matrix
  659. correlation_matrix <- cor(semantic_columns, use = "complete.obs")
  660. # Print
  661. print(correlation_matrix)
  662. ```
  663. Test if any of these correlations are statistically significant:
  664. ```{r}
  665. # Correlation test between BERT and GloVe- Heard Segment
  666. test1 <- cor.test(df.data$bert_cosine_similarity,
  667. df.data$glove_text_average_max_similarity)
  668. # Correlation test between BERT and GloVe- Written Summary
  669. test2 <- cor.test(df.data$bert_cosine_similarity,
  670. df.data$glove_sum_max_similarity_rescaled)
  671. # Correlation test between GloVe- Heard Segment & GloVe- Written Summary
  672. test3 <- cor.test(df.data$glove_text_average_max_similarity,
  673. df.data$glove_sum_max_similarity_rescaled)
  674. print(test1)
  675. print(test2)
  676. print(test3)
  677. ```
  678. ### Comparison
  679. Which semantic similarity analysis method explains the most varience in comprehension?
  680. To see which if semantic similarity scores explains variance in comprehension better than the others,we conducted a linear mixed model was conducted to predict the 10-Point Scale responses based on BERT, GloVe- Written Summary, and GloVe- Heard Segment. Random effects for digit span score and digit in noise score were included in the model.
  681. ```{r}
  682. model_all<- lmer(likert_rescaled ~
  683. bert_cosine_similarity + glove_text_average_max_similarity +
  684. glove_sum_max_similarity_rescaled +
  685. (1 | digit_span_score) +
  686. (1 | digit_in_noise_score), data = df.data)
  687. #model_compare <- anova(model_g1, model_all)
  688. summary(model_all)
  689. ```
  690. All fixed effects (different sem similarity scores) are significant, they are finding independent variance, interestingly. Glove- Written Summary has the highest t value and estimate. Therefore, I will use/ report the GLoVe- written summary Segment for the rest of the analyses.
  691. ### Speech Rate
  692. Are semantic similarity scores btw speech rates sig different from each other?
  693. #### Glove- Written Summary
  694. ```{r}
  695. semantic.anova <- aov(glove_sum_max_similarity_rescaled ~ speech_rate, data = df.data)
  696. summary(semantic.anova)
  697. pairwise_results <- df.data %>% pairwise_t_test(
  698. glove_sum_max_similarity_rescaled ~ speech_rate, pool.sd = FALSE,
  699. p.adjust.method = "bonferroni"
  700. )
  701. pairwise_results
  702. ```
  703. All significant except x1-x2
  704. #### Glove- Heard Segment
  705. ```{r}
  706. semantic.anova2 <- aov(glove_text_average_max_similarity ~ speech_rate, data = df.data)
  707. summary(semantic.anova2)
  708. pairwise_results2 <- df.data %>% pairwise_t_test(
  709. glove_text_average_max_similarity ~ speech_rate, pool.sd = FALSE,
  710. p.adjust.method = "bonferroni"
  711. )
  712. pairwise_results2
  713. ```
  714. All significant except x1-x2 & x2-x3
  715. #### BERT
  716. ```{r}
  717. semantic.anova3 <- aov(bert_cosine_similarity ~ speech_rate, data = df.data)
  718. summary(semantic.anova3)
  719. pairwise_results3 <- df.data %>% pairwise_t_test(
  720. bert_cosine_similarity ~ speech_rate, pool.sd = FALSE,
  721. p.adjust.method = "bonferroni"
  722. )
  723. pairwise_results3
  724. ```
  725. All significant except except x2-x3
  726. ### Recency Effect
  727. Is there a recency effect in summaries that participants wrote?
  728. #### Glove- Written Summary
  729. ```{r}
  730. # First, make sure that the right ones are NA, right now NAs are also 0
  731. df.data <- df.data %>%
  732. mutate(
  733. rescaled_glove_bin1 = ifelse(textbox.text_corrected_manual == "",
  734. NA, rescaled_glove_bin1),
  735. rescaled_glove_bin2 = ifelse(textbox.text_corrected_manual == "",
  736. NA, rescaled_glove_bin2),
  737. rescaled_glove_bin3 = ifelse(textbox.text_corrected_manual == "",
  738. NA, rescaled_glove_bin3),
  739. rescaled_glove_bin4 = ifelse(textbox.text_corrected_manual == "",
  740. NA, rescaled_glove_bin4),
  741. rescaled_glove_bin5 = ifelse(textbox.text_corrected_manual == "",
  742. NA, rescaled_glove_bin5)
  743. )
  744. # Create a long dataset for bins and scores
  745. df.bin <- df.data %>%
  746. select(participant, index, speech_rate, segment_text, textbox.text_corrected_manual,
  747. glove_bin1, glove_bin2, glove_bin3, glove_bin4, glove_bin5) %>%
  748. pivot_longer(cols = starts_with("glove_bin"),
  749. names_to = "bin",
  750. names_prefix = "glove_bin",
  751. values_to = "semantic_score") %>%
  752. mutate(bin = as.integer(bin)) %>%
  753. arrange(participant, index, bin)
  754. # If a recency effect -> latter bins should be recalled better than earlier ones
  755. binned.anova <- aov(semantic_score ~ speech_rate * bin, data = df.bin)
  756. summary(binned.anova)
  757. df.bin_factor = df.bin %>%
  758. mutate(speech_bin = factor(paste(speech_rate, bin, sep="_")))
  759. # pairwise comparison
  760. df.bin_factor %>% pairwise_t_test(
  761. semantic_score ~ speech_bin, pool.sd = FALSE,
  762. p.adjust.method = "bonferroni"
  763. )
  764. ```
  765. #### Glove- Heard Segment
  766. ```{r}
  767. # First, make sure that the right ones are NA, right now NAs are also 0
  768. df.data <- df.data %>%
  769. mutate(
  770. glove2_bin1 = ifelse(textbox.text_corrected_manual == "", NA, glove2_bin1),
  771. glove2_bin2 = ifelse(textbox.text_corrected_manual == "", NA, glove2_bin2),
  772. glove2_bin3 = ifelse(textbox.text_corrected_manual == "", NA, glove2_bin3),
  773. glove2_bin4 = ifelse(textbox.text_corrected_manual == "", NA, glove2_bin4),
  774. glove2_bin5 = ifelse(textbox.text_corrected_manual == "", NA, glove2_bin5)
  775. )
  776. # Create a long dataset for bins and scores
  777. df.bin_glove2 <- df.data %>%
  778. select(participant, index, speech_rate, segment_text, textbox.text_corrected_manual,
  779. glove2_bin1, glove2_bin2, glove2_bin3, glove2_bin4, glove2_bin5) %>%
  780. pivot_longer(cols = starts_with("glove2_bin"),
  781. names_to = "bin",
  782. names_prefix = "glove2_bin",
  783. values_to = "glove2_score") %>%
  784. mutate(bin = as.integer(bin)) %>%
  785. arrange(participant, index, bin)
  786. # If a recency effect -> latter bins should be recalled better than earlier ones
  787. binned.anova2 <- aov(glove2_score ~ speech_rate * bin, data = df.bin_glove2)
  788. summary(binned.anova2)
  789. df.bin_factor = df.bin_glove2 %>%
  790. mutate(speech_bin = factor(paste(speech_rate, bin, sep="_")))
  791. # pairwise comparison
  792. df.bin_factor %>% pairwise_t_test(
  793. glove2_score ~ speech_bin, pool.sd = FALSE,
  794. p.adjust.method = "bonferroni"
  795. )
  796. ```
  797. #### BERT
  798. ```{r}
  799. # Create a long dataset for bins and scores for BERT
  800. df.bin_bert <- df.data %>%
  801. select(participant, index, speech_rate, segment_text, textbox.text_corrected_manual,
  802. cosine_similarity_bin1,
  803. cosine_similarity_bin2,
  804. cosine_similarity_bin3,
  805. cosine_similarity_bin4,
  806. cosine_similarity_bin5) %>%
  807. pivot_longer(cols = starts_with("cosine_similarity_bin"),
  808. names_to = "bin",
  809. names_prefix = "cosine_similarity_bin",
  810. values_to = "bert_score") %>%
  811. mutate(bin = as.integer(bin)) %>%
  812. arrange(participant, index, bin)
  813. binned.anova_bert <- aov(bert_score ~ speech_rate * bin, data = df.bin_bert)
  814. summary(binned.anova_bert)
  815. df.bin_bert_factor = df.bin_bert %>%
  816. mutate(speech_bin = factor(paste(speech_rate, bin, sep="_")))
  817. # pairwise comparison
  818. df.bin_bert_factor %>% pairwise_t_test(
  819. bert_score ~ speech_bin, pool.sd = FALSE,
  820. p.adjust.method = "bonferroni"
  821. )
  822. ```
  823. There is a main effect of Speech Rate for all 3, main effect of binning (recency) only for GloVe- Heard Summary & BERT.
  824. ## II. Validation
  825. Can we predict the scores of our novel comprehension measure from the existing post-hoc comprehension measures?
  826. ### Correlation between comprehension measures
  827. ```{r}
  828. vars <- c("trial_median_rescaled",
  829. "glove_sum_max_similarity_rescaled",
  830. "likert_rescaled",
  831. "multiple_choice_accuracy")
  832. # <-- rename map to match your visreg wording
  833. labs <- c(
  834. trial_median_rescaled = "Real-Time Comprehension",
  835. glove_sum_max_similarity_rescaled = "Semantic Similarity",
  836. likert_rescaled = "10-Point Scale",
  837. multiple_choice_accuracy = "Multiple Choice"
  838. )
  839. X <- df.data[, vars]
  840. rc_p <- rcorr(as.matrix(X), type = "pearson")
  841. # apply pretty labels to matrices
  842. R <- rc_p$r; P <- rc_p$P
  843. dimnames(R) <- lapply(dimnames(R), function(nm) unname(labs[nm]))
  844. dimnames(P) <- lapply(dimnames(P), function(nm) unname(labs[nm]))
  845. # tidy pairwise table with pretty names
  846. pairs_tbl <- (\(R, P) {
  847. nm <- colnames(R)
  848. out <- do.call(rbind, lapply(1:(ncol(R)-1), function(i)
  849. do.call(rbind, lapply((i+1):ncol(R), function(j)
  850. data.frame(var1 = nm[i], var2 = nm[j], r = R[i,j], p = P[i,j])
  851. ))
  852. ))
  853. out[order(abs(out$r), decreasing = TRUE), ]
  854. })(R, P) %>%
  855. mutate(r = round(r, 2),
  856. p = signif(p,4))
  857. pairs_tbl
  858. ```
  859. ```{r}
  860. # palette: purple (-1) → white (0) → green (+1)
  861. pal_pg <- colorRampPalette(c("#6A00A8", "#F7F7F7", "#10B981"))(200)
  862. corrplot(R,
  863. method = "circle",
  864. type = "upper",
  865. col = pal_pg,
  866. tl.col = "black",
  867. tl.srt = 45,
  868. cl.lim = c(-1, 1),
  869. p.mat = P,
  870. sig.level = c(0.001, 0.01, 0.05),
  871. insig = "label_sig", # prints ***, **, *
  872. pch.cex = 1.2,
  873. pch.col = "black")
  874. ```
  875. ### Each measure as a fixed effect in seperate models
  876. #### 10-Point Scale Ratings
  877. ```{r}
  878. model_likert<- lmer(trial_median_rescaled ~
  879. likert_rescaled +
  880. (1 | digit_span_score) +
  881. (1 | digit_in_noise_score), data = df.data)
  882. summary(model_likert)
  883. ```
  884. ##### Visualize 10-Point Scale Ratings per Sepeech Speed
  885. ```{r figure-size, fig.width=6, fig.height=6 }
  886. # Calculate the average 10-point scale scores per speech_rate category
  887. average_score_likert <- df.data %>%
  888. group_by(speech_rate) %>%
  889. summarise(average_likert = mean(likert_rescaled, na.rm = TRUE),
  890. sd = sd(likert_rescaled, na.rm = TRUE),
  891. n = n(),
  892. se = sd / sqrt(n) # standard error
  893. )
  894. # Plot
  895. likert_plot <- ggplot(average_score_likert, aes(x = factor(speech_rate),
  896. y = average_likert)) +
  897. geom_bar(stat = "identity", width = 0.7, fill = "royalblue") +
  898. labs(x = "Speech Rate", y = "10-Point Scale Response") +
  899. geom_text(aes(label = round(average_likert, 2)), vjust = -0.2, size = 6) +
  900. scale_x_discrete(labels = function(x) paste0("x ", x)) +
  901. geom_errorbar(aes(ymin = average_likert - se, ymax = average_likert + se),
  902. width = 0, position = position_dodge(width = 0.5)) +
  903. scale_y_continuous(breaks = c(0, 0.25, 0.50)) +
  904. theme(axis.title.x = element_text(size = 24),
  905. axis.title.y = element_text(size = 24),
  906. axis.text.x = element_text(size = 20),
  907. axis.text.y = element_text(size = 20))
  908. print(likert_plot)
  909. ```
  910. ##### variance explained by the fixed effect (partial r2)
  911. The proportion of variance explain uniquely by the predictor relative to total
  912. variance
  913. ```{r}
  914. res_likert <- partR2(model_likert,
  915. partvars = "likert_rescaled",
  916. data = df.data,
  917. R2_type = "marginal")
  918. res_likert
  919. res_likert_round <- as.data.frame(res_likert$R2) %>%
  920. mutate(across(where(is.numeric), ~ round(., 2)))
  921. res_likert_round
  922. ```
  923. Cohen's f2
  924. ```{r}
  925. r2 <- r.squaredGLMM(model_likert)
  926. R2_m <- as.numeric(r2[1])
  927. # Cohen's f^2 for a single fixed effect
  928. f2 <- if (is.finite(R2_m) && R2_m < 1) R2_m / (1 - R2_m) else NA_real_
  929. # Partial R^2 equals Marginal R^2 when there's only one fixed effect
  930. PR2_likert <- R2_m
  931. # Pretty print (2 decimals)
  932. cat(sprintf("Marginal R^2 (fixed effects): %.2f\n", R2_m))
  933. cat(sprintf("Cohen's f^2: %.2f\n", f2))
  934. cat(sprintf("Partial R^2 for likert_rescaled: %.2f\n", PR2_likert))
  935. ```
  936. #### Multiple Choice Quesitons
  937. ```{r}
  938. model_multiple_choice<- lmer(trial_median_rescaled ~
  939. multiple_choice_accuracy +
  940. (1 | digit_span_score) +
  941. (1 | digit_in_noise_score), data = df.data)
  942. summary(model_multiple_choice)
  943. ```
  944. ##### Visualize Multiple Choice Quesitons per Speech Rate
  945. ```{r figure-size, fig.width=6, fig.height=6 }
  946. # Calculate the average multiple_choice_accuracy scores per speech_rate category
  947. average_scores <- df.data %>%
  948. group_by(speech_rate) %>%
  949. summarise(average_accuracy = mean(multiple_choice_accuracy, na.rm = TRUE),
  950. sd = sd(multiple_choice_accuracy, na.rm = TRUE),
  951. n = n(),
  952. se = sd / sqrt(n) # standard error
  953. )
  954. question_plot <- ggplot(average_scores, aes(x = factor(speech_rate), y = average_accuracy)) +
  955. geom_bar(stat = "identity", width = 0.7, fill = "violetred") +
  956. labs(x = "Speech Rate", y = "Multiple Choice Accuracy") +
  957. geom_text(aes(label = round(average_accuracy, 2)), vjust = -0.5, size = 6) +
  958. scale_x_discrete(labels = function(x) paste0("x ", x)) +
  959. geom_errorbar(aes(ymin = average_accuracy - se, ymax = average_accuracy + se),
  960. width = 0, position = position_dodge(width = 0.5)) +
  961. # Add dashed line at chance level:
  962. geom_hline(yintercept = 0.25, linetype = "dashed", color = "black", size=1.5) +
  963. scale_y_continuous(breaks = c(0, 0.25, 0.5)) +
  964. theme(axis.title.x = element_text(size = 24),
  965. axis.title.y = element_text(size = 24),
  966. axis.text.x = element_text(size = 20),
  967. axis.text.y = element_text(size = 20))
  968. print(question_plot)
  969. ```
  970. ##### variance explained by the fixed effect (partial r2)
  971. The proportion of variance explain uniquely by the predictor relative to total
  972. variance
  973. ```{r}
  974. res_mcq <- partR2(model_multiple_choice,
  975. partvars = "multiple_choice_accuracy",
  976. data = df.data,
  977. R2_type = "marginal")
  978. res_mcq
  979. res_mcq_round <- as.data.frame(res_mcq$R2) %>%
  980. mutate(across(where(is.numeric), ~ round(., 2)))
  981. res_mcq_round
  982. ```
  983. Cohen's f2
  984. ```{r}
  985. ## 1) Marginal R^2 (fixed effects) + Cohen's f^2
  986. r2 <- r.squaredGLMM(model_multiple_choice)
  987. R2_m <- as.numeric(r2[1]) # marginal R^2
  988. f2 <- R2_m / (1 - R2_m) # Cohen's f^2
  989. ## 2) Partial R^2 for the single fixed effect (equals marginal R^2)
  990. PR2_mcq <- R2_m
  991. ## 3) Standardized beta (ΔSD_y per 1 SD_x)
  992. df.data$y_z <- scale(df.data$trial_median_rescaled)
  993. df.data$x_z <- scale(df.data$multiple_choice_accuracy)
  994. mod_std <- lmer(y_z ~ x_z +
  995. (1 | digit_span_score) +
  996. (1 | digit_in_noise_score),
  997. data = df.data)
  998. beta_std <- fixef(mod_std)["x_z"]
  999. ## 4) (Optional) Semi-standardized beta: SD(y) per 1 raw-unit of x
  1000. mod_semistd <- lmer(y_z ~ multiple_choice_accuracy +
  1001. (1 | digit_span_score) +
  1002. (1 | digit_in_noise_score),
  1003. data = df.data)
  1004. beta_semistd <- fixef(mod_semistd)["multiple_choice_accuracy"]
  1005. ## Pretty print
  1006. cat(sprintf(
  1007. "Marginal R^2: %.2f\nCohen's f^2: %.2f\nPartial R^2: %.2f",
  1008. R2_m, f2, PR2_mcq
  1009. ))
  1010. ```
  1011. #### Summary
  1012. ```{r}
  1013. # Summary- GloVe- Written Summary
  1014. model_glove1<- lmer(trial_median_rescaled ~
  1015. glove_sum_max_similarity_rescaled +
  1016. (1 | digit_span_score) +
  1017. (1 | digit_in_noise_score), data = df.data)
  1018. summary(model_glove1)
  1019. ```
  1020. ##### Visualize Semantic Simlarity Scores of Summaries per Speech Rate
  1021. ```{r figure-size, fig.width=6, fig.height=6}
  1022. plot_glove_summary <- ggplot(average_similarity_summary, aes(x = factor(speech_rate),
  1023. y = average_similarity,
  1024. fill = as.factor(speech_rate))) +
  1025. geom_bar(stat = "identity", position = "dodge", width = 0.7, fill = "green3") +
  1026. scale_x_discrete(labels = function(x) paste0("x ", x)) +
  1027. labs(x = "Speech Rate", y= "Semantic Similarity\nScore") +
  1028. geom_text(aes(label = round(average_similarity, 2)),
  1029. vjust = -0.5, position = position_dodge(width = 0.7), size = 6) +
  1030. geom_errorbar(aes(ymin = average_similarity - se, ymax = average_similarity + se),
  1031. width = 0, position = position_dodge(width = 0.7)) +
  1032. scale_y_continuous(breaks = c(0, 0.25, 0.5), limits = c(0, 0.5)) +
  1033. theme(axis.title.x = element_text(size = 24),
  1034. axis.title.y = element_text(size = 24),
  1035. axis.text.x = element_text(size = 20),
  1036. axis.text.y = element_text(size = 20),
  1037. plot.title = element_text(size = 20, hjust = 0.5))
  1038. plot_glove_summary
  1039. ```
  1040. ##### variance explained by the fixed effect (partial r2)
  1041. The proportion of variance explain uniquely by the predictor relative to total
  1042. variance
  1043. ```{r}
  1044. res_sem <- partR2(model_glove1,
  1045. partvars = "glove_sum_max_similarity_rescaled",
  1046. data = df.data,
  1047. R2_type = "marginal")
  1048. res_sem
  1049. res_sem_round <- as.data.frame(res_sem$R2) %>%
  1050. mutate(across(where(is.numeric), ~ round(., 2)))
  1051. res_sem_round
  1052. ```
  1053. Cohen's f2
  1054. ```{r}
  1055. ## 1) Marginal R^2 (fixed effects) + Cohen's f^2
  1056. r2 <- r.squaredGLMM(model_glove1)
  1057. R2_m <- as.numeric(r2[1]) # marginal R^2
  1058. f2 <- R2_m / (1 - R2_m) # Cohen's f^2
  1059. ## 2) Partial R^2 (same as marginal R^2 with one fixed effect)
  1060. PR2_glove <- R2_m
  1061. ## 3) Standardized beta (ΔSD_y per 1 SD_x)
  1062. df.data$y_glove_z <- scale(df.data$trial_median_rescaled)
  1063. df.data$x_glove_z <- scale(df.data$glove_sum_max_similarity_rescaled)
  1064. mod_std_glove <- lmer(y_glove_z ~ x_glove_z +
  1065. (1 | digit_span_score) +
  1066. (1 | digit_in_noise_score),
  1067. data = df.data)
  1068. ## print
  1069. cat(sprintf(
  1070. "Marginal R^2: %.3f\nCohen's f^2: %.3f\nPartial R^2: %.3f",
  1071. R2_m, f2, PR2_glove
  1072. ))
  1073. ```
  1074. ### Visualize single fixed effect regression models
  1075. ```{r figure-size, fig.width=6, fig.height=6 }
  1076. visreg_summary <- visreg(model_glove1, "glove_sum_max_similarity_rescaled", gg=TRUE,
  1077. line=list(col="green3", size=1.7), points=list(alpha=0.5),
  1078. xlab= "Semantic Similarity Score",
  1079. ylab= "Predicted Real-Time\nComprehension Score") +
  1080. theme(axis.title.x = element_text(size = 24),
  1081. axis.title.y = element_text(size = 24),
  1082. axis.text.x = element_text(size = 20), # Adjust X-axis tick label size
  1083. axis.text.y = element_text(size = 20)) + # Adjust Y-axis tick label size
  1084. scale_x_continuous(breaks = c(0, 0.5, 1)) + # Set x-axis ticks at 0, 0.5, 1
  1085. scale_y_continuous(breaks = c(0, 0.5, 1))
  1086. visreg_likert <- visreg(model_likert, "likert_rescaled", gg=TRUE,
  1087. line=list(col="royalblue",size=1.7), points=list(alpha=0.5),
  1088. xlab= "10-point Scale Response",
  1089. ylab= "") +
  1090. theme(axis.title.x = element_text(size = 24),
  1091. axis.title.y = element_text(size = 24),
  1092. axis.text.x = element_text(size = 20), # Adjust X-axis tick label size
  1093. axis.text.y = element_text(size = 20)) + # Adjust Y-axis tick label size
  1094. scale_x_continuous(breaks = c(0, 0.5, 1)) + # Force x-axis to include 0, 0.5, 1
  1095. scale_y_continuous(breaks = c(0, 0.5, 1))
  1096. visreg_multiple <- visreg(model_multiple_choice, "multiple_choice_accuracy", gg=TRUE,
  1097. line=list(col="violetred", size=1.7), points=list(alpha=0.5),
  1098. xlab= "Multiple Choice Accuracy",
  1099. ylab= "") +
  1100. theme(axis.title.x = element_text(size = 24),
  1101. axis.title.y = element_text(size = 24),
  1102. axis.text.x = element_text(size = 20), # Adjust X-axis tick label size
  1103. axis.text.y = element_text(size = 20)) + # Adjust Y-axis tick label size
  1104. scale_x_continuous(breaks = c(0, 0.5, 1)) + # Set x-axis ticks at 0, 0.5, 1
  1105. scale_y_continuous(breaks = c(0, 0.5, 1))
  1106. visreg_summary
  1107. visreg_likert
  1108. visreg_multiple
  1109. ```
  1110. ### Visualize regression models + Bar plots
  1111. ```{r figure-size, fig.width=18, fig.height=14}
  1112. # Create a multi-panel plot for
  1113. # Create title groups
  1114. title1 <- textGrob("Summary", gp=gpar(fontsize=28, color='green3',fontface="bold"))
  1115. title2 <- textGrob("10-Point Scale", gp=gpar(fontsize=28, color='royalblue', fontface="bold"))
  1116. title3 <- textGrob("Multiple Choice Question", gp=gpar(fontsize=28,color='violetred', fontface="bold"))
  1117. combined_plot_effects <- grid.arrange(
  1118. title1, title2, title3,
  1119. visreg_summary, visreg_likert, visreg_multiple,
  1120. plot_glove_summary, likert_plot, question_plot,
  1121. ncol = 3,
  1122. nrow = 4,
  1123. heights = unit.c(unit(4, "null"), unit(8, "null"), unit(2, "null"), unit(8, "null")), # Adjust heights
  1124. layout_matrix = rbind(c(1, 2, 3), c(4, 5, 6), c(NA, NA, NA), c(7, 8, 9))
  1125. )
  1126. print(combined_plot_effects)
  1127. ```
  1128. ## All post-hoc measures as fixed effects in one regression model
  1129. Now predict the novel measure's scores from all post-hoc comprehension measures to see if they can explain independent variance.
  1130. ```{r}
  1131. model_all_glove1 <- lmer(trial_median_rescaled ~
  1132. glove_sum_max_similarity_rescaled +
  1133. likert_rescaled +
  1134. multiple_choice_accuracy +
  1135. (1 | digit_span_score) +
  1136. (1 | digit_in_noise_score), data = df.data)
  1137. summary(model_all_glove1)
  1138. ```
  1139. ### Visualize the multiple fixed effects regression
  1140. ```{r figure-size, fig.width=15, fig.height=5 }
  1141. visreg_summary_all <- visreg(model_all_glove1, "glove_sum_max_similarity_rescaled",
  1142. gg=TRUE,
  1143. line=list(col="green3", size = 1.7), points=list(alpha=0.5),
  1144. xlab= "Semantic Similarity Score",
  1145. ylab= "Predicted Real-Time\nComprehension Score") +
  1146. theme(axis.title.x = element_text(size = 24),
  1147. axis.title.y = element_text(size = 24),
  1148. axis.text.x = element_text(size = 20),
  1149. axis.text.y = element_text(size = 20)) +
  1150. scale_x_continuous(breaks = c(0, 0.5, 1)) +
  1151. scale_y_continuous(breaks = c(0, 0.5, 1))
  1152. # Update for visreg_likert
  1153. visreg_likert_all <- visreg(model_all_glove1, "likert_rescaled", gg=TRUE,
  1154. line=list(col="royalblue",size=1.7), points=list(alpha=0.5),
  1155. xlab= "10-point Scale Response",
  1156. ylab= NULL)+
  1157. theme(axis.title.x = element_text(size = 24),
  1158. axis.title.y = element_text(size = 24),
  1159. axis.text.x = element_text(size = 20),
  1160. axis.text.y = element_text(size = 20)) +
  1161. scale_x_continuous(breaks = c(0, 0.5, 1)) +
  1162. scale_y_continuous(breaks = c(0, 0.5, 1))
  1163. # Update for visreg_multiple
  1164. visreg_multiple_all <- visreg(model_all_glove1, "multiple_choice_accuracy", gg=TRUE,
  1165. line=list(col="violetred", size=1.7), points=list(alpha=0.5),
  1166. xlab= "Multiple Choice Accuracy",
  1167. ylab= NULL) +
  1168. theme(axis.title.x = element_text(size = 24),
  1169. axis.title.y = element_text(size = 24),
  1170. axis.text.x = element_text(size = 20),
  1171. axis.text.y = element_text(size = 20)) +
  1172. scale_x_continuous(breaks = c(0, 0.5, 1)) +
  1173. scale_y_continuous(breaks = c(0, 0.5, 1))
  1174. # Arrange plots in a grid
  1175. grid.arrange(
  1176. arrangeGrob(
  1177. visreg_summary_all, visreg_likert_all, visreg_multiple_all,
  1178. ncol = 3
  1179. )
  1180. )
  1181. # For random slopes
  1182. visreg_wm <- visreg(model_all_glove1, "digit_span_score", gg=TRUE,
  1183. xlab= "Working Memory Scores",
  1184. ylab= NULL)
  1185. #scale_y_continuous(breaks = c(1, 2, 3, 4, 5), limits = c(1, 5))
  1186. visreg_hearing <- visreg(model_all_glove1, "digit_in_noise_score", gg=TRUE,
  1187. xlab= "Hearing In Noise",
  1188. ylab= NULL)
  1189. #scale_y_continuous(breaks = c(1, 2, 3, 4, 5), limits = c(1, 5))
  1190. # put them in a grid
  1191. grid.arrange(
  1192. arrangeGrob(
  1193. visreg_wm, visreg_hearing,
  1194. ncol = 2,
  1195. left = textGrob(" Predicted real-time comprehension scores
  1196. given the post-hoc measures",
  1197. rot = 90, vjust = 1.5)
  1198. )
  1199. )
  1200. ```
  1201. 0.00##### variance explained
  1202. ```{r}
  1203. cohens_f2_lmer <- function(mod, digits = 3) {
  1204. stopifnot(inherits(mod, "lmerMod"))
  1205. # Use the exact rows the model used (avoid NA mismatches)
  1206. mf <- model.frame(mod, na.action = na.omit)
  1207. mod_clean <- lmer(formula(mod), data = mf, REML = lme4::isREML(mod))
  1208. # Pieces
  1209. resp <- as.character(formula(mod_clean))[2]
  1210. fixed_terms <- attr(terms(mod_clean), "term.labels") # fixed-only labels
  1211. rand_terms <- lme4::findbars(formula(mod_clean)) # random terms
  1212. # Keep random effects in all reduced models
  1213. rand_str <- if (length(rand_terms)) {
  1214. vapply(rand_terms, function(x) paste0("(", deparse(x), ")"), character(1))
  1215. } else character(0)
  1216. # Model-level R^2 (marginal for fixed effects)
  1217. r2_full_vec <- r.squaredGLMM(mod_clean)
  1218. r2_full_m <- as.numeric(r2_full_vec[1]) # Marginal R^2 (fixed effects)
  1219. r2_full_c <- as.numeric(r2_full_vec[2]) # Conditional R^2 (fixed + random), FYI
  1220. # Build reduced models dropping ONE fixed term at a time
  1221. rows <- lapply(fixed_terms, function(trm) {
  1222. keep_fixed <- setdiff(fixed_terms, trm)
  1223. fixed_rhs <- if (length(keep_fixed)) paste(keep_fixed, collapse = " + ") else "1"
  1224. rhs <- paste(c(fixed_rhs, rand_str), collapse = " + ")
  1225. f_reduced <- as.formula(paste(resp, "~", rhs))
  1226. m_red <- lmer(f_reduced, data = mf, REML = lme4::isREML(mod_clean))
  1227. r2_red <- as.numeric(r.squaredGLMM(m_red)[1]) # marginal R^2 of reduced
  1228. dR2 <- r2_full_m - r2_red # unique/semi-partial R^2
  1229. f2 <- if (is.finite(r2_full_m) && r2_full_m < 1) dR2 / (1 - r2_full_m) else NA_real_
  1230. data.frame(
  1231. Predictor = trm,
  1232. Marginal_R2 = r2_full_m, # same for all rows (model-level)
  1233. Partial_R2 = dR2, # unique/semi-partial R^2 for this predictor
  1234. R2_reduced = r2_red, # for transparency
  1235. Cohen_f2 = f2,
  1236. stringsAsFactors = FALSE
  1237. )
  1238. })
  1239. out <- bind_rows(rows) |>
  1240. arrange(desc(Partial_R2)) |>
  1241. mutate(across(c(Marginal_R2, Partial_R2, R2_reduced, Cohen_f2),
  1242. ~ round(., digits)))
  1243. # (optional) attach conditional R^2 as an attribute for reference
  1244. attr(out, "R2_conditional_model") <- round(r2_full_c, digits)
  1245. out
  1246. }
  1247. # ----- Use it -----
  1248. f2_tbl <- cohens_f2_lmer(model_all_glove1, digits = 3)
  1249. f2_tbl
  1250. # View the model Conditional R^2 (optional):
  1251. attr(f2_tbl, "R2_conditional_model")
  1252. # Cohen (1988) benchmarks: small ~0.02, medium ~0.15, large ~0.35
  1253. ```
  1254. ## Figure: Single fixed-effect regressions + bar plots + multiple fixed-effect regression
  1255. ```{r figure-size, fig.width=18, fig.height=22}
  1256. # all 3 plots in the same grid
  1257. # Create title groups
  1258. title1 <- textGrob("Summary", gp=gpar(fontsize=28,
  1259. color='green3',
  1260. fontface="bold"))
  1261. title2 <- textGrob("10-Point Scale", gp=gpar(fontsize=28,
  1262. color='royalblue',
  1263. fontface="bold"))
  1264. title3 <- textGrob("Multiple Choice Question", gp=gpar(fontsize=28,
  1265. color='violetred',
  1266. fontface="bold"))
  1267. combined_plot_effects <- grid.arrange(
  1268. title1, title2, title3,
  1269. visreg_summary, visreg_likert, visreg_multiple,
  1270. plot_glove_summary, likert_plot, question_plot,
  1271. visreg_summary_all, visreg_likert_all, visreg_multiple_all,
  1272. ncol = 3,
  1273. nrow = 6,
  1274. heights = unit.c(
  1275. unit(4, "null"), # Title row
  1276. unit(8, "null"), # First row of plots
  1277. unit(2, "null"), # Spacer row
  1278. unit(8, "null"), # Second row of plots
  1279. unit(2, "null"), # Spacer row (optional)
  1280. unit(8, "null") # Third row of plots
  1281. ),
  1282. layout_matrix = rbind(
  1283. c(1, 2, 3),
  1284. c(4, 5, 6),
  1285. c(NA, NA, NA),
  1286. c(7, 8, 9),
  1287. c(NA, NA, NA),
  1288. c(10, 11, 12)
  1289. )
  1290. )
  1291. print(combined_plot_effects)
  1292. ```
  1293. ## Predicting speech rate from comprehension
  1294. Does comprehension change with Speech Rate?
  1295. ```{r}
  1296. model_rate <- lmer(speech_rate ~
  1297. trial_median_rescaled +
  1298. glove_sum_max_similarity_rescaled +
  1299. likert_rescaled +
  1300. multiple_choice_accuracy +
  1301. (1 | digit_span_score) +
  1302. (1 | digit_in_noise_score), data = df.data)
  1303. summary(model_rate)
  1304. ```
  1305. ### Figure: Estimates of fixed effects (comprehension measures) on Speech Rate
  1306. ```{r}
  1307. # Extract the fixed effects from the model (including confidence intervals)
  1308. fixed_effects <- tidy(model_rate, effects = "fixed", conf.int = TRUE)
  1309. # Filter out the intercept term
  1310. fixed_effects <- fixed_effects %>%
  1311. filter(term != "(Intercept)")
  1312. # Map the actual terms to custom axis labels
  1313. term_mapping <- c(
  1314. "multiple_choice_accuracy" = "Multiple Choice Question Accuracy",
  1315. "glove_sum_max_similarity_rescaled" = "Summary Semantic Similarity",
  1316. "likert_rescaled" = "10-Point Scale",
  1317. "trial_median_rescaled" = "Median Real-Time Comprehension"
  1318. )
  1319. # Replace the term names with custom labels
  1320. fixed_effects <- fixed_effects %>%
  1321. mutate(term = recode(term, !!!term_mapping))
  1322. # Reorder the terms based on the estimate values in descending order
  1323. fixed_effects <- fixed_effects %>%
  1324. mutate(term = reorder(term, -estimate))
  1325. # Add a column for significance stars based on p-values
  1326. fixed_effects <- fixed_effects %>%
  1327. mutate(sig_stars = case_when(
  1328. p.value < 0.001 ~ "***",
  1329. p.value < 0.01 ~ "**",
  1330. p.value < 0.05 ~ "*",
  1331. TRUE ~ ""
  1332. ))
  1333. # Plot with ggplot2 - swapped x and y axes
  1334. ggplot(fixed_effects, aes(y = term, x = estimate)) + # Swapped axes
  1335. geom_point(aes(color = term), size = 3) + # Add color to the points
  1336. geom_errorbarh(aes(xmin = conf.low, xmax = conf.high, color = term),
  1337. height = 0,
  1338. width = 0) +
  1339. geom_text(aes(label = paste0(round(estimate, 2), sig_stars)), vjust = -1, size = 4) +
  1340. scale_color_manual(values = c(
  1341. "Multiple Choice Question Accuracy" = "violetred",
  1342. "10-Point Scale" = "royalblue",
  1343. "Summary Semantic Similarity" = "green3",
  1344. "Median Real-Time Comprehension" = "purple"
  1345. )) +
  1346. labs(title = " ",
  1347. x = "Regression Coefficient Estimates",
  1348. y = NULL) +
  1349. theme(
  1350. plot.title = element_text(hjust = 0.5, size = 18),
  1351. axis.title.x = element_text(size = 13),
  1352. axis.text.y = element_text(size = 13, color = "black"),
  1353. axis.text.x = element_text(size = 12)
  1354. ) +
  1355. guides(color = "none")
  1356. ```
  1357. ## III. Multiple Choice Questions Diagnosis
  1358. Are questions comparable in difficulty?
  1359. Take all stories presented at 1x. (divide by questions and average across participants for that item)
  1360. ```{r figure-size, fig.width=6, fig.height=6}
  1361. pid <- "participant"
  1362. # 1) per-participant accuracy for each QUESTION at 1×
  1363. by_pp_1x <- df.data %>%
  1364. filter(speech_rate == 1,
  1365. !is.na(question), !is.na(.data[[pid]]),
  1366. !is.na(multiple_choice_accuracy)) %>%
  1367. group_by(.data[[pid]], question) %>%
  1368. summarise(acc = mean(multiple_choice_accuracy, na.rm = TRUE), .groups = "drop")
  1369. # 2) per-question mean accuracy + n participants
  1370. q_summary_1x <- by_pp_1x %>%
  1371. group_by(question) %>%
  1372. summarise(
  1373. mean_acc = mean(acc, na.rm = TRUE),
  1374. n_participants = n(),
  1375. .groups = "drop"
  1376. )
  1377. # quick stats (optional)
  1378. summary_1x <- q_summary_1x %>%
  1379. summarise(
  1380. n_questions = n(),
  1381. mean = mean(mean_acc), median = median(mean_acc),
  1382. sd = sd(mean_acc), iqr = IQR(mean_acc)
  1383. )
  1384. print(round(summary_1x, 3))
  1385. # histogram
  1386. # counts of per-question means rounded to 2 decimals
  1387. counts_2dp <- q_summary_1x %>%
  1388. mutate(mean_acc_2dp = round(mean_acc, 1)) %>%
  1389. count(mean_acc_2dp) %>%
  1390. arrange(mean_acc_2dp)
  1391. ggplot(counts_2dp,
  1392. aes(x = factor(mean_acc_2dp, levels = sort(unique(mean_acc_2dp))), y = n)) +
  1393. scale_x_discrete(expand = c(0,0)) +
  1394. scale_y_continuous(expand = c(0,0), limits = c(0, NA))+
  1395. geom_col(width = 0.75, color = "white") +
  1396. labs(x = "Accuracy", y = "Number of questions", title = "") +
  1397. theme_classic(base_size = 20)
  1398. tbl_1x <- df.data %>%
  1399. filter(speech_rate == 1,
  1400. !is.na(question), !is.na(.data[[pid]]),
  1401. !is.na(multiple_choice_accuracy)) %>%
  1402. # per-participant accuracy per question (in case of multiple trials)
  1403. group_by(.data[[pid]], question) %>%
  1404. summarise(acc = mean(multiple_choice_accuracy, na.rm = TRUE), .groups = "drop") %>%
  1405. # average across participants
  1406. group_by(question) %>%
  1407. summarise(
  1408. n_participants = n(),
  1409. mean_accuracy = mean(acc, na.rm = TRUE),
  1410. .groups = "drop"
  1411. ) %>%
  1412. mutate(mean_accuracy = round(mean_accuracy, 2)) %>% # 2 decimals
  1413. arrange(desc(mean_accuracy))
  1414. tbl_1x
  1415. ```
  1416. Distribution of per-question accuracy averaged across participants for speech rate x1

data_analysis.Rmd at commit b741b80, no license · at the source

Overview

Authors: Irmak Ergin1,2,3, Jill Kries1,2, Shiven Gupta2, Maria Papworth Burrel1,2, Laura Gwilliams1,2,3
  1. Department of Psychology, Stanford University,Stanford, CA USA
  2. Wu Tsai Neurosciences Institute, Stanford University,Stanford, CA USA
  3. Stanford Data Science, Stanford University,Stanford, CA USA
Institutions: Stanford University (United States)
Journal: Behavior research methods, volume 58, issue 4, article 93
Dates: received 3 May 2025; accepted 19 December 2025; published online 27 March 2026; in print 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.3758/s13428-026-02941-1 · PMID 41896392 · PMCID PMC13031255 · OpenAlex W7141543328
Open access: hybrid, a free copy (OpenAlex)
Status: code verified (how the code is found and verified)
Categories: human (organism), cognitive (subfield)
Keywords: Speech comprehension, Naturalistic, Real-time measurement, Language
MeSH: Comprehension*, Speech*, Speech Perception*, Adult, Female, Humans, Male, Young Adult (* major topic)
Journal subjects: Original Manuscript
Topic: Neuroscience and Music Perception (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Citations: not cited yet (Europe PMC); 81 references in the paper

Abstract

Speech comprehension has been described as an effortless and robust process; yet, in real-world contexts, it is common for a listener to misunderstand what was said or fail to derive meaning entirely. Typically, methods of measuring speech comprehension are applied ‘post hoc’ - that is, after the comprehension has happened. This approach fails to capture comprehension as it occurs, limiting the field’s understanding of the cognitive processes involved in real-time comprehension. To overcome these challenges, we designed and tested a novel method of measuring real-time speech comprehension during naturalistic listening. We built a slider device that synchronizes with experimental software and provides millisecond read-out. In three experiments, participants listened to audiobook segments while providing continuous comprehension ratings using the slider. To vary comprehension success, we presented speech segments at speed factors of 1–5 times faster than normal. We validated the time-resolved slider data against established speech comprehension assessment methods. Overall, our findings validate our novel time-resolved comprehension measure and demonstrate that it is possible to derive an online behavioral measure of real-time speech comprehension. We also confirmed numerous limitations of static post hoc assessments, including challenges with multiple-choice question design and the confounding of potential effects due to recency bias and comprehension for summarization. The measure proposed here overcomes the constraints of static post hoc assessments and can be effectively integrated with neuroimaging techniques, offering a valuable tool for future research on dynamic processes during naturalistic listening.

Supplementary Information: The online version contains supplementary material available at 10.3758/s13428-026-02941-1.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repository

Its files are read in the Code ↔ Paper reader above, with 16 matches between paragraphs and lines of code.

irmak-ergin/measuring_naturalistic_speech_comprehension_2024

License: none: the authors keep all their rights
State: the link answers, verified on 30 September 2026
Evidence: files inventoried
Commit: b741b80d2cf99b80484da4fdb781af702bb04092, 3 May 2026
Languages: Jupyter (7), R (4), Python (4), JavaScript (2)
Size: 529 files, 17 scripts
Software Heritage: not archived
Found in: “Data and Code Availability”
Holds: README, 11 notebooks
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (7 files), Matplotlib (6 files), pandas (6 files), scikit-learn (5 files), seaborn (5 files), SciPy (4 files), broom (3 files), caret (3 files), cowplot (3 files), ggplot2 (3 files), lmerTest (3 files), patchwork (3 files), rstatix (3 files), statsmodels (3 files), tidyverse (3 files), emmeans (2 files), lme4 (2 files), multcomp (1 file), PsychoPy (1 file)
Availability: 1 check, the latest on 30 September 2026: the link answers
  • 30 September 2026: the link answers
14 files

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 13 scripts, each with its path and the digest of its content;
  • 16 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

Datasets cited

Data and Code Availability

Data is available at https://osf.io/zk3c5/files/osfstorage, and code and experiment files are publicly available at https://github.com/irmak-ergin/measuring_naturalistic_speech_comprehension_2024. You can find the preregistration and link to the GitHub repository on doi.org/10.17605/OSF.IO/F5UZ8 as well. The design, analyses, and participant-exclusion criteria for Experiment 1 were preregistered. The only exploratory analyses not preregistered were the recency analysis and the investigation of the nature of decline in comprehension (linear vs categorical decline). Experiments 2 and 3 were conducted after preregistration, during the review process.

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 30 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 5 authors, 4 keywords, 8 MeSH terms, 3 funders, 56 references.

Cite

This paper

Ergin, I., Kries, J., Gupta, S., Papworth Burrel, M., & Gwilliams, L. (2026). Measuring naturalistic speech comprehension in real time. Behavior research methods, 58(4), 93. https://doi.org/10.3758/s13428-026-02941-1

BibTeX

@article{ergin2026measuring,
author = {Ergin, Irmak and Kries, Jill and Gupta, Shiven and Papworth Burrel, Maria and Gwilliams, Laura},
title = {{Measuring naturalistic speech comprehension in real time}},
journal = {Behavior research methods},
year = {2026},
month = mar,
volume = {58},
number = {4},
pages = {93},
publisher = {Springer Science+Business Media},
issn = {1554-351X},
doi = {10.3758/s13428-026-02941-1},
url = {https://doi.org/10.3758/s13428-026-02941-1},
pmid = {41896392},
pmcid = {PMC13031255}
}

RIS

TY - JOUR
AU - Ergin, Irmak
AU - Kries, Jill
AU - Gupta, Shiven
AU - Papworth Burrel, Maria
AU - Gwilliams, Laura
TI - Measuring naturalistic speech comprehension in real time
T2 - Behavior research methods
J2 - Behav Res Methods
PY - 2026
DA - 2026/03/27
VL - 58
IS - 4
SP - 93
SN - 1554-351X
PB - Springer Science+Business Media
DO - 10.3758/s13428-026-02941-1
UR - https://doi.org/10.3758/s13428-026-02941-1
LA - en
ER -

CSL-JSON

{
"id": "10.3758/s13428-026-02941-1",
"type": "article-journal",
"title": "Measuring naturalistic speech comprehension in real time",
"container-title": "Behavior research methods",
"author": [
{
"family": "Ergin",
"given": "Irmak"
},
{
"family": "Kries",
"given": "Jill"
},
{
"family": "Gupta",
"given": "Shiven"
},
{
"family": "Papworth Burrel",
"given": "Maria"
},
{
"family": "Gwilliams",
"given": "Laura"
}
],
"container-title-short": "Behav Res Methods",
"volume": "58",
"issue": "4",
"page": "93",
"DOI": "10.3758/s13428-026-02941-1",
"PMID": "41896392",
"PMCID": "PMC13031255",
"ISSN": "1554-351X",
"publisher": "Springer Science+Business Media",
"URL": "https://doi.org/10.3758/s13428-026-02941-1",
"language": "en",
"issued": {
"date-parts": [
[
2026,
3,
27
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1523/jneurosci.1001-25.2025 [code]
The Spatio-Temporal Dynamics of Phoneme Encoding in Aging and Aphasia.
Journal: The Journal of neuroscience : the official journal of the Society for Neuroscience
In common: seaborn, scikit-learn, pandas, 3 other tools, cognitive, 5 references, 2 authors
[2] doi:10.1371/journal.pbio.3003588 [code]
Sensory sharpening and semantic prediction errors unify competing models of predictive processing in human speech comprehension.
Journal: PLoS biology
In common: PsychoPy, emmeans, lmerTest, 8 other tools, cognitive, 8 references
[3] doi:10.1126/sciadv.ado8915 [code]
The human auditory cortex concurrently tracks syllabic and phonemic timescales via acoustic spectral flux.
Journal: Science advances
In common: multcomp, emmeans, lmerTest, 8 other tools, 6 references
[4] doi:10.1523/jneurosci.0657-25.2025 [code]
Attentional Engagement with Target and Distractor Streams Predicts Speech Comprehension in Multitalker Environments.
Journal: The Journal of neuroscience : the official journal of the Society for Neuroscience
In common: rstatix, emmeans, lmerTest, 9 other tools, cognitive, 5 references
[5] doi:10.1162/nol_a_00081 [code]
Causal Contributions of the Domain-General (Multiple Demand) and the Language-Selective Brain Networks to Perceptual and Semantic Challenges in Speech Comprehension.
Journal: Neurobiology of language (Cambridge, Mass.)
In common: rstatix, emmeans, lmerTest, 4 other tools, cognitive, 6 references
[6] doi:10.1002/hbm.26305 [code]
Cortical haemodynamic responses predict individual ability to recognise vocal emotions with uninformative pitch cues but do not distinguish different emotions.
Journal: Human brain mapping
In common: multcomp, caret, broom, 9 other tools, cognitive, 2 references
[7] doi:10.1523/eneuro.0069-26.2026 [code]
Electrophysiological Indices of Hierarchical Speech Processing Differentially Reflect the Comprehension of Speech in Noise.
Journal: eNeuro
In common: emmeans, lmerTest, cognitive, 10 references
[8] doi:10.1038/s41598-024-69602-1 [code]
Neural encoding of linguistic speech cues is unaffected by cognitive decline, but decreases with increasing hearing impairment.
Journal: Scientific reports
In common: emmeans, lmerTest, lme4, 8 other tools, cognitive, 5 references
[9] doi:10.1093/bioinformatics/btag592 [code]
Network-based stratification of allele-specific expression reveals patient subgroups in Huntington's disease.
Journal: Bioinformatics (Oxford, England)
In common: multcomp, broom, rstatix, 12 other tools
[10] doi:10.1016/j.dcn.2023.101251 [code]
Age and vocabulary knowledge differentially influence the N400 and theta responses during semantic retrieval.
Journal: Developmental cognitive neuroscience
In common: multcomp, broom, rstatix, 9 other tools, 2 references

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Add the badge to the code's README

OSCR: the badge of this paper

If you are an author or a maintainer of this paper's code, you can add this badge. It says what OSCR knows of the paper (its code, its validated tracing map) and links the code to this page. Paste one of these snippets under the README's title. Keep the two marks around it, so that the badge can be replaced in place and is never added twice. OSCR changes nothing in your repository.

Markdown
<!-- oscr-badge:start -->
[![OSCR](https://openscicode.org/badge/doi/10.3758/s13428-026-02941-1.svg)](https://openscicode.org/badge/doi/10.3758/s13428-026-02941-1)
<!-- oscr-badge:end -->
reStructuredText
.. oscr-badge:start

.. image:: https://openscicode.org/badge/doi/10.3758/s13428-026-02941-1.svg
   :target: https://openscicode.org/badge/doi/10.3758/s13428-026-02941-1
   :alt: OSCR

.. oscr-badge:end
HTML
<!-- oscr-badge:start -->
<a href="https://openscicode.org/badge/doi/10.3758/s13428-026-02941-1"><img src="https://openscicode.org/badge/doi/10.3758/s13428-026-02941-1.svg" alt="OSCR"></a>
<!-- oscr-badge:end -->

On GitHub, open the README, choose Edit (the pencil), paste the snippet under the title, then "Commit changes".

If you choose to, you can have the badge kept up to date for you, with oscr badge --write from the command line or with the GitHub Action in your repository's workflows. Every other form and option (a card, AsciiDoc, LaTeX, shields.io) is on the badge page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: code errors and mismatches between the code and the paper about this paper and its code, from readers and its authors, beside the paper and its tracing map.

Report a mismatch between the code and the paper · Report a code error (signed in)

Reproductions: reports from readers who ran the authors' code and did not get the paper's result, what they ran, with which environment, commit and data.

Report a reproduction failure (signed in)

Versions of its code: the releases of its code tied to a version of this paper (the preprint, the accepted manuscript, the version of record…), each with the tracing map versioned with it.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.