OSCR

Unbiased data-driven analysis of five amyloid-beta peptides for biomarker investigations in familial Alzheimer's disease.

Code ↔ Paper

1 match 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 1 match
  1. [1] § Statistical analysis › Main metrics ↔ R/evaluate_biomarkers.R, lines 1 to 72 · score 0.63 · confidence intervals, evaluate biomarkers, Median, CIs, bootstrapping, metric · 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 · 413 lines · 22 KB · GPL-3.0 · 1 match

  1. #' Evaluate Discovered Biomarkers Across Multiple Datasets and Groups
  2. #'
  3. #' Reads biomarker definitions from a discovery results file and evaluates their
  4. #' performance (Repeatability, Separation, Sample Size Estimate) on specified
  5. #' datasets and diagnostic groups (CU/CI), optionally calculating confidence intervals.
  6. #' Evaluation is performed dataset-by-dataset, group-by-group.
  7. #'
  8. #' @param discovery_results_csv_path Character string. Path to the input CSV file
  9. #' containing biomarker discovery results (typically generated by `run_experiments`).
  10. #' Expected columns include `regs_numerator`, `regs_denominator`, `var_composition`,
  11. #' and ideally identifiers like `discovery_dataset`, `experiment_tag`.
  12. #' @param prepared_data_list List. The output from `preprocess_data()`,
  13. #' containing filtered data for multiple datasets (e.g., `list(ADNI=list(data=..., data_suv_bi=...), MAYO=...)`).
  14. #' @param config List. The loaded main configuration object (output from
  15. #' `preprocess_data$config`). Must contain sections like
  16. #' `preprocessing`, `model_equations`, `power_params`.
  17. #' @param datasets_to_evaluate Character vector. Names of the datasets within
  18. #' `prepared_data_list` to evaluate the biomarkers on. Defaults to using all
  19. #' datasets present in `prepared_data_list`.
  20. #' @param groups_to_evaluate NULL or character vector. Groups ("CU", "CI") to evaluate
  21. #' within each dataset. Defaults to `NULL`.
  22. #' When NULL, it takes the same group that was used in the given row.
  23. #' @param calculate_ci Logical. If `TRUE`, calculate and return bootstrap/estimated
  24. #' 95% confidence intervals using `.feval_group_95CI`. Defaults to `FALSE`.
  25. #' @param nsim Integer. Number of bootstrap simulations if `calculate_ci` is `TRUE`.
  26. #' Defaults to 100. Ignored if `calculate_ci` is `FALSE`.
  27. #' @param output_evaluation_csv_path Character string or NULL. If provided, the full
  28. #' path to save the *evaluation* results table as a *new* CSV file. If the file
  29. #' exists, it will be overwritten. Directory created if needed.
  30. #' @param id_col Character string. Name of the unique individual identifier column,
  31. #' used for merging and checking data. Fetched from config if possible, defaults
  32. #' to "RID".
  33. #' @param verbose Logical. Print progress messages? Defaults to TRUE.
  34. #'
  35. #' @return A data frame containing the evaluation results. Each row corresponds to
  36. #' one discovered biomarker evaluated on one specific dataset and group. Columns
  37. #' include identifiers from the input CSV, evaluation context (dataset, group),
  38. #' evaluation status, and the calculated metrics (Rep, SepAB, SSE, plus CIs
  39. #' if requested). Returns `NULL` if the input CSV cannot be read or essential
  40. #' data is missing.
  41. #'
  42. #' @details
  43. #' Workflow:
  44. #' 1. Reads the `discovery_results_csv_path`.
  45. #' 2. Iterates through each dataset in `datasets_to_evaluate`.
  46. #' 3. For each dataset, identifies available features and prepares clinical data.
  47. #' 4. Iterates through each group in `groups_to_evaluate`.
  48. #' 5. For each dataset/group combination, iterates through each row (discovered biomarker)
  49. #' in the discovery results file.
  50. #' 6. Validates that the biomarker's required regions exist in the current dataset.
  51. #' 7. Reconstructs a 'chromosome' vector based on the biomarker's definition and
  52. #' the available features for the current dataset.
  53. #' 8. Calls internal `.calculate_cvr` to compute the biomarker value.
  54. #' 9. Merges the calculated value with the clinical data.
  55. #' 10. Checks for sufficient data in the specific dataset/group combination.
  56. #' 11. Calls `.feval_group` (or `.feval_group_95CI`) to get performance metrics.
  57. #' 12. Records the metrics along with discovery identifiers and evaluation context.
  58. #' 13. Combines all results into a single data frame after all loops complete.
  59. #' 14. Optionally saves the results table to `output_evaluation_csv_path`.
  60. #'
  61. #' @export
  62. #' @importFrom utils read.csv head
  63. #' @importFrom readr read_csv write_csv
  64. #' @importFrom dplyr bind_rows select all_of left_join filter n_distinct between
  65. #' @importFrom stringr str_split
  66. #' @importFrom rlang `%||%` .data sym is_scalar_character is_scalar_logical is_scalar_integerish
  67. #' @importFrom stats setNames median quantile IQR sd na.omit aggregate as.formula
  68. #' @importFrom lme4 lmerControl
  69. #' @importFrom longpower lmmpower
  70. #' @importFrom methods is
  71. #' @importFrom progress progress_bar
  72. evaluate_biomarkers <- function(discovery_results_csv_path,
  73. prepared_data_list,
  74. config,
  75. datasets_to_evaluate = names(prepared_data_list),
  76. groups_to_evaluate = c("CU", "CI"),
  77. calculate_ci = FALSE,
  78. nsim = 1000,
  79. output_evaluation_csv_path = NULL,
  80. id_col = NULL, # Get from config later
  81. verbose = TRUE) {
  82. # --- Input Validation ---
  83. stopifnot(
  84. rlang::is_scalar_character(discovery_results_csv_path),
  85. file.exists(discovery_results_csv_path),
  86. is.list(prepared_data_list), length(prepared_data_list) > 0,
  87. is.list(config),
  88. is.null(datasets_to_evaluate) || (is.character(datasets_to_evaluate) && length(datasets_to_evaluate) > 0),
  89. is.null(groups_to_evaluate) || (is.character(groups_to_evaluate) && all(groups_to_evaluate %in% c("CU", "CI"))),
  90. rlang::is_scalar_logical(calculate_ci),
  91. rlang::is_scalar_integerish(nsim), nsim > 1,
  92. is.null(output_evaluation_csv_path) || rlang::is_scalar_character(output_evaluation_csv_path),
  93. is.null(id_col) || rlang::is_scalar_character(id_col),
  94. rlang::is_scalar_logical(verbose)
  95. )
  96. # Check datasets exist
  97. missing_dsets <- setdiff(datasets_to_evaluate, names(prepared_data_list))
  98. datasets_to_evaluate <- intersect(datasets_to_evaluate, names(prepared_data_list))
  99. if (length(missing_dsets) > 0) message("Datasets not found in prepared_data_list: ", paste(missing_dsets, collapse=", "), "\nProceeding with ", datasets_to_evaluate)
  100. # Get ID col from config or use default
  101. id_col <- id_col %||% config$preprocessing$id_column %||% "RID"
  102. ignore_cols <- config$preprocessing$ignore_columns %||% "idx"
  103. # --- Load Discovery Results ---
  104. message("--- Loading Discovery Results ---")
  105. discovery_results_df <- try(readr::read_csv(discovery_results_csv_path, show_col_types = FALSE), silent = TRUE)
  106. if (inherits(discovery_results_df, "try-error")) {
  107. stop("Failed to read discovery results CSV: ", discovery_results_csv_path, "\nError: ", conditionMessage(attr(discovery_results_df,"condition")))
  108. }
  109. if (nrow(discovery_results_df) == 0) {
  110. warning("Discovery results CSV is empty: ", discovery_results_csv_path, call. = FALSE); return(NULL)
  111. }
  112. message(sprintf("Loaded %d discovered biomarker results to evaluate.", nrow(discovery_results_df)))
  113. # Check for essential columns from discovery results
  114. required_discovery_cols <- c("regs_numerator", "regs_denominator", "var_composition") # Add others used for ID if needed
  115. missing_discovery_cols <- setdiff(required_discovery_cols, names(discovery_results_df))
  116. if (length(missing_discovery_cols) > 0) {
  117. stop("Input discovery CSV is missing required columns: ", paste(missing_discovery_cols, collapse=", "))
  118. }
  119. # required_cols
  120. required_cols <- c(id_col, "time", "DX", "AB", "value") # Ensure these align with formulas/logic
  121. # --- Prepare Common Elements (Formulas, Control) ---
  122. all_power_params <- config$power_params
  123. eq_all_str <- config$model_equations$eq_all_string
  124. eq_group_str <- config$model_equations$eq_group_string
  125. if(is.null(eq_all_str) || is.null(eq_group_str)) stop("Missing formula strings in config.")
  126. eq_all <- try(stats::as.formula(eq_all_str)); if(inherits(eq_all, "try-error")) stop("Invalid eq_all_str")
  127. eq_group <- try(stats::as.formula(eq_group_str)); if(inherits(eq_group, "try-error")) stop("Invalid eq_group_str")
  128. lmer_control <- lme4::lmerControl(optimizer = "nloptwrap", calc.derivs = FALSE,
  129. check.conv.singular = "ignore",
  130. optCtrl = list(method = "nlminb", starttests = FALSE, kkt = FALSE))
  131. # --- Initialize List to Store All Evaluation Results ---
  132. all_evaluation_results <- list()
  133. result_counter <- 0
  134. # --- progress bar ---
  135. # Multicohort take as many time as the number of cohorts
  136. total_iterations <- max(1,length(datasets_to_evaluate)) * max(1, length(groups_to_evaluate)) * nrow(discovery_results_df)
  137. message(sprintf("Performing %d evaluations (%d datasets, %d groups, %d biomarkers).", total_iterations, length(datasets_to_evaluate), length(groups_to_evaluate), nrow(discovery_results_df)))
  138. pb <- progress_bar$new(
  139. format = "Progress [:bar] :percent (:current/:total) | ETA: :eta ",
  140. total = total_iterations,
  141. clear = FALSE,
  142. width = 50
  143. )
  144. # --- Loop 1: Through Datasets to Evaluate On ---
  145. message("\n--- Starting Biomarker Evaluation ---")
  146. for (eval_dset_name in datasets_to_evaluate) {
  147. if(verbose) message(sprintf("\nProcessing Dataset: %s", eval_dset_name))
  148. current_eval_data_list <- prepared_data_list[[eval_dset_name]]
  149. # --- Prepare data for this dataset once ---
  150. if (!"data" %in% names(current_eval_data_list) || !is.data.frame(current_eval_data_list$data) ||
  151. !"data_suv_bi" %in% names(current_eval_data_list) || !is.data.frame(current_eval_data_list$data_suv_bi)) {
  152. warning(sprintf("Skipping dataset '%s': Missing 'data' or 'data_suv_bi'.", eval_dset_name), call. = FALSE)
  153. next # Skip to next dataset
  154. }
  155. data_clinical <- current_eval_data_list$data
  156. data_suv <- current_eval_data_list$data_suv_bi
  157. available_features <- setdiff(names(data_suv), c(id_col, ignore_cols)) # Features available in THIS dataset
  158. required_clinical_cols <- c(id_col, "time", "DX", "AB") # Align with .feval_group needs
  159. if (length(available_features) == 0) {
  160. warning(sprintf("Skipping dataset '%s': No valid feature columns found in 'data_suv_bi'.", eval_dset_name), call. = FALSE)
  161. next
  162. }
  163. if (!all(required_clinical_cols %in% names(data_clinical))) {
  164. warning(sprintf("Skipping dataset '%s': Clinical data missing required columns: %s.",
  165. eval_dset_name, paste(setdiff(required_clinical_cols, names(data_clinical)), collapse=", ")), call. = FALSE)
  166. next
  167. }
  168. # --- Loop 2: Through Groups to Evaluate ---
  169. for (eval_group in groups_to_evaluate) {
  170. if(verbose) message(sprintf(" Evaluating Group: %s", eval_group))
  171. target_group_dx_val <- if(eval_group == "CI") 1L else 0L
  172. # --- Pre-Biomarker Loop Data Sufficiency Check *** ---
  173. min_rows_threshold <- 30
  174. min_group_members_threshold <- 20
  175. eval_status_group <- "Pending" # Status for this group overall
  176. # Check the clinical data prepared for this dataset
  177. group_subset_check <- data_clinical |>
  178. dplyr::filter(.data$DX == target_group_dx_val, .data$AB == TRUE, !is.na(.data$AB))
  179. n_group_rows_check <- nrow(group_subset_check)
  180. n_group_ids_check <- dplyr::n_distinct(group_subset_check[[id_col]])
  181. if(n_group_rows_check < min_rows_threshold || n_group_ids_check < min_group_members_threshold ) {
  182. warning(sprintf("Skipping ALL biomarkers for Dataset '%s', Group '%s': Insufficient initial data rows (%d) or unique IDs (%d) for reliable metrics evaluation. Min required: %d rows, %d IDs.",
  183. eval_dset_name, eval_group, n_group_rows_check, n_group_ids_check, min_rows_threshold, min_group_members_threshold), call.=FALSE)
  184. eval_status_group <- "Insufficient Data Pre-Check"
  185. # # --- Optionally log skipped rows for *all* biomarkers for this group/dataset ---
  186. # # This creates placeholder rows indicating the skip reason for clarity in output CSV
  187. # if (!is.null(output_evaluation_csv_path)) {
  188. # message(sprintf(" -> Logging skipped status for %d biomarkers...", nrow(discovery_results_df)))
  189. # # Define NA metrics structure
  190. # if (calculate_ci) metrics_na <- stats::setNames(rep(NA_real_, 9), c("Rep", "Rep.lower", "Rep.upper", "SepAB", "SepAB.lower", "SepAB.upper", "SSE", "SSE.lower", "SSE.upper"))
  191. # else metrics_na <- stats::setNames(rep(NA_real_, 3), c("Rep", "SepAB", "SSE"))
  192. # metrics_df_na <- as.data.frame(as.list(metrics_na))
  193. #
  194. # # Loop through biomarkers just to create the log entries
  195. # for (i_skip in seq_len(nrow(discovery_results_df))) {
  196. # biomarker_row_skip <- discovery_results_df[i_skip, , drop = FALSE]
  197. # discovery_info_skip <- biomarker_row_skip |>
  198. # dplyr::select(dplyr::any_of(c("timestamp", "experiment_tag", "discovery_dataset",
  199. # "group_evaluated", "bilateral", "var_composition",
  200. # "fitness_value", "regs_numerator", "regs_denominator",
  201. # "reference_vector", "ga_seed_used")))
  202. # result_counter <- result_counter + 1
  203. # all_evaluation_results[[result_counter]] <- dplyr::bind_cols(
  204. # discovery_info_skip,
  205. # data.frame(evaluation_dataset = eval_dset_name,
  206. # evaluation_group = eval_group,
  207. # evaluation_status = eval_status_group, # Log skip reason
  208. # stringsAsFactors = FALSE),
  209. # metrics_df_na
  210. # )
  211. # } # End loop logging skipped biomarkers
  212. # } # End if logging skipped rows
  213. # Advance progress bar
  214. pb$tick(nrow(discovery_results_df))
  215. next # Skip to the next group (or dataset if this was the last group)
  216. }
  217. # --- *** END: Pre-Biomarker Loop Data Sufficiency Check *** ---
  218. # --- Loop 3: Through Each Discovered Biomarker (Row in CSV) ---
  219. for (i in seq_len(nrow(discovery_results_df))) {
  220. # tick now in case a biomarker is skipped
  221. pb$tick()
  222. biomarker_row <- discovery_results_df[i, , drop = FALSE]
  223. # Use a unique identifier for the biomarker if available (e.g., from discovery_dataset + experiment_tag)
  224. # biomarker_id_str <- paste(biomarker_row$discovery_dataset %||% "NA", biomarker_row$experiment_tag %||% i, sep="_")
  225. # if(verbose) message(sprintf(" Biomarker %d/%d (%s)...", i, nrow(discovery_results_df), biomarker_id_str))
  226. # Extract definition
  227. regs_num_str <- biomarker_row$regs_numerator[[1]] %||% ""
  228. regs_den_str <- biomarker_row$regs_denominator[[1]] %||% ""
  229. var_comp <- biomarker_row$var_composition[[1]]
  230. if (!nzchar(regs_num_str) || !nzchar(regs_den_str) || is.na(var_comp)) {
  231. # warning(sprintf("Skipping biomarker row %d on %s/%s: Missing/invalid definition.", i, eval_dset_name, eval_group), call. = FALSE)
  232. next # Skip this biomarker if definition is bad
  233. }
  234. # Parse regions
  235. num_regions <- stringr::str_split(regs_num_str, ", ")[[1]]
  236. den_regions <- stringr::str_split(regs_den_str, ", ")[[1]]
  237. num_regions <- num_regions[nzchar(num_regions)]
  238. den_regions <- den_regions[nzchar(den_regions)]
  239. if (length(num_regions) == 0 || length(den_regions) == 0) {
  240. # warning(sprintf("Skipping biomarker row %d on %s/%s: Parsed definition empty.", i, eval_dset_name, eval_group), call. = FALSE)
  241. next # Skip this biomarker
  242. }
  243. # --- Check if required regions exist in THIS dataset ---
  244. missing_num <- setdiff(num_regions, available_features)
  245. missing_den <- setdiff(den_regions, available_features)
  246. if (length(missing_num) > 0 || length(missing_den) > 0) {
  247. # warning(sprintf("Skipping biomarker row %d on %s/%s: Required regions not found.", i, eval_dset_name, eval_group), call. = FALSE)
  248. # Optionally log which regions were missing
  249. next # Skip this biomarker for this dataset
  250. }
  251. # --- Reconstruct Chromosome for .calculate_cvr ---
  252. chromosome <- rep(1.5, length(available_features))
  253. chromosome[available_features %in% num_regions] <- 0.5
  254. chromosome[available_features %in% den_regions] <- 2.5
  255. # --- Calculate CVR Value ---
  256. calculated_value <- tryCatch({
  257. .calculate_cvr(
  258. chromosome = chromosome,
  259. dataset_cohort_data = current_eval_data_list,
  260. features = available_features,
  261. var_composition = var_comp,
  262. fixed_numerator_regs = NULL, # Not applicable here
  263. fixed_denominator_regs = NULL,
  264. verbose = FALSE
  265. )
  266. }, error = function(e) { NULL })
  267. if (is.null(calculated_value) || all(is.na(calculated_value))) {
  268. # warning(sprintf("Skipping biomarker row %d on %s/%s: .calculate_cvr failed.", i, eval_dset_name, eval_group), call.=FALSE)
  269. # Store a row indicating CVR failure? Or just skip? Let's skip.
  270. next
  271. }
  272. # --- Prepare data for evaluation ---
  273. eval_data <- data_clinical # Start fresh for each biomarker
  274. if (nrow(eval_data) != length(calculated_value)) {
  275. warning(sprintf("Skipping biomarker row %d on %s/%s: Row mismatch CVR/clinical (%d vs %d).",
  276. i, eval_dset_name, eval_group, length(calculated_value), nrow(eval_data)), call. = FALSE)
  277. next
  278. }
  279. eval_data$value <- calculated_value # Add the calculated biomarker value
  280. eval_data <- stats::na.omit(eval_data[, required_cols, drop = FALSE])
  281. # --- Initialize metrics & status ---
  282. metrics <- NULL
  283. eval_status <- "Pending"
  284. # --- Check data sufficiency for this group ---
  285. min_rows_threshold <- 30
  286. min_group_members_threshold <- 20
  287. group_subset <- eval_data |> dplyr::filter(.data$DX == target_group_dx_val, .data$AB == TRUE)
  288. n_group_rows <- nrow(group_subset)
  289. n_group_ids <- dplyr::n_distinct(group_subset[[id_col]])
  290. if(n_group_rows < min_rows_threshold || n_group_ids < min_group_members_threshold ) {
  291. # warning(sprintf("Biomarker row %d on %s/%s: Insufficient data rows (%d) or unique IDs (%d).",
  292. # i, eval_dset_name, eval_group, n_group_rows, n_group_ids), call.=FALSE)
  293. eval_status <- "Insufficient Data"
  294. if (calculate_ci) metrics <- stats::setNames(rep(NA_real_, 9), c("Rep", "Rep.lower", "Rep.upper", "SepAB", "SepAB.lower", "SepAB.upper", "SSE", "SSE.lower", "SSE.upper"))
  295. else metrics <- stats::setNames(rep(NA_real_, 3), c("Rep", "SepAB", "SSE"))
  296. } else {
  297. # --- Call appropriate evaluation function ---
  298. eval_func <- if(calculate_ci) .feval_group_95CI else .feval_group
  299. eval_args <- list(
  300. data = eval_data, # Use the data with 'value' column added
  301. group = eval_group,
  302. eq_all = eq_all,
  303. eq_group = eq_group,
  304. all_power_params = all_power_params,
  305. lmer_control = lmer_control
  306. )
  307. if(calculate_ci) eval_args$nsim <- nsim
  308. metrics_result <- try(do.call(eval_func, eval_args), silent = TRUE)
  309. if (inherits(metrics_result, "try-error")) {
  310. warning(sprintf("Biomarker row %d on %s/%s: Evaluation function failed. Error: %s",
  311. i, eval_dset_name, eval_group, conditionMessage(attr(metrics_result, "condition"))), call.=FALSE)
  312. eval_status <- "Metrics Calculation Error"
  313. if (calculate_ci) metrics <- stats::setNames(rep(NA_real_, 9), c("Rep", "Rep.lower", "Rep.upper", "SepAB", "SepAB.lower", "SepAB.upper", "SSE", "SSE.lower", "SSE.upper"))
  314. else metrics <- stats::setNames(rep(NA_real_, 3), c("Rep", "SepAB", "SSE"))
  315. } else if (anyNA(metrics_result)) {
  316. # warning(sprintf("Biomarker row %d on %s/%s: Evaluation function returned NA values.", i, eval_dset_name, eval_group), call.=FALSE)
  317. eval_status <- "Metrics Calculation NA"
  318. metrics <- metrics_result # Store the NAs
  319. } else {
  320. eval_status <- "Success"
  321. metrics <- metrics_result
  322. }
  323. } # End if sufficient data
  324. # --- Store Result Row ---
  325. result_counter <- result_counter + 1
  326. # Combine discovery info with evaluation info and metrics
  327. discovery_info <- biomarker_row |>
  328. dplyr::select(dplyr::any_of(c("timestamp", "experiment_tag", "discovery_dataset",
  329. "group_evaluated", "bilateral", "var_composition",
  330. "fitness_value", "regs_numerator", "regs_denominator",
  331. "reference_vector", "ga_seed_used"))) # Select relevant discovery columns
  332. metrics_df <- as.data.frame(as.list(metrics))
  333. all_evaluation_results[[result_counter]] <- dplyr::bind_cols(
  334. discovery_info,
  335. data.frame(evaluation_dataset = eval_dset_name,
  336. evaluation_group = eval_group,
  337. evaluation_status = eval_status,
  338. stringsAsFactors = FALSE),
  339. metrics_df
  340. )
  341. } # End loop over biomarkers (CSV rows)
  342. } # End loop over groups
  343. } # End loop over datasets
  344. # --- Combine All Results ---
  345. if (length(all_evaluation_results) == 0) {
  346. warning("No evaluation results were generated.", call. = FALSE)
  347. final_results_df <- data.frame() # Return empty data frame
  348. } else {
  349. final_results_df <- dplyr::bind_rows(all_evaluation_results)
  350. message(sprintf("\n--- Evaluation Complete: Generated %d result rows. ---", nrow(final_results_df)))
  351. }
  352. # --- Save Final Table (Optional) ---
  353. if (!is.null(output_evaluation_csv_path)) {
  354. tryCatch({
  355. output_dir <- dirname(output_evaluation_csv_path)
  356. if (!dir.exists(output_dir)) {
  357. message("Creating output directory for evaluation CSV: ", output_dir)
  358. dir.create(output_dir, recursive = TRUE, showWarnings = FALSE)
  359. }
  360. if(dir.exists(output_dir)){
  361. readr::write_csv(final_results_df, output_evaluation_csv_path, na = "NA") # Write NA for missing
  362. message("Evaluation results saved to: ", output_evaluation_csv_path)
  363. } else {
  364. warning("Failed to create output directory '", output_dir, "'. Cannot write evaluation CSV.", call.=FALSE)
  365. }
  366. }, error = function(e) {
  367. warning(sprintf("Failed to write evaluation results to '%s': %s", output_evaluation_csv_path, e$message), call. = FALSE)
  368. })
  369. }
  370. invisible(final_results_df)
  371. }

evaluate_biomarkers.R at commit 4903f81, under GPL-3.0 · at the source

Overview

Authors: Isaac Llorente-Saguer1, Rebecca Gabriele2, Teisha Y Bradshaw3, Claire A Leckey4,5,6, Christopher R S Belder5,7,8, Lucía Chávez-Gutiérrez9,10, Rohan de Silva3, Nick C Fox2,5,6, Selina Wray2, Neil P Oxtoby11, Charles Arber2
  1. Department of Medical Physics and Biomedical Engineering and UCL Hawkes Institute, University College London, London WC1V 6LJ, UK
  2. Department of Neurodegenerative Disease, UCL Queen Square Institute of Neurology, London WC1N 3BG, UK
  3. Department of Clinical and Movement Neurosciences, Reta Lila Weston Institute, UCL Queen Square Institute of Neurology, London WC1N 3BG, UK
  4. Translational Mass Spectrometry Research Group, UCL Great Ormond Street Hospital Institute of Child Health, University College London, London WC1N 1EH, UK
  5. Dementia Research Centre, UCL Queen Square Institute of Neurology, London WC1N 3BG, UK
  6. UK Dementia Research Institute at UCL, London WC1E 6BT, UK
  7. Adelaide Medical School, The University of Adelaide, Adelaide, SA 5000, Australia
  8. Department of Neurology, Royal Adelaide Hospital and Queen Elizabeth Hospital, Adelaide, South Australia 5005, Australia
  9. VIB-KU Leuven Center for Brain & Disease Research, Leuven 3001, Belgium
  10. Department of Neurosciences, Leuven Research Institute for Neuroscience and Disease (LIND), KU Leuven, Leuven 3001, Belgium
  11. Department of Computer Science and UCL Hawkes Institute, University College London, London WC1V 6LJ, UK
Journal: Brain communications, volume 8, issue 2, article fcag105
Dates: received 3 October 2025; accepted 19 March 2026; published online 22 March 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1093/braincomms/fcag105 · PMID 42022292 · PMCID PMC13096935 · OpenAlex W7140040425
Open access: gold, a free copy (OpenAlex)
Status: code verified (how the code is found and verified)
Categories: human (organism), Alzheimer's / dementia (population), clinical / translational (subfield)
Methods: Connectivity, Statistics, Machine learning
Keywords: amyloid, data-driven, biomarker, iPSC, Alzheimer’s disease
Topic: Alzheimer's disease research and treatments (Cellular and Molecular Neuroscience, Neuroscience), according to OpenAlex
Funding: UK Research and Innovation (UKRI) Future Leaders Fellowship (MR/S03546X/1); Race Against Dementia; Alzheimer’s Society (AS-JF-18-008); Alzheimer's Research UK (ARUKSRF-2016B); EPSRC; UCL Centre for Doctoral Training in Intelligent; Integrated Imaging in Healthcare (i4health) (EP/S021930/1); Medical Research Council (MR/M02492X/1); National Institute for Health and Care Research University College London Hospitals Biomedical Research Centre
Citations: not cited yet (Europe PMC); 29 references in the paper

Abstract

Changes to the relative abundance of amyloid-beta (Aβ) peptides are hallmarks of Alzheimer’s disease. Induced pluripotent stem cell (iPSC)-derived neurons offer a physiological model of Aβ production. We employed unbiased, data-driven analyses to investigate combinations of Aβ peptides as Alzheimer’s disease biomarkers and the relative contribution of peptides to Alzheimer’s disease pathogenesis.

We measured Aβ37, Aβ38, Aβ40, Aβ42 and Aβ43 in 10 iPSC-neuronal cultures from PSEN1 mutation carriers. We combined these data with published cell model data and used linear weighted combinations to (i) distinguish Alzheimer’s disease from controls, and (ii) predict age-at-onset for PSEN1 mutations. Data-driven approaches distinguished Aβ42 and Aβ43 from shorter peptides, providing unbiased evidence for a greater association of Aβ42 and Aβ43 to disease pathogenesis, compared with shorter peptides (Aβ37, Aβ38 and Aβ40). Weighted linear combinations of Aβ peptides outperform Aβ42/40 and provide insights into relative peptide contribution as biomarkers. A representative weighted composite value ratio (wCVR) derived from all data, balancing both disease classification and age-at-onset prediction, was (21⋅Aβ37+10⋅Aβ38+69⋅Aβ40)/ (94⋅Aβ42+6⋅Aβ43). This work suggests a practical non-parametric harmonization approach to employing Aβ ratios as biomarkers for Alzheimer’s disease, from multiple sites and assays. Building on this foundation, we applied a new model using weighted composite value ratios, which outperform existing biomarkers across all tasks. This underscores the value of integrating multiple peptides and assigning optimized weightings. The study confirms the association of Aβ42 and Aβ43 with Alzheimer’s disease pathogenesis in a data-driven manner. Peptide weights further provide mechanistic insights into the relative contribution of each peptide to disease, such as a greater contribution of Aβ37 compared to Aβ38. The algorithm used herein can be further refined to improve biomarkers for Alzheimer’s disease.

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 1 match between paragraphs and lines of code.

isaac-6/biodiscvr

License: GPL-3.0
State: the link answers, verified on 30 September 2026
Evidence: files inventoried
Commit: 4903f81bc04d67aeb602dc1f9f068c6fb2925be4, 15 March 2026
Languages: R (17)
Size: 56 files, 17 scripts
Software Heritage: archived
Found in: “Data availability”
Holds: README, license file, CITATION.cff, environment (DESCRIPTION), tests, continuous integration, documentation, 3 notebooks
Tools: tidyverse (12 files), lme4 (6 files)
Availability: 1 check, the latest on 30 September 2026: the link answers
  • 30 September 2026: the link answers
19 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;
  • 17 scripts, each with its path and the digest of its content;
  • 1 match 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

No dataset and no data link were found in the paper.

Data availability

The datasets generated and analysed during the current study are available in the supplementary figures and from the primary publications: Petit et al. colleagues,9 Liu et al.,8 and Schultz et al.10 The framework code used for biomarker discovery (weighted-composite value ratios), is available in a public GitHub repository under the GNU General Public License v3.0: https://github.com/isaac-6/biodiscvr.

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, 11 authors, 5 keywords, 9 funders, 27 references.

Cite

This paper

Llorente-Saguer, I., Gabriele, R., Bradshaw, T. Y., Leckey, C. A., Belder, C. R. S., Chávez-Gutiérrez, L., de Silva, R., Fox, N. C., Wray, S., Oxtoby, N. P., & Arber, C. (2026). Unbiased data-driven analysis of five amyloid-beta peptides for biomarker investigations in familial Alzheimer's disease. Brain communications, 8(2), fcag105. https://doi.org/10.1093/braincomms/fcag105

BibTeX

@article{llorentesaguer2026unbiased,
author = {Llorente-Saguer, Isaac and Gabriele, Rebecca and Bradshaw, Teisha Y and Leckey, Claire A and Belder, Christopher R S and Chávez-Gutiérrez, Lucía and de Silva, Rohan and Fox, Nick C and Wray, Selina and Oxtoby, Neil P and Arber, Charles},
title = {{Unbiased data-driven analysis of five amyloid-beta peptides for biomarker investigations in familial Alzheimer's disease}},
journal = {Brain communications},
year = {2026},
month = mar,
volume = {8},
number = {2},
pages = {fcag105},
publisher = {Oxford University Press},
issn = {2632-1297},
doi = {10.1093/braincomms/fcag105},
url = {https://doi.org/10.1093/braincomms/fcag105},
pmid = {42022292},
pmcid = {PMC13096935}
}

RIS

TY - JOUR
AU - Llorente-Saguer, Isaac
AU - Gabriele, Rebecca
AU - Bradshaw, Teisha Y
AU - Leckey, Claire A
AU - Belder, Christopher R S
AU - Chávez-Gutiérrez, Lucía
AU - de Silva, Rohan
AU - Fox, Nick C
AU - Wray, Selina
AU - Oxtoby, Neil P
AU - Arber, Charles
TI - Unbiased data-driven analysis of five amyloid-beta peptides for biomarker investigations in familial Alzheimer's disease
T2 - Brain communications
J2 - Brain Commun
PY - 2026
DA - 2026/03/22
VL - 8
IS - 2
SP - fcag105
SN - 2632-1297
PB - Oxford University Press
DO - 10.1093/braincomms/fcag105
UR - https://doi.org/10.1093/braincomms/fcag105
LA - en
ER -

CSL-JSON

{
"id": "10.1093/braincomms/fcag105",
"type": "article-journal",
"title": "Unbiased data-driven analysis of five amyloid-beta peptides for biomarker investigations in familial Alzheimer's disease",
"container-title": "Brain communications",
"author": [
{
"family": "Llorente-Saguer",
"given": "Isaac"
},
{
"family": "Gabriele",
"given": "Rebecca"
},
{
"family": "Bradshaw",
"given": "Teisha Y"
},
{
"family": "Leckey",
"given": "Claire A"
},
{
"family": "Belder",
"given": "Christopher R S"
},
{
"family": "Chávez-Gutiérrez",
"given": "Lucía"
},
{
"family": "de Silva",
"given": "Rohan"
},
{
"family": "Fox",
"given": "Nick C"
},
{
"family": "Wray",
"given": "Selina"
},
{
"family": "Oxtoby",
"given": "Neil P"
},
{
"family": "Arber",
"given": "Charles"
}
],
"container-title-short": "Brain Commun",
"volume": "8",
"issue": "2",
"page": "fcag105",
"DOI": "10.1093/braincomms/fcag105",
"PMID": "42022292",
"PMCID": "PMC13096935",
"ISSN": "2632-1297",
"publisher": "Oxford University Press",
"URL": "https://doi.org/10.1093/braincomms/fcag105",
"language": "en",
"issued": {
"date-parts": [
[
2026,
3,
22
]
]
}
}

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.1371/journal.pbio.3002727 [code]
Selective suppression of oligodendrocyte-derived amyloid beta rescues neuronal dysfunction in Alzheimer's disease.
Journal: PLoS biology
In common: Alzheimer's / dementia, 2 references, 2 authors
[2] doi:10.1126/sciadv.adk1296 [code]
The annotation of &lt;i&gt;GBA1&lt;/i&gt; has been concealed by its protein-coding pseudogene &lt;i&gt;GBAP1&lt;/i&gt;.
Journal: Science advances
In common: tidyverse, 1 reference, 2 authors
[3] doi:10.1093/braincomms/fcaf497 [code]
Enhanced monitoring of Alzheimer's disease brain atrophy using composite value ratios of volumes.
Journal: Brain communications
In common: lme4, tidyverse, Alzheimer's / dementia, 2 authors
[4] doi:10.1186/s13195-024-01659-6 [code]
Integrative multiomics reveals common endotypes across PSEN1, PSEN2, and APP mutations in familial Alzheimer's disease.
Journal: Alzheimer's research & therapy
In common: tidyverse, Alzheimer's / dementia, 6 references
[5] doi:10.1002/advs.202514783
Autophagy Activators Normalize Aberrant Tau Proteostasis and Rescue Synapses in Human Familial Alzheimer's Disease iPSC-Derived Cortical Organoids.
Journal: Advanced science (Weinheim, Baden-Wurttemberg, Germany)
In common: Alzheimer's / dementia, 4 references
[6] doi:10.1021/acs.analchem.3c05557
Integrative Single-Plaque Analysis Reveals Signature Aβ and Lipid Profiles in the Alzheimer's Brain.
Journal: Analytical chemistry
In common: Alzheimer's / dementia, 4 references
[7] doi:10.1038/s41380-026-03686-1 [code]
Early oligodendrocyte dysfunction signature in Alzheimer's disease: Insights from DNA methylomics and transcriptomics.
Journal: Molecular psychiatry
In common: tidyverse, Alzheimer's / dementia, author Rohan de Silva
[8] doi:10.1101/2025.08.28.672821 [code]
Early oligodendrocyte dysfunction signature in Alzheimer’s disease: Insights from DNA methylomics and transcriptomics
Journal: bioRxiv (preprint)
In common: tidyverse, Alzheimer's / dementia, author Rohan de Silva
[9] doi:10.3390/cells12030420
The Breakthroughs and Caveats of Using Human Pluripotent Stem Cells in Modeling Alzheimer's Disease.
Journal: Cells
In common: Alzheimer's / dementia, 4 references
[10] doi:10.1093/brain/awad232 [code]
A data-driven study of Alzheimer's disease related amyloid and tau pathology progression.
Journal: Brain : a journal of neurology
In common: Alzheimer's / dementia, 1 reference, author Neil P. Oxtoby

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.1093/braincomms/fcag105.svg)](https://openscicode.org/badge/doi/10.1093/braincomms/fcag105)
<!-- oscr-badge:end -->
reStructuredText
.. oscr-badge:start

.. image:: https://openscicode.org/badge/doi/10.1093/braincomms/fcag105.svg
   :target: https://openscicode.org/badge/doi/10.1093/braincomms/fcag105
   :alt: OSCR

.. oscr-badge:end
HTML
<!-- oscr-badge:start -->
<a href="https://openscicode.org/badge/doi/10.1093/braincomms/fcag105"><img src="https://openscicode.org/badge/doi/10.1093/braincomms/fcag105.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.