From e8d0a7f1aea50bd06442ad0e579ead446f5a87d8 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 7 Aug 2025 17:00:53 +0200 Subject: [PATCH 01/30] Refactored code GenerateViolinPlots --- DIMS/GenerateViolinPlots.R | 649 +++++------------- DIMS/GenerateViolinPlots.nf | 5 +- DIMS/export/generate_violin_plots_functions.R | 572 +++++++++++++++ 3 files changed, 763 insertions(+), 463 deletions(-) create mode 100644 DIMS/export/generate_violin_plots_functions.R diff --git a/DIMS/GenerateViolinPlots.R b/DIMS/GenerateViolinPlots.R index 263c7201..2e6cb885 100644 --- a/DIMS/GenerateViolinPlots.R +++ b/DIMS/GenerateViolinPlots.R @@ -1,14 +1,3 @@ -# For untargeted metabolomics, this tool calculates probability scores for -# metabolic disorders. In addition, it provides visual support with violin plots -# of the DIMS measurements for the lab specialists. -# Input needed: -# 1. Excel file in which metabolites are listed with their intensities for -# controls (with C in samplename) and patients (with P in samplename) and their -# corresponding Z-scores. -# 2. All files from github: https://github.com/UMCUGenetics/DIMS - -## adapted from 15-dIEM_violin.R - # load packages suppressPackageStartupMessages(library("dplyr")) library(reshape2) @@ -21,462 +10,202 @@ library(stringr) cmd_args <- commandArgs(trailingOnly = TRUE) run_name <- cmd_args[1] -scripts_dir <- cmd_args[2] -z_score <- as.numeric(cmd_args[3]) -path_metabolite_groups <- cmd_args[4] -file_ratios_metabolites <- cmd_args[5] -file_expected_biomarkers_iem <- cmd_args[6] -file_explanation <- cmd_args[7] -file_isomers <- cmd_args[8] - -if (z_score == 1){ - # path: output folder for dIEM and violin plots - output_dir <- "./" - - file.copy(file_isomers, output_dir) - - # load functions - source(paste0(scripts_dir, "check_same_samplename.R")) - source(paste0(scripts_dir, "prepare_data.R")) - source(paste0(scripts_dir, "prepare_data_perpage.R")) - source(paste0(scripts_dir, "prepare_toplist.R")) - source(paste0(scripts_dir, "create_violin_plots.R")) - source(paste0(scripts_dir, "prepare_alarmvalues.R")) - source(paste0(scripts_dir, "output_helix.R")) - source(paste0(scripts_dir, "get_patient_data_to_helix.R")) - source(paste0(scripts_dir, "add_lab_id_and_onderzoeksnummer.R")) - source(paste0(scripts_dir, "is_diagnostic_patient.R")) - - # number of diseases that score highest in algorithm to plot - top_nr_iem <- 5 - # probability score cut-off for plotting the top diseases - threshold_iem <- 5 - # z-score cutoff of axis on the left for top diseases - ratios_cutoff <- -5 - # number of violin plots per page in PDF - nr_plots_perpage <- 20 - - # binary variable: run function, yes(1) or no(0) - if (z_score == 1) { - algorithm <- ratios <- violin <- 1 - } else { - algorithm <- ratios <- violin <- 0 - } - # are the sample names headers on row 1 or row 2 in the DIMS excel? (default 1) - header_row <- 1 - # column name where the data starts (default B) - col_start <- "B" - zscore_cutoff <- 5 - xaxis_cutoff <- 20 - protocol_name <- "DIMS_PL_DIAG" - - #### STEP 1: Preparation #### - # in: run_name, path_dims_file, header_row ||| out: output_dir, DIMS - - # load outlist instead of excel file - load("outlist.RData") - - # save outlist as dims_xls, will be changed during refactor - dims_xls <- outlist - rm(outlist) - - #### STEP 2: Edit DIMS data ##### - # in: dims_xls ||| out: Data, nr_contr, nr_pat - # Input: the xlsx file that comes out of the pipeline with format: - # [plots] [C] [P] [summary columns] [C_Zscore] [P_Zscore] - # Output: "_CSV.csv" file that is suited for the algorithm in shiny. - - # Determine the number of Contols and Patients in column names: - nr_contr <- length(grep("C", names(dims_xls))) / 2 - nr_pat <- length(grep("P", names(dims_xls))) / 2 - # total number of samples - nrsamples <- nr_contr + nr_pat - # check whether the number of intensity columns equals the number of Zscore columns - if (nr_contr + nr_pat != length(grep("_Zscore", names(dims_xls)))) { - cat("\n**** Error: there aren't as many intensities listed as Zscores") - } - cat(paste0("\n\n------------\n", nr_contr, " controls \n", nr_pat, " patients\n------------\n\n")) - - # Move the columns HMDB_code and HMDB_name to the beginning. - hmdb_info_cols <- c(which(colnames(dims_xls) == "HMDB_code"), which(colnames(dims_xls) == "HMDB_name")) - other_cols <- seq_along(1:ncol(dims_xls))[-hmdb_info_cols] - dims_xls_copy <- dims_xls[, c(hmdb_info_cols, other_cols)] - # Remove the columns from 'name' to 'pathway' - from_col <- which(colnames(dims_xls_copy) == "name") - to_col <- which(colnames(dims_xls_copy) == "pathway") - dims_xls_copy <- dims_xls_copy[, -c(from_col:to_col)] - # in case the excel had an empty "plots" column, remove it - if ("plots" %in% colnames(dims_xls_copy)) { - dims_xls_copy <- dims_xls_copy[, -grep("plots", colnames(dims_xls_copy))] - } - # Rename columns - names(dims_xls_copy) <- gsub("avg.ctrls", "Mean_controls", names(dims_xls_copy)) - names(dims_xls_copy) <- gsub("sd.ctrls", "SD_controls", names(dims_xls_copy)) - names(dims_xls_copy) <- gsub("HMDB_code", "HMDB.code", names(dims_xls_copy)) - names(dims_xls_copy) <- gsub("HMDB_name", "HMDB.name", names(dims_xls_copy)) - - # intensity columns and mean and standard deviation of controls - numeric_cols <- c(3:ncol(dims_xls_copy)) - # make sure all values are numeric - dims_xls_copy[, numeric_cols] <- sapply(dims_xls_copy[, numeric_cols], as.numeric) - - if (exists("dims_xls_copy") & (length(dims_xls_copy) < length(dims_xls))) { - cat("\n### Step 2 # Edit dims data is done.\n") - } else { - cat("\n**** Error: Could not execute step 2 \n") - } - - #### STEP 3: Calculate ratios of intensities for metabolites #### - # in: ratios, file_ratios_metabolites, dims_xls_copy, nr_contr, nr_pat ||| out: Zscore (+file) - # This script loads the file with Ratios (file_ratios_metabolites) and calculates - # the ratios of the intensities of the given metabolites. It also calculates - # Zs-cores based on the avg and sd of the ratios of the controls. - - # Input: dataframe with intenstities and Zscores of controls and patients: - # [HMDB.code] [HMDB.name] [C] [P] [Mean_controls] [SD_controls] [C_Zscore] [P_Zscore] - - # Output: "_CSV.csv" file that is suited for the algorithm, with format: - # "_Ratios_CSV.csv" file, same file as above, but with ratio rows added. - - if (ratios == 1) { - cat(paste0("\nloading ratios file:\n -> ", file_ratios_metabolites, "\n")) - ratio_input <- read.csv(file_ratios_metabolites, sep = ";", stringsAsFactors = FALSE) - - # Prepare empty data frame to fill with ratios - ratio_list <- setNames(data.frame(matrix( - ncol = ncol(dims_xls_copy), - nrow = nrow(ratio_input) - )), colnames(dims_xls_copy)) - ratio_list <- as.data.frame(ratio_list) - - # put HMDB info into first two columns of ratio_list - ratio_list[, 1:2] <- ratio_input[, 1:2] - - # look for intensity columns (exclude Zscore columns) - control_cols <- grep("C", colnames(ratio_list)[1:which(colnames(ratio_list) == "Mean_controls")]) - patient_cols <- grep("P", colnames(ratio_list)[1:which(colnames(ratio_list) == "Mean_controls")]) - intensity_cols <- c(control_cols, patient_cols) - # calculate each of the ratios of intensities - for (ratio_index in 1:nrow(ratio_input)) { - ratio_numerator <- ratio_input[ratio_index, "HMDB_numerator"] - ratio_numerator <- strsplit(ratio_numerator, "plus")[[1]] - ratio_denominator <- ratio_input[ratio_index, "HMDB_denominator"] - ratio_denominator <- strsplit(ratio_denominator, "plus")[[1]] - # find these HMDB IDs in dataset. Could be a sum of multiple metabolites - sel_denominator <- sel_numerator <- c() - for (numerator_index in 1:length(ratio_numerator)) { - sel_numerator <- c(sel_numerator, which(dims_xls_copy[, "HMDB.code"] == ratio_numerator[numerator_index])) - } - for (denominator_index in 1:length(ratio_denominator)) { - # special case for sum of metabolites (dividing by one) - if (ratio_denominator[denominator_index] != "one") { - sel_denominator <- c(sel_denominator, which(dims_xls_copy[, "HMDB.code"] == ratio_denominator[denominator_index])) - } - } - # calculate ratio - if (ratio_denominator[denominator_index] != "one") { - ratio_list[ratio_index, intensity_cols] <- apply(dims_xls_copy[sel_numerator, intensity_cols], 2, sum) / - apply(dims_xls_copy[sel_denominator, intensity_cols], 2, sum) - } else { - # special case for sum of metabolites (dividing by one) - ratio_list[ratio_index, intensity_cols] <- apply(dims_xls_copy[sel_numerator, intensity_cols], 2, sum) - } - # calculate log of ratio - ratio_list[ratio_index, intensity_cols] <- log2(ratio_list[ratio_index, intensity_cols]) - } - - # Calculate means and SD's of the calculated ratios for Controls - ratio_list[, "Mean_controls"] <- apply(ratio_list[, control_cols], 1, mean) - ratio_list[, "SD_controls"] <- apply(ratio_list[, control_cols], 1, sd) - - # Calc z-scores with the means and SD's of Controls - zscore_cols <- grep("Zscore", colnames(ratio_list)) - for (sample_index in 1:length(zscore_cols)) { - zscore_col <- zscore_cols[sample_index] - # matching intensity column - int_col <- intensity_cols[sample_index] - # test on column names - if (check_same_samplename(colnames(ratio_list)[int_col], colnames(ratio_list)[zscore_col])) { - # calculate Z-scores - ratio_list[, zscore_col] <- (ratio_list[, int_col] - ratio_list[, "Mean_controls"]) / ratio_list[, "SD_controls"] - } +export_scripts_dir <- cmd_args[2] +path_metabolite_groups <- cmd_args[3] +file_ratios_metabolites <- cmd_args[4] +file_expected_biomarkers_iem <- cmd_args[5] +file_explanation <- cmd_args[6] + +# load functions +source(paste0(export_scripts_dir, "generate_violin_plots_functions.R")) +# load dataframe with intensities and Z-scores for all samples +intensities_zscore_df <- get(load("outlist.RData")) +# read input files +ratios_metabs_df <- read.csv(file_ratios_metabolites, sep = ";", stringsAsFactors = FALSE) +expected_biomarkers_df <- read.csv(file_expected_biomarkers_iem, sep = ";", stringsAsFactors = FALSE) +explanation_violin_plot <- readLines(file_explanation) + + +## Set global variables +output_dir <- "./" # path: output folder for dIEM and violin plots +top_number_iem_diseases <- 5 # number of diseases that score highest in algorithm to plot +threshold_iem <- 5 # probability score cut-off for plotting the top diseases +ratios_cutoff <- -5 # z-score cutoff of axis on the left for top diseases +nr_plots_perpage <- 20 # number of violin plots per page in PDF +zscore_cutoff <- 5 +xaxis_cutoff <- 20 +protocol_name <- "DIMS_PL_DIAG" + +# Remove columns, move HMDB_code & HMDB_name column to the front, change intensity columns to numeric +intensities_zscore_df <- intensities_zscore_df %>% + select(-c(plots, HMDB_name_all, HMDB_ID_all, sec_HMDB_ID, HMDB_key, sec_HMBD_ID_rlvnc, name, + relevance, descr, origin, fluids, tissue, disease, pathway, nr_ctrls)) %>% + relocate(c(HMDB_code, HMDB_name)) %>% + rename(mean_controls = avg_ctrls, sd_controls = sd_ctrls) %>% + mutate(across(!c(HMDB_name, HMDB_code), as.numeric)) + +# Get the controls and patient IDs, select the intensity columns +controls <- colnames(intensities_zscore_df)[grepl("^C", colnames(intensities_zscore_df)) & + !grepl("_Zscore$", colnames(intensities_zscore_df))] +control_intensities_cols_index <- which(colnames(intensities_zscore_df) %in% controls) +nr_of_controls <- length(controls) + +patients <- colnames(intensities_zscore_df)[grepl("^P", colnames(intensities_zscore_df)) & + !grepl("_Zscore$", colnames(intensities_zscore_df))] +patient_intensities_cols_index <- which(colnames(intensities_zscore_df) %in% patients) +nr_of_patients <- length(patients) + +intensity_cols_index <- c(control_intensities_cols_index, patient_intensities_cols_index) +intensity_cols <- colnames(intensities_zscore_df)[intensity_cols_index] + +#### Calculate ratios of intensities for metabolites #### +# Prepare empty data frame to fill with ratios +ratio_zscore_df <- data.frame(matrix( + ncol = ncol(intensities_zscore_df), + nrow = nrow(ratios_metabs_df) +)) +colnames(ratio_zscore_df) <- colnames(intensities_zscore_df) + +# put HMDB info into first two columns of ratio_zscore_df +ratio_zscore_df$HMDB_code <- ratios_metabs_df$HMDB.code +ratio_zscore_df$HMDB_name <- ratios_metabs_df$Ratio_name + +for (row_index in 1:nrow(ratios_metabs_df)) { + numerator_intensities <- get_intentities_for_ratios(ratios_metabs_df, row_index, + intensities_zscore_df, "HMDB_numerator", intensity_cols) + denominator_intensities <- get_intentities_for_ratios(ratios_metabs_df, row_index, + intensities_zscore_df, "HMDB_denominator", intensity_cols) + # calculate intensity ratios + ratio_zscore_df[row_index, intensity_cols_index] <- log2(numerator_intensities / denominator_intensities) +} +# Calculate means and SD's of the calculated ratios for Controls +ratio_zscore_df[, "mean_controls"] <- apply(ratio_zscore_df[, control_intensities_cols_index], 1, mean) +ratio_zscore_df[, "sd_controls"] <- apply(ratio_zscore_df[, control_intensities_cols_index], 1, sd) + +# Calculate Zscores for the ratios +samples_zscore_columns <- get_zscore_columns(colnames(intensities_zscore_df), intensity_cols) +ratio_zscore_df[, samples_zscore_columns] <- (ratio_zscore_df[, intensity_cols] - ratio_zscore_df[, "mean_controls"]) / + ratio_zscore_df[, "sd_controls"] + +intensities_zscore_ratios_df <- rbind(intensities_zscore_df, ratio_zscore_df) + +# for debugging: +save(intensities_zscore_ratios_df, file = paste0(output_dir, "/outlist_with_ratios.RData")) + +# Select only the cols with zscores of the patients +zscore_patients_df <- intensities_zscore_ratios_df %>% select(HMDB_code, HMDB_name, any_of(paste0(patients, "_Zscore"))) +zscore_controls_df <- intensities_zscore_ratios_df %>% select(HMDB_code, HMDB_name, any_of(paste0(controls, "_Zscore"))) + +#### Make violin plots ##### +# preparation +colnames(zscore_patients_df) <- gsub("_Zscore", "", colnames(zscore_patients_df)) +colnames(zscore_controls_df) <- gsub("_Zscore", "", colnames(zscore_controls_df)) + +expected_biomarkers_df <- expected_biomarkers_df %>% rename(HMDB_code = HMDB.code, HMDB_name = Metabolite) + +expected_biomarkers_info <- expected_biomarkers_df %>% + select(c(Disease, HMDB_code, HMDB_name)) %>% + distinct(Disease, HMDB_code, .keep_all = TRUE) + +metabolite_dirs <- list.files(path = path_metabolite_groups, full.names = FALSE, recursive = FALSE) +for (metabolite_dir in metabolite_dirs) { + # create a directory for the output PDFs + pdf_dir <- paste(output_dir, metabolite_dir, sep = "/") + dir.create(pdf_dir, showWarnings = FALSE) + + metab_list_all <- get_list_metabolites(paste(path_metabolite_groups, metabolite_dir, sep = "/")) + + # prepare list of metabolites; max nr_plots_perpage on one page + metab_interest_sorted <- combine_metab_info_zscores(metab_list_all, zscore_patients_df) + metab_interest_controls <- combine_metab_info_zscores(metab_list_all, zscore_controls_df) + metab_perpage <- prepare_data_perpage(metab_interest_sorted, metab_interest_controls, + nr_plots_perpage, nr_of_patients, nr_of_controls) + + # for Diagnostics metabolites to be saved in Helix + if(grepl("Diagnost", pdf_dir)) { + # get table that combines DIMS results with stofgroepen/Helix table + dims_helix_table <- get_patient_data_to_helix(metab_interest_sorted, metab_list_all) + + # check if run contains Diagnostics patients (e.g. "P2024M"), not for research runs + if(any(is_diagnostic_patient(dims_helix_table$Sample))){ + # get output file for Helix + output_helix <- output_for_helix(protocol_name, dims_helix_table) + # write output to file + path_helixfile <- paste0(output_dir, "output_Helix_", run_name,".csv") + write.csv(output_helix, path_helixfile, quote = F, row.names = F) } - - # Add rows of the ratio hmdb codes to the data of zscores from the pipeline. - dims_xls_ratios <- rbind(ratio_list, dims_xls_copy) - - # Edit the DIMS output Zscores of all patients in format: - # HMDB_code patientname1 patientname2 - names(dims_xls_ratios) <- gsub("HMDB.code", "HMDB_code", names(dims_xls_ratios)) - names(dims_xls_ratios) <- gsub("HMDB.name", "HMDB_name", names(dims_xls_ratios)) - - # for debugging: - write.table(dims_xls_ratios, file = paste0(output_dir, "/ratios.txt"), sep = "\t") - - # Select only the cols with zscores of the patients - zscore_patients <- dims_xls_ratios[, c(1, 2, zscore_cols[grep("P", colnames(dims_xls_ratios)[zscore_cols])])] - # Select only the cols with zscores of the controls - zscore_controls <- dims_xls_ratios[, c(1, 2, zscore_cols[grep("C", colnames(dims_xls_ratios)[zscore_cols])])] - } - - #### STEP 4: Run the IEM algorithm ######### - # in: algorithm, file_expected_biomarkers_iem, zscore_patients ||| out: prob_score (+file) - # algorithm taken from DOI: 10.3390/ijms21030979 - - if (algorithm == 1) { - # Load data - cat(paste0("\nloading expected file:\n -> ", file_expected_biomarkers_iem, "\n")) - expected_biomarkers <- read.csv(file_expected_biomarkers_iem, sep = ";", stringsAsFactors = FALSE) - # modify column names - names(expected_biomarkers) <- gsub("HMDB.code", "HMDB_code", names(expected_biomarkers)) - names(expected_biomarkers) <- gsub("Metabolite", "HMDB_name", names(expected_biomarkers)) - - # prepare dataframe scaffold rank_patients - rank_patients <- zscore_patients - # Fill df rank_patients with the ranks for each patient - for (patient_index in 3:ncol(zscore_patients)) { - # number of positive zscores in patient - pos <- sum(zscore_patients[, patient_index] > 0) - # sort the column on zscore; NB: this sorts the entire object, not just one column - rank_patients <- rank_patients[order(-rank_patients[patient_index]), ] - # Rank all positive zscores highest to lowest - rank_patients[1:pos, patient_index] <- as.numeric(ordered(-rank_patients[1:pos, patient_index])) - # Rank all negative zscores lowest to highest - rank_patients[(pos + 1):nrow(rank_patients), patient_index] <- as.numeric(ordered(rank_patients[(pos + 1): - nrow(rank_patients), patient_index])) - } - - # Calculate metabolite score, using the dataframes with only values, and later add the cols without values (1&2). - expected_zscores <- merge(x = expected_biomarkers, y = zscore_patients, by.x = c("HMDB_code"), by.y = c("HMDB_code")) - expected_zscores_original <- expected_zscores - - # determine which columns contain Z-scores and which contain disease info - select_zscore_cols <- grep("_Zscore", colnames(expected_zscores)) - select_info_cols <- 1:(min(select_zscore_cols) - 1) - # set some zscores to zero - select_incr_indisp <- which(expected_zscores$Change == "Increase" & expected_zscores$Dispensability == "Indispensable") - expected_zscores[select_incr_indisp, select_zscore_cols] <- lapply(expected_zscores[select_incr_indisp, - select_zscore_cols], function(x) ifelse (x <= 1.6, 0, x)) - select_decr_indisp <- which(expected_zscores$Change == "Decrease" & expected_zscores$Dispensability == "Indispensable") - expected_zscores[select_decr_indisp, select_zscore_cols] <- lapply(expected_zscores[select_decr_indisp, - select_zscore_cols], function(x) ifelse (x >= -1.2, 0, x)) - - # calculate rank score: - expected_ranks <- merge(x = expected_biomarkers, y = rank_patients, by.x = c("HMDB_code"), by.y = c("HMDB_code")) - rank_scores <- expected_zscores[order(expected_zscores$HMDB_code), select_zscore_cols] / - (expected_ranks[order(expected_ranks$HMDB_code), select_zscore_cols] * 0.9) - # combine disease info with rank scores - expected_metabscore <- cbind(expected_ranks[order(expected_zscores$HMDB_code), select_info_cols], rank_scores) - - # multiply weight score and rank score - weight_score <- expected_zscores - weight_score[, select_zscore_cols] <- expected_metabscore$Total_Weight * expected_metabscore[, select_zscore_cols] - - # sort table on Disease and Absolute_Weight - weight_score <- weight_score[order(weight_score$Disease, weight_score$Absolute_Weight, decreasing = TRUE), ] - - # select columns to check duplicates - dup <- weight_score[, c("Disease", "M.z")] - uni <- weight_score[!duplicated(dup) | !duplicated(dup, fromLast = FALSE), ] - - # calculate probability score - prob_score <- aggregate(uni[, select_zscore_cols], uni["Disease"], sum) - - # list of all diseases that have at least one metabolite Zscore at 0 - for (patient_index in 2:ncol(prob_score)) { - patient_zscore_colname <- colnames(prob_score)[patient_index] - matching_colname_expected <- which(colnames(expected_zscores) == patient_zscore_colname) - # determine which Zscores are 0 for this patient - zscores_zero <- which(expected_zscores[, matching_colname_expected] == 0) - # get Disease for these - disease_zero <- unique(expected_zscores[zscores_zero, "Disease"]) - # set the probability score of these diseases to 0 - prob_score[which(prob_score$Disease %in% disease_zero), patient_index] <- 0 - } - - # determine disease rank per patient - disease_rank <- prob_score - # rank diseases in decreasing order - disease_rank[2:ncol(disease_rank)] <- lapply(2:ncol(disease_rank), function(x) - as.numeric(ordered(-disease_rank[1:nrow(disease_rank), x]))) - # modify column names, Zscores have now been converted to probability scores - colnames(prob_score) <- gsub("_Zscore", "_prob_score", colnames(prob_score)) - colnames(disease_rank) <- gsub("_Zscore", "", colnames(disease_rank)) - - # Create conditional formatting for output Excel sheet. Colors according to values. - wb <- createWorkbook() - addWorksheet(wb, "Probability Scores") - writeData(wb, "Probability Scores", prob_score) - conditionalFormatting(wb, "Probability Scores", cols = 2:ncol(prob_score), rows = 1:nrow(prob_score), - type = "colourScale", style = c("white", "#FFFDA2", "red"), rule = c(1, 10, 100)) - saveWorkbook(wb, file = paste0(output_dir, "/dIEM_algoritme_output_", run_name, ".xlsx"), overwrite = TRUE) - # check whether prob_score df exists and has expected dimensions. - if (exists("expected_biomarkers") & (length(disease_rank) == length(prob_score))) { - cat("\n### Step 4 # Running the IEM algorithm is done.\n\n") + + # make violin plots per patient + for (patient_id in patients) { + # for category Diagnostics, make list of metabolites that exceed alarm values for this patient + # for category Other, make list of top highest and lowest Z-scores for this patient + if (grepl("Diagnost", pdf_dir)) { + top_metabs_patient <- prepare_alarmvalues(patient_id, dims_helix_table) } else { - cat("\n**** Error: Could not run IEM algorithm. Check if path to expected_biomarkers csv-file is correct. \n") + top_metabs_patient <- prepare_toplist(patient_id, zscore_patients) } - rm(wb) + # generate normal violin plots + create_pdf_violin_plots(pdf_dir, patient_id, metab_perpage, top_metabs_patient, explanation_violin_plot) } - #### STEP 5: Make violin plots ##### - # in: algorithm / zscore_patients, violin, nr_contr, nr_pat, Data, path_textfiles, zscore_cutoff, xaxis_cutoff, - # top_diseases, top_metab, output_dir ||| out: pdf file, Helix csv file - - if (violin == 1) { - - # preparation - zscore_patients_copy <- zscore_patients - colnames(zscore_patients) <- gsub("_Zscore", "", colnames(zscore_patients)) - colnames(zscore_controls) <- gsub("_Zscore", "", colnames(zscore_controls)) - - # Make patient list for violin plots - patient_list <- names(zscore_patients)[-c(1, 2)] - - # from table expected_biomarkers, choose selected columns - select_columns <- c("Disease", "HMDB_code", "HMDB_name") - #select_col_nrs <- which(colnames(expected_biomarkers) %in% select_columns) - expected_biomarkers_select <- expected_biomarkers %>% select(all_of(select_columns)) - # remove duplicates - expected_biomarkers_select <- expected_biomarkers_select[!duplicated(expected_biomarkers_select[, c(1, 2)]), ] - - # load file with explanatory information to be included in PDF. - explanation <- readLines(file_explanation) - - # first step: normal violin plots - # Find all text files in the given folder, which contain metabolite lists of which - # each file will be a page in the pdf with violin plots. - # Make a PDF file for each of the categories in metabolite_dirs - metabolite_dirs <- list.files(path = path_metabolite_groups, full.names = FALSE, recursive = FALSE) - for (metabolite_dir in metabolite_dirs) { - # create a directory for the output PDFs - pdf_dir <- paste(output_dir, metabolite_dir, sep = "/") - dir.create(pdf_dir, showWarnings = FALSE) - cat("making plots in category:", metabolite_dir, "\n") - - # get a list of all metabolite files - metabolite_files <- list.files(path = paste(path_metabolite_groups, metabolite_dir, sep = "/"), - pattern = "*.txt", full.names = FALSE, recursive = FALSE) - # put all metabolites into one list - metab_list_all <- list() - metab_list_names <- c() - cat("making plots from the input files:") - # open the text files and add each to a list of dataframes (metab_list_all) - for (file_index in seq_along(metabolite_files)) { - infile <- metabolite_files[file_index] - metab_list <- read.table(paste(path_metabolite_groups, metabolite_dir, infile, sep = "/"), - sep = "\t", header = TRUE, quote = "") - # put into list of all lists - metab_list_all[[file_index]] <- metab_list - metab_list_names <- c(metab_list_names, strsplit(infile, ".txt")[[1]][1]) - cat(paste0("\n", infile)) - } - # include list of classes in metabolite list - names(metab_list_all) <- metab_list_names - - # prepare list of metabolites; max nr_plots_perpage on one page - metab_interest_sorted <- prepare_data(metab_list_all, zscore_patients) - metab_interest_controls <- prepare_data(metab_list_all, zscore_controls) - metab_perpage <- prepare_data_perpage(metab_interest_sorted, metab_interest_controls, nr_plots_perpage, nr_pat, nr_contr) - - # for Diagnostics metabolites to be saved in Helix - if(grepl("Diagnost", pdf_dir)) { - # get table that combines DIMS results with stofgroepen/Helix table - dims_helix_table <- get_patient_data_to_helix(metab_interest_sorted, metab_list_all) - - # check if run contains Diagnostics patients (e.g. "P2024M"), not for research runs - if(any(is_diagnostic_patient(dims_helix_table$Patient))){ - # get output file for Helix - output_helix <- output_for_helix(protocol_name, dims_helix_table) - # write output to file - path_helixfile <- paste0(output_dir, "/output_Helix_", run_name,".csv") - write.csv(output_helix, path_helixfile, quote = F, row.names = F) - } - } - - # make violin plots per patient - for (pt_nr in 1:length(patient_list)) { - pt_name <- patient_list[pt_nr] - # for category Diagnostics, make list of metabolites that exceed alarm values for this patient - # for category Other, make list of top highest and lowest Z-scores for this patient - if (grepl("Diagnost", pdf_dir)) { - top_metab_pt <- prepare_alarmvalues(pt_name, dims_helix_table) - } else { - top_metab_pt <- prepare_toplist(pt_name, zscore_patients) - } - - # generate normal violin plots - create_violin_plots(pdf_dir, pt_name, metab_perpage, top_metab_pt) - - } - - } - - # Second step: dIEM plots in separate directory - diem_plot_dir <- paste(output_dir, "dIEM_plots", sep = "/") - dir.create(diem_plot_dir) - - # Select the metabolites that are associated with the top highest scoring IEM, for each patient - # disease_rank is from step 4: the dIEM algorithm. The lower the value, the more likely. - for (pt_nr in 1:length(patient_list)) { - pt_name <- patient_list[pt_nr] - # get top diseases for this patient - pt_colnr <- which(colnames(disease_rank) == pt_name) - pt_top_indices <- which(disease_rank[, pt_colnr] <= top_nr_iem) - pt_iems <- disease_rank[pt_top_indices, "Disease"] - pt_top_iems <- pt_prob_score_top_iems <- c() - for (single_iem in pt_iems) { - # get the probability score - prob_score_iem <- prob_score[which(prob_score$Disease == single_iem), pt_colnr] - # use only diseases for which probability score is above threshold - if (prob_score_iem >= threshold_iem) { - pt_top_iems <- c(pt_top_iems, single_iem) - pt_prob_score_top_iems <- c(pt_prob_score_top_iems, prob_score_iem) - } - } - - # prepare data for plotting dIEM violin plots - # If prob_score_top_iem is an empty list, don't make a plot - if (length(pt_top_iems) > 0) { - # Sorting from high to low, both prob_score_top_iems and pt_top_iems. - pt_prob_score_order <- order(-pt_prob_score_top_iems) - pt_prob_score_top_iems <- round(pt_prob_score_top_iems, 1) - pt_prob_score_top_iem_sorted <- pt_prob_score_top_iems[pt_prob_score_order] - pt_top_iem_sorted <- pt_top_iems[pt_prob_score_order] - # getting metabolites for each top_iem disease exactly like in metab_list_all - metab_iem_all <- list() - metab_iem_names <- c() - for (single_iem_index in 1:length(pt_top_iem_sorted)) { - single_iem <- pt_top_iem_sorted[single_iem_index] - single_prob_score <- pt_prob_score_top_iem_sorted[single_iem_index] - select_rows <- which(expected_biomarkers_select$Disease == single_iem) - metab_list <- expected_biomarkers_select[select_rows, ] - metab_iem_names <- c(metab_iem_names, paste0(single_iem, ", probability score ", single_prob_score)) - metab_list <- metab_list[, -1] - metab_iem_all[[single_iem_index]] <- metab_list - } - # put all metabolites into one list - names(metab_iem_all) <- metab_iem_names - - # get Zscore information from zscore_patients_copy, similar to normal violin plots - metab_iem_sorted <- prepare_data(metab_iem_all, zscore_patients_copy) - metab_iem_controls <- prepare_data(metab_iem_all, zscore_controls) - # make sure every page has 20 metabolites - diem_metab_perpage <- prepare_data_perpage(metab_iem_sorted, metab_iem_controls, nr_plots_perpage, nr_pat) - # add table of metabolites with increased or decreased Z-scores - top_metab_pt <- prepare_toplist(pt_name, zscore_patients) - - # generate dIEM violin plots - create_violin_plots(diem_plot_dir, pt_name, diem_metab_perpage, top_metab_pt) - - } else { - cat(paste0("\n\n**** This patient had no prob_scores higher than ", threshold_iem, ". - Therefore, this pdf was not made:\t ", pt_name, "_iem \n")) - } - - } +} +#### Run the IEM algorithm ######### +expected_biomarkers_df <- expected_biomarkers_df %>% rename(HMDB_code = HMDB.code, HMDB_name = Metabolite) + +diem_probability_score <- run_diem_algorithm(expected_biomarkers_df, zscore_patients_df, patients) + +save_prob_scores_to_Excel(diem_probability_score, output_dir, run_name) + + +#### Generate dIEM plots ######### +diem_plot_dir <- paste(output_dir, "dIEM_plots", sep = "/") +dir.create(diem_plot_dir) + +colnames(diem_probability_score) <- gsub("_Zscore", "", colnames(diem_probability_score)) +patient_no_iem <- c() + +for (patient_id in patients) { + # Select the top IEMs and filter on the IEM threshold + patient_top_iems_probs <- diem_probability_score %>% + select(c(Disease, !!sym(patient_id))) %>% + arrange(desc(!!sym(patient_id))) %>% + slice(1:top_number_iem_diseases) %>% + filter(!!sym(patient_id) >= threshold_iem) + + if (nrow(patient_top_iems_probs) > 0) { + top_iems <- patient_top_iems_probs %>% pull(Disease) + # Get the metabolites for each IEM and their probability + metabs_iems_names <- c() + metabs_iems <- lapply(top_iems, function(iem) { + iem_probablity <- patient_top_iems_probs %>% filter(Disease == iem) %>% pull(!!sym(patient_id)) + metabs_iems_names <- c(metabs_iems_names, paste0(iem, ", probability score ", iem_probablity)) + metab_iem <- expected_biomarkers_df %>% filter(Disease == iem) %>% select(HMDB_code, HMDB_name) + return(metab_iem) + }) + names(metabs_iems) <- metabs_iems_names + + # Get the Z-scores with metabolite information + metab_iem_sorted <- combine_metab_info_zscores(metabs_iems, zscore_patients_df) + metab_iem_controls <- combine_metab_info_zscores(metabs_iems, zscore_controls_df) + # Get a list of dataframes for each IEM + diem_metab_perpage <- prepare_data_perpage(metab_iem_sorted, metab_iem_controls, + nr_plots_perpage, nr_of_patients, nr_of_controls) + # Get a dataframe of the top metabolites + top_metabs_patient <- prepare_toplist(patient_id, zscore_patients_df) + + # Generate and save dIEM violin plots + create_pdf_violin_plots(diem_plot_dir, patient_id, diem_metab_perpage, top_metabs_patient, explanation_violin_plot) + + } else { + patient_no_iem <- c(patient_no_iem, patient_id) } } + +if (length(patient_no_iem) > 0) { + patient_no_iem <- c(paste0("The following patient(s) did not have dIEM probability scores higher than ", threshold_iem, " :"), + patient_no_iem) + write(file = paste0(output_dir, "missing_probability_scores.txt"), patient_no_iem) +} diff --git a/DIMS/GenerateViolinPlots.nf b/DIMS/GenerateViolinPlots.nf index 1c4b532d..ec65a2e9 100755 --- a/DIMS/GenerateViolinPlots.nf +++ b/DIMS/GenerateViolinPlots.nf @@ -18,11 +18,10 @@ process GenerateViolinPlots { script: """ - Rscript ${baseDir}/CustomModules/DIMS/GenerateViolinPlots.R $analysis_id $params.scripts_dir $params.zscore \ + Rscript ${baseDir}/CustomModules/DIMS/GenerateViolinPlots.R $analysis_id $params.export_scripts_dir \ $params.path_metabolite_groups \ $params.file_ratios_metabolites \ $params.file_expected_biomarkers_IEM \ - $params.file_explanation \ - $params.file_isomers + $params.file_explanation """ } diff --git a/DIMS/export/generate_violin_plots_functions.R b/DIMS/export/generate_violin_plots_functions.R new file mode 100644 index 00000000..b1855a7f --- /dev/null +++ b/DIMS/export/generate_violin_plots_functions.R @@ -0,0 +1,572 @@ +#' Getting the intensities for calculating ratio Z-scores +#' +#' @param ratios_metabs_df: dataframe with HMDB codes for the ratios (dataframe) +#' @param row_index: index of the row in the ratios_metabs_df (integer) +#' @param intensities_zscore_df: dataframe with intensities for each sample (dataframe) +#' @param fraction_side: either numerator or denominator, which side of the fraction (string) +#' @param intensity_cols: names of the columns that contain the intensities (string) +#' +#' @returns fraction_side_intensity: a vector of intensities (vector of integers) +get_intentities_for_ratios <- function(ratios_metabs_df, row_index, intensities_zscore_df, fraction_side, intensity_cols) { + fraction_side_hmdb_ids <- ratios_metabs_df[row_index, fraction_side] + if (grepl("plus", fraction_side_hmdb_ids)) { + fraction_side_hmdb_id_list <- strsplit(fraction_side_hmdb_ids, "plus")[[1]] + fraction_side_intensity_list <- intensities_zscore_df %>% filter(HMDB_code %in% fraction_side_hmdb_id_list) %>% + select(any_of(intensity_cols)) + fraction_side_intensity <- apply(fraction_side_intensity_list, 2, sum) + } else if(fraction_side_hmdb_ids == "one") { + fraction_side_intensity <- 1 + } else { + fraction_side_intensity <- intensities_zscore_df %>% filter(HMDB_code == fraction_side_hmdb_ids) %>% + select(any_of(intensity_cols)) + } + return(fraction_side_intensity) +} + +#' Get the sample IDs for columns that have Z-score and intensities +#' +#' @param colnames_zscore: vector of sample IDs from the dataframe containing Z-scores (vector of strings) +#' @param intensity_cols: vector of sample IDs form the dataframe containing intensities (vector of strings) +#' +#' @returns: vector of sample IDs that are in both input vectors (vector of strings) +get_zscore_columns <- function(colnames_zscore, intensity_cols) { + intersect(paste0(intensity_cols, "_Zscore"), grep("_Zscore", colnames_zscore, value = T)) +} + +#' Get a list with dataframes for all off the metabolite group in a directory +#' +#' @param metab_group_dir: directory containing txt files with metabolites per group (string) +#' +#' @returns: list with dataframes with info on metabolites (list of dataframes) +get_list_metabolites <- function(metab_group_dir) { + # get a list of all metabolite files + metabolite_files <- list.files(metab_group_dir, pattern = "*.txt", full.names = FALSE, recursive = FALSE) + # put all metabolites into one list + metab_list_all <- lapply(paste(metab_group_dir, metabolite_files, sep = "/"), + read.table, sep = "\t", header = TRUE, quote = "") + names(metab_list_all) <- gsub(".txt", "", metabolite_files) + + return(metab_list_all) +} + +#' Combine patient Z-scores with metabolite info +#' +#' @param metab_list_all: list of dataframes with metabolite information for different stofgroepen (list) +#' @param zscore_df: dataframe with metabolite Z-scores for all patient +#' +#' @return: list of dataframes for each stofgroep with data for each metabolite and patient/control per row +combine_metab_info_zscores <- function(metab_list_all, zscore_df) { + # remove HMDB_name column and "_Zscore" from column (patient) names + zscore_df <- zscore_df %>% select(-HMDB_name) %>% + rename_with(~ str_remove(.x, "_Zscore"), .cols = contains("_Zscore")) + + # put data into pages, max 20 violin plots per page in PDF + metab_interest_sorted <- list() + + for (metab_class in names(metab_list_all)) { + metab_df <- metab_list_all[[metab_class]] + # Select HMDB_code and HMDB_name columns + metab_df <- metab_df %>% select(HMDB_code, HMDB_name) + + # Change the HMDB_name column so all names have 45 characters + metab_df <- metab_df %>% mutate(HMDB_name = case_when( + str_length(HMDB_name) > 45 ~ str_c(str_sub(HMDB_name, 1, 42), "..."), + str_length(HMDB_name) < 45 ~ str_pad(HMDB_name, 45, side = "right", pad = " "), + TRUE ~ HMDB_name + )) + + # Join metabolite info with the Z-score dataframe + metab_interest <- metab_df %>% inner_join(zscore_df, by = "HMDB_code") %>% select(-HMDB_code) + + # put the data frame in long format + metab_interest_melt <- reshape2::melt(metab_interest, id.vars = "HMDB_name", variable.name = "Sample", + value.name = "Z_score") + # Add the dataframe sorted on HMDB_name to a list + metab_interest_sorted[[metab_class]] <- metab_interest_melt + } + + return(metab_interest_sorted) +} + +#' Combine patient and control data for each page of the violinplot pdf +#' +#' @param metab_interest_sorted: list of dataframes with data for each metabolite and patient (list) +#' @param metab_interest_contr: list of dataframes with data for each metabolite and control (list) +#' @param nr_plots_perpage: number of plots per page in the violinplot pdf (integer) +#' @param nr_pat: number of patients (integer) +#' @param nr_contr: number of controls (integer) +#' +#' @return: list of dataframes with metabolite Z-scores for each patient and control, +#' the length of list is the number of pages for the violinplot pdf (list) +prepare_data_perpage <- function(metab_interest_sorted, metab_interest_contr, nr_plots_perpage, nr_pat, nr_contr) { + metab_perpage <- list() + metab_category <- c() + + for (metab_class in names(metab_interest_sorted)) { + # Get the data for patients and controls for the metab_interest_sorted list + metab_sort_patients_df <- metab_interest_sorted[[metab_class]] + metab_sort_controls_df <- metab_interest_contr[[metab_class]] + + # Calculate the number of pages + nr_pages <- ceiling(length(unique(metab_sort_patients_df$HMDB_name)) / nr_plots_perpage) + + # Get all metabolites and create list with HMDB naames of max nr_plots_perpage long + metabolites <- unique(metab_sort_patients_df$HMDB_name) + metabolites_in_chunks <- split(metabolites, ceiling(seq_along(metabolites) / nr_plots_perpage)) + nr_chunks <- length(metabolites_in_chunks) + + current_perpage <- lapply(metabolites_in_chunks, function(metab_name) { + patients_df <- metab_sort_patients_df %>% filter(HMDB_name %in% metab_name) + controls_df <- metab_sort_controls_df %>% filter(HMDB_name %in% metab_name) + + # Combine both dataframes + combined_df <- rbind(patients_df, controls_df) + + # Add empty dummy's to extend the number of metabs to the nr_plots_perpage + n_missing <- nr_plots_perpage - length(metab_name) + if (n_missing > 0) { + dummy_names <- paste0(" ", strrep(" ", seq_len(n_missing))) + metab_order <- c(metab_name, dummy_names) + } else { + metab_order <- metab_name + } + attr(combined_df, "y_order") <- rev(metab_order) + + return(combined_df) + }) + # Add new items to main list + metab_perpage <- append(metab_perpage, current_perpage) + # create list of page headers + metab_category <- c(metab_category, paste(metab_class, seq(nr_chunks), sep = "_")) + } + # add page headers to list + names(metab_perpage) <- metab_category + + return(metab_perpage) +} + +#' Get patient data to be uploaded to Helix +#' +#' @param metab_interest_sorted: list of dataframes with metabolite Z-scores for each sample/patient (list) +#' @param metab_list_all: list of tables with metabolites for Helix and violin plots (list) +#' +#' @return: dataframe with patient data with only metabolites for Helix and violin plots +#' with Helix name, high/low Z-score cutoffs +get_patient_data_to_helix <- function(metab_interest_sorted, metab_list_all) { + # Combine Z-scores of metab groups together + df_all_metabs_zscores <- bind_rows(metab_interest_sorted) + + # Change the Sample column to characters, trim HMDB_name and split HMDB_name in new column + df_all_metabs_zscores <- df_all_metabs_zscores %>% + mutate(Sample = as.character(Sample), + HMDB_name = str_trim(HMDB_name, "right"), + HMDB_name_split = str_split_fixed(HMDB_name, "nitine;", 2)[, 1]) + + # Combine stofgroepen + dims_helix_table <- bind_rows(metab_list_all) + + # Filter for Helix metabolites and split HMDB_name column for matching with df_all_metabs_zscores + dims_helix_table <- dims_helix_table %>% + filter(Helix == "ja") %>% + mutate(HMDB_name_split = str_split_fixed(HMDB_name, "nitine;", 2)[, 1]) %>% + select(HMDB_name_split, Helix_naam, high_zscore, low_zscore) + + # Filter DIMS results for metabolites for Helix and combine Helix info + df_metabs_helix <- df_all_metabs_zscores %>% + filter(HMDB_name_split %in% dims_helix_table$HMDB_name_split) %>% + left_join(dims_helix_table, by = join_by(HMDB_name_split)) %>% + select(HMDB_name, Sample, Z_score, Helix_naam, high_zscore, low_zscore) + + return(df_metabs_helix) +} + +#' Check for Diagnostics patients with correct patient number (e.g. starting with "P2024M") +#' +#' @param patient_column: a column from dataframe with IDs (character vector) +#' +#' @return: a logical vector with TRUE or FALSE for each element (vector) +is_diagnostic_patient <- function(patient_column) { + diagnostic_patients <- grepl("^P[0-9]{4}M", patient_column) + + return(diagnostic_patients) +} + +#' Get the output dataframe for Helix +#' +#' @param protocol_name: protocol name (string) +#' @param df_metabs_helix: dataframe with metabolite Z-scores for patients (dataframe) +#' +#' @return: dataframe with patient metabolite Z-scores in correct format for Helix +output_for_helix <- function(protocol_name, df_metabs_helix) { + # Remove positive controls + df_metabs_helix <- df_metabs_helix %>% filter(is_diagnostic_patient(Sample)) + + # Add 'Vial' column, each patient has unique ID + df_metabs_helix <- df_metabs_helix %>% + group_by(Sample) %>% + mutate(Vial = cur_group_id()) %>% + ungroup() + + # Split patient number into labnummer and Onderzoeksnummer + df_metabs_helix <- add_lab_id_and_onderzoeksnummer(df_metabs_helix) + + # Add column with protocol name + df_metabs_helix$Protocol <- protocol_name + + # Change name Z_score and Helix_naam columns to Amount and Name + change_columns <- c(Amount = "Z_score", Name = "Helix_naam") + df_metabs_helix <- df_metabs_helix %>% rename(all_of(change_columns)) + + # Select only necessary columns and set them in correct order + df_metabs_helix <- df_metabs_helix %>% + select(c(Vial, labnummer, Onderzoeksnummer, Protocol, Name, Amount)) + + # Remove duplicate patient-metabolite combinations ("leucine + isoleucine + allo-isoleucin_Z-score" is added 3 times) + df_metabs_helix <- df_metabs_helix %>% + group_by(Onderzoeksnummer, Name) %>% + distinct() %>% + ungroup() + + return(df_metabs_helix) +} + +#' Adding labnummer and Onderzoeksnummer to a dataframe +#' +#' @param df_metabs_helix: dataframe with patient data to be uploaded to Helix +#' +#' @return: dataframe with added labnummer and Onderzoeksnummer columns +add_lab_id_and_onderzoeksnummer <- function(df_metabs_helix) { + # Split patient number into labnummer and Onderzoeksnummer + for (row in 1:nrow(df_metabs_helix)) { + df_metabs_helix[row, "labnummer"] <- gsub("^P|\\.[0-9]*", "", df_metabs_helix[row, "Sample"]) + labnummer_split <- strsplit(as.character(df_metabs_helix[row, "labnummer"]), "M")[[1]] + df_metabs_helix[row, "Onderzoeksnummer"] <- paste0("MB", labnummer_split[1], "/", labnummer_split[2]) + } + + return(df_metabs_helix) +} + +#' Create a dataframe with all metabolites that exceed the min and max Z-score cutoffs +#' +#' @param patient_name: patient code (string) +#' @param dims_helix_table: dataframe with metabolite Z-scores for each patient and Helix info (dataframe) +#' +#' @return: dataframe with metabolites that exceed the min and max Z-score cutoffs for the selected patient +prepare_alarmvalues <- function(patient_name, dims_helix_table) { + # extract data for patient of interest (patient_name) + patient_metabs_helix <- dims_helix_table %>% + filter(Sample == patient_name) %>% + mutate(Z_score = round(Z_score, 2)) + + patient_high_df <- patient_metabs_helix %>% filter(Z_score > high_zscore) + patient_low_df <- patient_metabs_helix %>% filter(Z_score < low_zscore) + + # sort tables on zscore + patient_high_df <- patient_high_df %>% arrange(desc(Z_score)) + patient_low_df <- patient_low_df %>% arrange(Z_score) + # add lines for increased, decreased + extra_line1 <- c("Increased", "") + extra_line2 <- c("Decreased", "") + + # combine the two lists + top_metab_patient <- rbind(extra_line1, patient_high_df, extra_line2, patient_low_df) + top_metab_patient <- top_metab_patient %>% select(c(HMDB_name, Z_score)) + # remove row names + rownames(top_metab_patient) <- NULL + # change column names for display + colnames(top_metab_patient) <- c("Metabolite", "Z-score") + + return(top_metab_patient) +} + +#' Create a dataframe with the top 20 highest and top 10 lowest metabolites per patient +#' +#' @param pt_name: patient code (string) +#' @param zscore_patients: dataframe with metabolite Z-scores per patient (dataframe) +#' @param top_highest: the number of metabolites with the highest Z-score to display in the table (numeric) +#' @param top_lowest: the number of metabolites with the lowest Z-score to display in the table (numeric) +#' +#' @return: dataframe with 30 metabolites and Z-scores (dataframe) +prepare_toplist <- function(patient_id, zscore_patients) { + top_highest <- 20 + top_lowest <- 10 + patient_df <- zscore_patients %>% + select(c(HMDB_code, HMDB_name, all_of(patient_id))) %>% + arrange(desc(across(patient_id))) + + # Get lowest Zscores + patient_df_low <- patient_df[1:top_lowest, ] + patient_df_low <- patient_df_low %>% mutate(across(patient_id, ~ round(.x ,2))) + + # Get highest Zscores + patient_df_high <- patient_df[nrow(patient_df):(nrow(patient_df) - top_highest + 1), ] + patient_df_high <- patient_df_high %>% mutate(across(patient_id, ~ round(.x ,2))) + + # add lines for increased, decreased + extra_line1 <- c("Increased", "", "") + extra_line2 <- c("Decreased", "", "") + top_metab_pt <- rbind(extra_line1, patient_df_high, extra_line2, patient_df_high) + # remove row names + rownames(top_metab_pt) <- NULL + + # change column names for display + colnames(top_metab_pt) <- c("HMDB_ID", "Metabolite", "Z-score") + + return(top_metab_pt) +} + +#' Create a pdf with table with metabolites and violin plots +#' +#' @param pdf_dir: location where to save the pdf file (string) +#' @param patient_id: patient id (string) +#' @param metab_perpage: list of dataframes, each dataframe contains data for a page in de pdf (list) +#' @param top_metab_pt: dataframe with increased and decreased metabolites for this patient (dataframe) +#' @param explanation: text that explains the violin plots and the pipeline version (string) +create_pdf_violin_plots <- function(pdf_dir, patient_id, metab_perpage, top_metab_pt, explanation) { + # set parameters for plots + plot_height <- 9.6 + plot_width <- 6 + + # patient plots, create the PDF device + patient_id_sub <- patient_id + suffix <- "" + if (grepl("Diagnostics", pdf_dir) & is_diagnostic_patient(patient_id)) { + prefix <- "MB" + suffix <- "_DIMS_PL_DIAG" + # substitute P and M in P2020M00001 into right format for Helix + patient_id_sub <- gsub("[PM]", "", patient_id) + patient_id_sub <- gsub("\\..*", "", patient_id_sub) + } else if (grepl("Diagnostics", pdf_dir)) { + prefix <- "Dx_" + } else if (grepl("IEM", pdf_dir)) { + prefix <- "IEM_" + } else { + prefix <- "R_" + } + + pdf(paste0(pdf_dir, "/", prefix, patient_id_sub, suffix, ".pdf"), + onefile = TRUE, + width = plot_width, + height = plot_height) + + # page headers: + page_headers <- names(metab_perpage) + + # put table into PDF file, if not empty + if (!is.null(dim(top_metab_pt))) { + max_rows_per_page <- 35 + total_rows <- nrow(top_metab_pt) + number_of_pages <- ceiling(total_rows / max_rows_per_page) + + # get the names and numbers in the table aligned + table_theme <- ttheme_default(core = list(fg_params = list(hjust = 0, x = 0.05, fontsize = 6)), + colhead = list(fg_params = list(fontsize = 8, fontface = "bold"))) + + for (page in seq(number_of_pages)) { + start_row <- (page - 1) * max_rows_per_page + 1 + end_row <- min(page * max_rows_per_page, total_rows) + page_data <- top_metab_pt[start_row:end_row, ] + + table_grob <- tableGrob(page_data, theme = table_theme, rows = NULL) + + grid.arrange( + table_grob, + top = paste0("Top deviating metabolites for patient: ", patient_id) + ) + } + } + + # violin plots + for (metab_class in names(metab_perpage)) { + # extract list of metabolites to plot on a page + metab_zscores_df <- metab_perpage[[metab_class]] + # extract original data for patient of interest (pt_name) before cut-offs + patient_zscore_df <- metab_zscores_df %>% filter(Sample == patient_id) + + # Remove patient column and change Z-score. If under -5 to -5 and if above 20 to 20. + metab_zscores_df <- metab_zscores_df %>% + filter(Sample != patient_id) %>% + mutate(Z_score = pmin(pmax(Z_score, -5), 20)) + + # subtitle per page + sub_perpage <- gsub("_", " ", metab_class) + # for IEM plots, put subtitle on two lines + sub_perpage <- gsub("probability", "\nprobability", sub_perpage) + + # draw violin plot. + ggplot_object <- create_violin_plot(metab_zscores_df, patient_zscore_df, sub_perpage, patient_id) + + suppressWarnings(print(ggplot_object)) + } + + # add explanation of violin plots, version number etc. + plot(NA, xlim = c(0, 5), ylim = c(0, 5), bty = "n", xaxt = "n", yaxt = "n", xlab = "", ylab = "") + if (length(explanation) > 0) { + text(0.2, 5, explanation[1], pos = 4, cex = 0.8) + for (line_index in 2:length(explanation)) { + text_y_position <- 5 - (line_index * 0.2) + text(-0.2, text_y_position, explanation[line_index], pos = 4, cex = 0.5) + } + } + + # close the PDF file + dev.off() +} + +#' Create violin plots +#' +#' @param metab_zscores_df: dataframe with Z-scores for all samples (dataframe) +#' @param patient_zscore_df: dataframe with Z-scores for the specified patient (dataframe) +#' @param sub_perpage: subtitle of the page (string) +#' @param patient_id: the patient id of the selected patient (string) +#' +#' @returns +create_violin_plot <- function(metab_zscores_df, patient_zscore_df, sub_perpage, patient_id) { + fontsize <- 1 + circlesize <- 0.8 + # Set colors for the violinplot: green, blue, blue/purple, purple, orange, red + colors_plot <- c("#22E4AC", "#00B0F0", "#504FFF", "#A704FD", "#F36265", "#DA0641") + + y_order <- attr(metab_zscores_df, "y_order") + metab_zscores_df$HMDB_name <- rev(factor(metab_zscores_df$HMDB_name, levels = rev(y_order))) + patient_zscore_df$HMDB_name <- rev(factor(patient_zscore_df$HMDB_name, levels = rev(y_order))) + + ggplot_object <- ggplot(metab_zscores_df, aes(x = Z_score, y = HMDB_name)) + + # Make violin plots + geom_violin(scale = "width", na.rm = TRUE) + + # Add Z-score for the selected patient, shape=22 gives square for patient of interest + geom_point(data = patient_zscore_df, aes(color = Z_score), + size = 3.5 * circlesize, shape = 22, fill = "white", na.rm = TRUE) + + # Add the Z-score at the right side of the plot + geom_text( + data = patient_zscore_df, + aes(16, label = paste0("Z=", round(Z_score, 2))), + hjust = "left", vjust = +0.2, size = 3, na.rm = TRUE) + + # Set colour for the Z-score of the selected patient + scale_fill_gradientn( + colors = colors_plot, values = NULL, space = "Lab", na.value = "grey50", guide = "colourbar", + aesthetics = "colour" + ) + + # Add labels to the axis + labs(x = "Z-scores", y = "Metabolites", subtitle = sub_perpage, color = "z-score") + + # Add a title to the page + ggtitle(label = paste0("Results for patient ", patient_id)) + + # Set theme: size and font type of y-axis labels, remove legend and make the + theme( + axis.text.y = element_text(family = "Courier", size = 6), + legend.position = "none", + plot.caption = element_text(size = rel(fontsize)) + ) + + # Set y-axis to set order + scale_y_discrete(limits = y_order) + + # Limit the x-axis to between -5 and 20 + xlim(-5, 20) + + # Set grey vertical lines at -2 and 2 + geom_vline(xintercept = c(-2, 2), col = "grey", lwd = 0.5, lty = 2) + + + return(ggplot_object) +} + +#' Run the dIEM algorithm (DOI: 10.3390/ijms21030979) +#' +#' @param expected_biomarkers_df: table with information for HMDB codes about IEMs (dataframe) +#' @param zscore_patients: dataframe containing Z-scores for patient (dataframe) +#' +#' @returns probability_score: a dataframe with probability scores for IEMs for each patient (dataframe) +run_diem_algorithm <- function(expected_biomarkers_df, zscore_patients_df, sample_cols) { + # Rank the metabolites for each patient individually + ranking_patients <- zscore_patients_df %>% + mutate(across(-c(HMDB_code, HMDB_name), rank_patient_zscores)) + + ranking_patients <- merge(x = expected_biomarkers_df, y = ranking_patients, + by.x = c("HMDB_code"), by.y = c("HMDB_code")) + + zscore_expected_df <- merge(x = expected_biomarkers_df, y = zscore_patients_df, + by.x = c("HMDB_code"), by.y = c("HMDB_code")) + + # Change Z-score to zero for specific cases + zscore_expected_df <- zscore_expected_df %>% mutate(across( + all_of(sample_cols), + ~ case_when( + Change == "Increase" & Dispensability == "Indispensable" & .x <= 1.6 ~ 0, + Change == "Decrease" & Dispensability == "Indispensable" & .x >= -1.2 ~ 0, + TRUE ~ .x + ) + )) + + # Sort both dataframes on HMDB_code for calculating the metabolite score + zscore_expected_df <- zscore_expected_df[order(zscore_expected_df$HMDB_code), ] + ranking_patients <- ranking_patients[order(ranking_patients$HMDB_code), ] + + # Set up dataframe for the metabolite score, copy zscore_expected_df for biomarker info + metabolite_score_info <- zscore_expected_df + # Calculate metabolite score: Z-score/(Rank * 0.9) + metabolite_score_info[sample_cols] <- zscore_expected_df[sample_cols] / (ranking_patients[sample_cols] * 0.9) + + # Calculate the weighted score: metabolite_score * Total_Weight + metabolite_weight_score <- metabolite_score_info %>% + mutate(across( + all_of(sample_cols), + ~ .x * Total_Weight + )) + + #TODO: dit klopt nu niet, checken dat alleen de eerste waarde wordt gebruikt in orginele dIEM algoritme + # Calculate the probability score for each disease - Mz combination + probability_score <- metabolite_weight_score %>% + group_by(Disease, M.z) %>% + summarise(across( + all_of(sample_cols), + ~ sum(.x, na.rm = TRUE) + ), .groups = "drop") + + # Set probability score to 0 for Z-scores == 0 + for (sample_col in sample_cols) { + # Get indexes of Zscore that equal 0 + zscores_zero_idx <- which(zscore_expected_df[[sample_col]] == 0) + # Get diseases that have a Zscore of 0 + diseases_zero <- unique(zscore_expected_df[zscores_zero_idx, "Disease"]) + # Set probabilty of these diseases to 0 + probability_score[probability_score$Disease %in% diseases_zero, sample_col] <- 0 + } + + colnames(probability_score) <- gsub("_Zscore", "_prob_score", colnames(probability_score)) + + return(probability_score) +} + +#' Ranking Z-scores for a patient, separate for positive and negative Z-scores +#' +#' @param zscore_col: vector with Z-scores for a single patient (vector of integers) +#' +#' @returns ranking: a vector of the ranking of the Z-scores (vector of integers) +rank_patient_zscores <- function(zscore_col) { + # Create ranking column with default NA values + ranking <- rep(NA_real_, length(zscore_col)) + + # Get indexes for negative and positive rows + neg_indexes <- which(zscore_col <= 0) + pos_indexes <- which(zscore_col > 0) + + # Rank the negative and positive Zscores + ranking[neg_indexes] <- dense_rank(zscore_col[neg_indexes]) + ranking[pos_indexes] <- dense_rank(-zscore_col[pos_indexes]) + + return(ranking) +} + +#' Save the probability score dataframe as an Excel file +#' +#' @param probability_score: a dataframe containing probability scores for each patient (dataframe) +#' @param output_dir: location where to save the Excel file (string) +#' @param run_name: name of the run, for the file name (string) +save_prob_scores_to_Excel <- function(probability_score, output_dir, run_name) { + # Create conditional formatting for output Excel sheet. Colors according to values. + wb <- createWorkbook() + addWorksheet(wb, "Probability Scores") + writeData(wb, "Probability Scores", probability_score) + conditionalFormatting(wb, "Probability Scores", cols = 2:ncol(probability_score), rows = 1:nrow(probability_score), + type = "colourScale", style = c("white", "#FFFDA2", "red"), rule = c(1, 10, 100)) + saveWorkbook(wb, file = paste0(output_dir, "/dIEM_algoritme_output_", run_name, ".xlsx"), overwrite = TRUE) + rm(wb) +} From 9ffd606737ba8606a0e4bbe710d86508c9b2ed46 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Fri, 15 Aug 2025 16:12:31 +0200 Subject: [PATCH 02/30] Fixed errors --- DIMS/GenerateViolinPlots.R | 2 +- DIMS/export/generate_violin_plots_functions.R | 43 +++++++++++-------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/DIMS/GenerateViolinPlots.R b/DIMS/GenerateViolinPlots.R index 2e6cb885..67f94b17 100644 --- a/DIMS/GenerateViolinPlots.R +++ b/DIMS/GenerateViolinPlots.R @@ -143,7 +143,7 @@ for (metabolite_dir in metabolite_dirs) { if (grepl("Diagnost", pdf_dir)) { top_metabs_patient <- prepare_alarmvalues(patient_id, dims_helix_table) } else { - top_metabs_patient <- prepare_toplist(patient_id, zscore_patients) + top_metabs_patient <- prepare_toplist(patient_id, zscore_patients_df) } # generate normal violin plots diff --git a/DIMS/export/generate_violin_plots_functions.R b/DIMS/export/generate_violin_plots_functions.R index b1855a7f..beb0ddb9 100644 --- a/DIMS/export/generate_violin_plots_functions.R +++ b/DIMS/export/generate_violin_plots_functions.R @@ -30,7 +30,8 @@ get_intentities_for_ratios <- function(ratios_metabs_df, row_index, intensities_ #' #' @returns: vector of sample IDs that are in both input vectors (vector of strings) get_zscore_columns <- function(colnames_zscore, intensity_cols) { - intersect(paste0(intensity_cols, "_Zscore"), grep("_Zscore", colnames_zscore, value = T)) + sample_intersect <- intersect(paste0(intensity_cols, "_Zscore"), grep("_Zscore", colnames_zscore, value = TRUE)) + return(sample_intersect) } #' Get a list with dataframes for all off the metabolite group in a directory @@ -261,16 +262,18 @@ prepare_alarmvalues <- function(patient_name, dims_helix_table) { patient_high_df <- patient_metabs_helix %>% filter(Z_score > high_zscore) patient_low_df <- patient_metabs_helix %>% filter(Z_score < low_zscore) - # sort tables on zscore - patient_high_df <- patient_high_df %>% arrange(desc(Z_score)) - patient_low_df <- patient_low_df %>% arrange(Z_score) + if (nrow(patient_high_df) > 0 | nrow(patient_low_df) > 0) { + # sort tables on zscore + patient_high_df <- patient_high_df %>% arrange(desc(Z_score)) %>% select(c(HMDB_name, Z_score)) + patient_low_df <- patient_low_df %>% arrange(Z_score) %>% select(c(HMDB_name, Z_score)) + } # add lines for increased, decreased extra_line1 <- c("Increased", "") extra_line2 <- c("Decreased", "") # combine the two lists top_metab_patient <- rbind(extra_line1, patient_high_df, extra_line2, patient_low_df) - top_metab_patient <- top_metab_patient %>% select(c(HMDB_name, Z_score)) + # remove row names rownames(top_metab_patient) <- NULL # change column names for display @@ -291,21 +294,21 @@ prepare_toplist <- function(patient_id, zscore_patients) { top_highest <- 20 top_lowest <- 10 patient_df <- zscore_patients %>% - select(c(HMDB_code, HMDB_name, all_of(patient_id))) %>% - arrange(desc(across(patient_id))) + select(HMDB_code, HMDB_name, !!sym(patient_id)) %>% + arrange(!!sym(patient_id)) # Get lowest Zscores patient_df_low <- patient_df[1:top_lowest, ] - patient_df_low <- patient_df_low %>% mutate(across(patient_id, ~ round(.x ,2))) + patient_df_low <- patient_df_low %>% mutate(across(!!sym(patient_id), ~ round(.x ,2))) # Get highest Zscores patient_df_high <- patient_df[nrow(patient_df):(nrow(patient_df) - top_highest + 1), ] - patient_df_high <- patient_df_high %>% mutate(across(patient_id, ~ round(.x ,2))) + patient_df_high <- patient_df_high %>% mutate(across(!!sym(patient_id), ~ round(.x ,2))) # add lines for increased, decreased extra_line1 <- c("Increased", "", "") extra_line2 <- c("Decreased", "", "") - top_metab_pt <- rbind(extra_line1, patient_df_high, extra_line2, patient_df_high) + top_metab_pt <- rbind(extra_line1, patient_df_high, extra_line2, patient_df_low) # remove row names rownames(top_metab_pt) <- NULL @@ -420,7 +423,7 @@ create_pdf_violin_plots <- function(pdf_dir, patient_id, metab_perpage, top_meta #' @param sub_perpage: subtitle of the page (string) #' @param patient_id: the patient id of the selected patient (string) #' -#' @returns +#' @returns ggpplot_object: a violin plot of metabolites that highlights the selected patient (ggplot object) create_violin_plot <- function(metab_zscores_df, patient_zscore_df, sub_perpage, patient_id) { fontsize <- 1 circlesize <- 0.8 @@ -509,16 +512,18 @@ run_diem_algorithm <- function(expected_biomarkers_df, zscore_patients_df, sampl mutate(across( all_of(sample_cols), ~ .x * Total_Weight - )) + )) %>% + arrange(desc(Disease), desc(Absolute_Weight)) - #TODO: dit klopt nu niet, checken dat alleen de eerste waarde wordt gebruikt in orginele dIEM algoritme # Calculate the probability score for each disease - Mz combination - probability_score <- metabolite_weight_score %>% - group_by(Disease, M.z) %>% - summarise(across( - all_of(sample_cols), - ~ sum(.x, na.rm = TRUE) - ), .groups = "drop") + probability_score <- metabolite_weight_score %>% + filter( + !duplicated(select(., Disease, M.z)) | + !duplicated(select(., Disease, M.z), fromLast = FALSE) + ) %>% + group_by(Disease) %>% + summarise(across(all_of(sample_cols), sum), .groups = "drop") + # Set probability score to 0 for Z-scores == 0 for (sample_col in sample_cols) { From aede6ee53a197286609bda7937f7b812c3314717 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 21 Aug 2025 12:13:54 +0200 Subject: [PATCH 03/30] Fixed linting --- DIMS/GenerateViolinPlots.R | 43 ++-- DIMS/export/generate_violin_plots_functions.R | 222 +++++++++--------- 2 files changed, 130 insertions(+), 135 deletions(-) diff --git a/DIMS/GenerateViolinPlots.R b/DIMS/GenerateViolinPlots.R index 67f94b17..847c4cb5 100644 --- a/DIMS/GenerateViolinPlots.R +++ b/DIMS/GenerateViolinPlots.R @@ -38,7 +38,7 @@ protocol_name <- "DIMS_PL_DIAG" # Remove columns, move HMDB_code & HMDB_name column to the front, change intensity columns to numeric intensities_zscore_df <- intensities_zscore_df %>% - select(-c(plots, HMDB_name_all, HMDB_ID_all, sec_HMDB_ID, HMDB_key, sec_HMBD_ID_rlvnc, name, + select(-c(plots, HMDB_name_all, HMDB_ID_all, sec_HMDB_ID, HMDB_key, sec_HMBD_ID_rlvnc, name, relevance, descr, origin, fluids, tissue, disease, pathway, nr_ctrls)) %>% relocate(c(HMDB_code, HMDB_name)) %>% rename(mean_controls = avg_ctrls, sd_controls = sd_ctrls) %>% @@ -70,11 +70,11 @@ colnames(ratio_zscore_df) <- colnames(intensities_zscore_df) ratio_zscore_df$HMDB_code <- ratios_metabs_df$HMDB.code ratio_zscore_df$HMDB_name <- ratios_metabs_df$Ratio_name -for (row_index in 1:nrow(ratios_metabs_df)) { - numerator_intensities <- get_intentities_for_ratios(ratios_metabs_df, row_index, - intensities_zscore_df, "HMDB_numerator", intensity_cols) - denominator_intensities <- get_intentities_for_ratios(ratios_metabs_df, row_index, - intensities_zscore_df, "HMDB_denominator", intensity_cols) +for (row_index in seq_len(nrow(ratios_metabs_df))) { + numerator_intensities <- get_intentities_for_ratios(ratios_metabs_df, row_index, + intensities_zscore_df, "HMDB_numerator", intensity_cols) + denominator_intensities <- get_intentities_for_ratios(ratios_metabs_df, row_index, + intensities_zscore_df, "HMDB_denominator", intensity_cols) # calculate intensity ratios ratio_zscore_df[row_index, intensity_cols_index] <- log2(numerator_intensities / denominator_intensities) } @@ -103,8 +103,8 @@ colnames(zscore_controls_df) <- gsub("_Zscore", "", colnames(zscore_controls_df) expected_biomarkers_df <- expected_biomarkers_df %>% rename(HMDB_code = HMDB.code, HMDB_name = Metabolite) -expected_biomarkers_info <- expected_biomarkers_df %>% - select(c(Disease, HMDB_code, HMDB_name)) %>% +expected_biomarkers_info <- expected_biomarkers_df %>% + select(c(Disease, HMDB_code, HMDB_name)) %>% distinct(Disease, HMDB_code, .keep_all = TRUE) metabolite_dirs <- list.files(path = path_metabolite_groups, full.names = FALSE, recursive = FALSE) @@ -122,20 +122,20 @@ for (metabolite_dir in metabolite_dirs) { nr_plots_perpage, nr_of_patients, nr_of_controls) # for Diagnostics metabolites to be saved in Helix - if(grepl("Diagnost", pdf_dir)) { + if (grepl("Diagnost", pdf_dir)) { # get table that combines DIMS results with stofgroepen/Helix table dims_helix_table <- get_patient_data_to_helix(metab_interest_sorted, metab_list_all) - + # check if run contains Diagnostics patients (e.g. "P2024M"), not for research runs - if(any(is_diagnostic_patient(dims_helix_table$Sample))){ + if (any(is_diagnostic_patient(dims_helix_table$Sample))) { # get output file for Helix output_helix <- output_for_helix(protocol_name, dims_helix_table) # write output to file - path_helixfile <- paste0(output_dir, "output_Helix_", run_name,".csv") - write.csv(output_helix, path_helixfile, quote = F, row.names = F) + path_helixfile <- paste0(output_dir, "output_Helix_", run_name, ".csv") + write.csv(output_helix, path_helixfile, quote = FALSE, row.names = FALSE) } } - + # make violin plots per patient for (patient_id in patients) { # for category Diagnostics, make list of metabolites that exceed alarm values for this patient @@ -157,7 +157,7 @@ expected_biomarkers_df <- expected_biomarkers_df %>% rename(HMDB_code = HMDB.cod diem_probability_score <- run_diem_algorithm(expected_biomarkers_df, zscore_patients_df, patients) -save_prob_scores_to_Excel(diem_probability_score, output_dir, run_name) +save_prob_scores_to_excel(diem_probability_score, output_dir, run_name) #### Generate dIEM plots ######### @@ -174,7 +174,7 @@ for (patient_id in patients) { arrange(desc(!!sym(patient_id))) %>% slice(1:top_number_iem_diseases) %>% filter(!!sym(patient_id) >= threshold_iem) - + if (nrow(patient_top_iems_probs) > 0) { top_iems <- patient_top_iems_probs %>% pull(Disease) # Get the metabolites for each IEM and their probability @@ -186,26 +186,27 @@ for (patient_id in patients) { return(metab_iem) }) names(metabs_iems) <- metabs_iems_names - + # Get the Z-scores with metabolite information metab_iem_sorted <- combine_metab_info_zscores(metabs_iems, zscore_patients_df) metab_iem_controls <- combine_metab_info_zscores(metabs_iems, zscore_controls_df) # Get a list of dataframes for each IEM diem_metab_perpage <- prepare_data_perpage(metab_iem_sorted, metab_iem_controls, nr_plots_perpage, nr_of_patients, nr_of_controls) - # Get a dataframe of the top metabolites + # Get a dataframe of the top metabolites top_metabs_patient <- prepare_toplist(patient_id, zscore_patients_df) - + # Generate and save dIEM violin plots create_pdf_violin_plots(diem_plot_dir, patient_id, diem_metab_perpage, top_metabs_patient, explanation_violin_plot) - + } else { patient_no_iem <- c(patient_no_iem, patient_id) } } if (length(patient_no_iem) > 0) { - patient_no_iem <- c(paste0("The following patient(s) did not have dIEM probability scores higher than ", threshold_iem, " :"), + patient_no_iem <- c(paste0("The following patient(s) did not have dIEM probability scores higher than ", + threshold_iem, " :"), patient_no_iem) write(file = paste0(output_dir, "missing_probability_scores.txt"), patient_no_iem) } diff --git a/DIMS/export/generate_violin_plots_functions.R b/DIMS/export/generate_violin_plots_functions.R index beb0ddb9..0ae7edff 100644 --- a/DIMS/export/generate_violin_plots_functions.R +++ b/DIMS/export/generate_violin_plots_functions.R @@ -11,13 +11,15 @@ get_intentities_for_ratios <- function(ratios_metabs_df, row_index, intensities_ fraction_side_hmdb_ids <- ratios_metabs_df[row_index, fraction_side] if (grepl("plus", fraction_side_hmdb_ids)) { fraction_side_hmdb_id_list <- strsplit(fraction_side_hmdb_ids, "plus")[[1]] - fraction_side_intensity_list <- intensities_zscore_df %>% filter(HMDB_code %in% fraction_side_hmdb_id_list) %>% + fraction_side_intensity_list <- intensities_zscore_df %>% + filter(HMDB_code %in% fraction_side_hmdb_id_list) %>% select(any_of(intensity_cols)) fraction_side_intensity <- apply(fraction_side_intensity_list, 2, sum) - } else if(fraction_side_hmdb_ids == "one") { + } else if (fraction_side_hmdb_ids == "one") { fraction_side_intensity <- 1 } else { - fraction_side_intensity <- intensities_zscore_df %>% filter(HMDB_code == fraction_side_hmdb_ids) %>% + fraction_side_intensity <- intensities_zscore_df %>% + filter(HMDB_code == fraction_side_hmdb_ids) %>% select(any_of(intensity_cols)) } return(fraction_side_intensity) @@ -43,10 +45,10 @@ get_list_metabolites <- function(metab_group_dir) { # get a list of all metabolite files metabolite_files <- list.files(metab_group_dir, pattern = "*.txt", full.names = FALSE, recursive = FALSE) # put all metabolites into one list - metab_list_all <- lapply(paste(metab_group_dir, metabolite_files, sep = "/"), + metab_list_all <- lapply(paste(metab_group_dir, metabolite_files, sep = "/"), read.table, sep = "\t", header = TRUE, quote = "") names(metab_list_all) <- gsub(".txt", "", metabolite_files) - + return(metab_list_all) } @@ -58,34 +60,35 @@ get_list_metabolites <- function(metab_group_dir) { #' @return: list of dataframes for each stofgroep with data for each metabolite and patient/control per row combine_metab_info_zscores <- function(metab_list_all, zscore_df) { # remove HMDB_name column and "_Zscore" from column (patient) names - zscore_df <- zscore_df %>% select(-HMDB_name) %>% + zscore_df <- zscore_df %>% + select(-HMDB_name) %>% rename_with(~ str_remove(.x, "_Zscore"), .cols = contains("_Zscore")) - + # put data into pages, max 20 violin plots per page in PDF metab_interest_sorted <- list() - + for (metab_class in names(metab_list_all)) { metab_df <- metab_list_all[[metab_class]] # Select HMDB_code and HMDB_name columns metab_df <- metab_df %>% select(HMDB_code, HMDB_name) - + # Change the HMDB_name column so all names have 45 characters metab_df <- metab_df %>% mutate(HMDB_name = case_when( str_length(HMDB_name) > 45 ~ str_c(str_sub(HMDB_name, 1, 42), "..."), str_length(HMDB_name) < 45 ~ str_pad(HMDB_name, 45, side = "right", pad = " "), TRUE ~ HMDB_name )) - + # Join metabolite info with the Z-score dataframe metab_interest <- metab_df %>% inner_join(zscore_df, by = "HMDB_code") %>% select(-HMDB_code) - + # put the data frame in long format - metab_interest_melt <- reshape2::melt(metab_interest, id.vars = "HMDB_name", variable.name = "Sample", + metab_interest_melt <- reshape2::melt(metab_interest, id.vars = "HMDB_name", variable.name = "Sample", value.name = "Z_score") # Add the dataframe sorted on HMDB_name to a list metab_interest_sorted[[metab_class]] <- metab_interest_melt } - + return(metab_interest_sorted) } @@ -102,27 +105,27 @@ combine_metab_info_zscores <- function(metab_list_all, zscore_df) { prepare_data_perpage <- function(metab_interest_sorted, metab_interest_contr, nr_plots_perpage, nr_pat, nr_contr) { metab_perpage <- list() metab_category <- c() - + for (metab_class in names(metab_interest_sorted)) { # Get the data for patients and controls for the metab_interest_sorted list metab_sort_patients_df <- metab_interest_sorted[[metab_class]] metab_sort_controls_df <- metab_interest_contr[[metab_class]] - + # Calculate the number of pages nr_pages <- ceiling(length(unique(metab_sort_patients_df$HMDB_name)) / nr_plots_perpage) - + # Get all metabolites and create list with HMDB naames of max nr_plots_perpage long metabolites <- unique(metab_sort_patients_df$HMDB_name) metabolites_in_chunks <- split(metabolites, ceiling(seq_along(metabolites) / nr_plots_perpage)) nr_chunks <- length(metabolites_in_chunks) - + current_perpage <- lapply(metabolites_in_chunks, function(metab_name) { patients_df <- metab_sort_patients_df %>% filter(HMDB_name %in% metab_name) controls_df <- metab_sort_controls_df %>% filter(HMDB_name %in% metab_name) - + # Combine both dataframes combined_df <- rbind(patients_df, controls_df) - + # Add empty dummy's to extend the number of metabs to the nr_plots_perpage n_missing <- nr_plots_perpage - length(metab_name) if (n_missing > 0) { @@ -132,7 +135,7 @@ prepare_data_perpage <- function(metab_interest_sorted, metab_interest_contr, nr metab_order <- metab_name } attr(combined_df, "y_order") <- rev(metab_order) - + return(combined_df) }) # Add new items to main list @@ -142,7 +145,7 @@ prepare_data_perpage <- function(metab_interest_sorted, metab_interest_contr, nr } # add page headers to list names(metab_perpage) <- metab_category - + return(metab_perpage) } @@ -151,33 +154,33 @@ prepare_data_perpage <- function(metab_interest_sorted, metab_interest_contr, nr #' @param metab_interest_sorted: list of dataframes with metabolite Z-scores for each sample/patient (list) #' @param metab_list_all: list of tables with metabolites for Helix and violin plots (list) #' -#' @return: dataframe with patient data with only metabolites for Helix and violin plots +#' @return: dataframe with patient data with only metabolites for Helix and violin plots #' with Helix name, high/low Z-score cutoffs get_patient_data_to_helix <- function(metab_interest_sorted, metab_list_all) { # Combine Z-scores of metab groups together df_all_metabs_zscores <- bind_rows(metab_interest_sorted) - + # Change the Sample column to characters, trim HMDB_name and split HMDB_name in new column df_all_metabs_zscores <- df_all_metabs_zscores %>% mutate(Sample = as.character(Sample), HMDB_name = str_trim(HMDB_name, "right"), HMDB_name_split = str_split_fixed(HMDB_name, "nitine;", 2)[, 1]) - + # Combine stofgroepen dims_helix_table <- bind_rows(metab_list_all) - + # Filter for Helix metabolites and split HMDB_name column for matching with df_all_metabs_zscores dims_helix_table <- dims_helix_table %>% filter(Helix == "ja") %>% mutate(HMDB_name_split = str_split_fixed(HMDB_name, "nitine;", 2)[, 1]) %>% select(HMDB_name_split, Helix_naam, high_zscore, low_zscore) - + # Filter DIMS results for metabolites for Helix and combine Helix info df_metabs_helix <- df_all_metabs_zscores %>% filter(HMDB_name_split %in% dims_helix_table$HMDB_name_split) %>% left_join(dims_helix_table, by = join_by(HMDB_name_split)) %>% select(HMDB_name, Sample, Z_score, Helix_naam, high_zscore, low_zscore) - + return(df_metabs_helix) } @@ -188,7 +191,7 @@ get_patient_data_to_helix <- function(metab_interest_sorted, metab_list_all) { #' @return: a logical vector with TRUE or FALSE for each element (vector) is_diagnostic_patient <- function(patient_column) { diagnostic_patients <- grepl("^P[0-9]{4}M", patient_column) - + return(diagnostic_patients) } @@ -201,33 +204,33 @@ is_diagnostic_patient <- function(patient_column) { output_for_helix <- function(protocol_name, df_metabs_helix) { # Remove positive controls df_metabs_helix <- df_metabs_helix %>% filter(is_diagnostic_patient(Sample)) - + # Add 'Vial' column, each patient has unique ID df_metabs_helix <- df_metabs_helix %>% group_by(Sample) %>% mutate(Vial = cur_group_id()) %>% ungroup() - + # Split patient number into labnummer and Onderzoeksnummer - df_metabs_helix <- add_lab_id_and_onderzoeksnummer(df_metabs_helix) - + df_metabs_helix <- add_lab_id_and_onderzoeksnr(df_metabs_helix) + # Add column with protocol name df_metabs_helix$Protocol <- protocol_name - + # Change name Z_score and Helix_naam columns to Amount and Name change_columns <- c(Amount = "Z_score", Name = "Helix_naam") df_metabs_helix <- df_metabs_helix %>% rename(all_of(change_columns)) - + # Select only necessary columns and set them in correct order df_metabs_helix <- df_metabs_helix %>% select(c(Vial, labnummer, Onderzoeksnummer, Protocol, Name, Amount)) - + # Remove duplicate patient-metabolite combinations ("leucine + isoleucine + allo-isoleucin_Z-score" is added 3 times) df_metabs_helix <- df_metabs_helix %>% group_by(Onderzoeksnummer, Name) %>% distinct() %>% ungroup() - + return(df_metabs_helix) } @@ -236,14 +239,13 @@ output_for_helix <- function(protocol_name, df_metabs_helix) { #' @param df_metabs_helix: dataframe with patient data to be uploaded to Helix #' #' @return: dataframe with added labnummer and Onderzoeksnummer columns -add_lab_id_and_onderzoeksnummer <- function(df_metabs_helix) { +add_lab_id_and_onderzoeksnr <- function(df_metabs_helix) { # Split patient number into labnummer and Onderzoeksnummer - for (row in 1:nrow(df_metabs_helix)) { + for (row in seq_len(nrow(df_metabs_helix))) { df_metabs_helix[row, "labnummer"] <- gsub("^P|\\.[0-9]*", "", df_metabs_helix[row, "Sample"]) labnummer_split <- strsplit(as.character(df_metabs_helix[row, "labnummer"]), "M")[[1]] df_metabs_helix[row, "Onderzoeksnummer"] <- paste0("MB", labnummer_split[1], "/", labnummer_split[2]) } - return(df_metabs_helix) } @@ -258,11 +260,11 @@ prepare_alarmvalues <- function(patient_name, dims_helix_table) { patient_metabs_helix <- dims_helix_table %>% filter(Sample == patient_name) %>% mutate(Z_score = round(Z_score, 2)) - + patient_high_df <- patient_metabs_helix %>% filter(Z_score > high_zscore) patient_low_df <- patient_metabs_helix %>% filter(Z_score < low_zscore) - - if (nrow(patient_high_df) > 0 | nrow(patient_low_df) > 0) { + + if (nrow(patient_high_df) > 0 || nrow(patient_low_df) > 0) { # sort tables on zscore patient_high_df <- patient_high_df %>% arrange(desc(Z_score)) %>% select(c(HMDB_name, Z_score)) patient_low_df <- patient_low_df %>% arrange(Z_score) %>% select(c(HMDB_name, Z_score)) @@ -270,15 +272,15 @@ prepare_alarmvalues <- function(patient_name, dims_helix_table) { # add lines for increased, decreased extra_line1 <- c("Increased", "") extra_line2 <- c("Decreased", "") - + # combine the two lists top_metab_patient <- rbind(extra_line1, patient_high_df, extra_line2, patient_low_df) - + # remove row names rownames(top_metab_patient) <- NULL # change column names for display colnames(top_metab_patient) <- c("Metabolite", "Z-score") - + return(top_metab_patient) } @@ -286,7 +288,7 @@ prepare_alarmvalues <- function(patient_name, dims_helix_table) { #' #' @param pt_name: patient code (string) #' @param zscore_patients: dataframe with metabolite Z-scores per patient (dataframe) -#' @param top_highest: the number of metabolites with the highest Z-score to display in the table (numeric) +#' @param top_highest: the number of metabolites with the highest Z-score to display in the table (numeric) #' @param top_lowest: the number of metabolites with the lowest Z-score to display in the table (numeric) #' #' @return: dataframe with 30 metabolites and Z-scores (dataframe) @@ -296,25 +298,25 @@ prepare_toplist <- function(patient_id, zscore_patients) { patient_df <- zscore_patients %>% select(HMDB_code, HMDB_name, !!sym(patient_id)) %>% arrange(!!sym(patient_id)) - + # Get lowest Zscores patient_df_low <- patient_df[1:top_lowest, ] - patient_df_low <- patient_df_low %>% mutate(across(!!sym(patient_id), ~ round(.x ,2))) - + patient_df_low <- patient_df_low %>% mutate(across(!!sym(patient_id), ~ round(.x, 2))) + # Get highest Zscores patient_df_high <- patient_df[nrow(patient_df):(nrow(patient_df) - top_highest + 1), ] - patient_df_high <- patient_df_high %>% mutate(across(!!sym(patient_id), ~ round(.x ,2))) - + patient_df_high <- patient_df_high %>% mutate(across(!!sym(patient_id), ~ round(.x, 2))) + # add lines for increased, decreased extra_line1 <- c("Increased", "", "") extra_line2 <- c("Decreased", "", "") top_metab_pt <- rbind(extra_line1, patient_df_high, extra_line2, patient_df_low) # remove row names rownames(top_metab_pt) <- NULL - + # change column names for display colnames(top_metab_pt) <- c("HMDB_ID", "Metabolite", "Z-score") - + return(top_metab_pt) } @@ -329,11 +331,11 @@ create_pdf_violin_plots <- function(pdf_dir, patient_id, metab_perpage, top_meta # set parameters for plots plot_height <- 9.6 plot_width <- 6 - + # patient plots, create the PDF device patient_id_sub <- patient_id suffix <- "" - if (grepl("Diagnostics", pdf_dir) & is_diagnostic_patient(patient_id)) { + if (grepl("Diagnostics", pdf_dir) && is_diagnostic_patient(patient_id)) { prefix <- "MB" suffix <- "_DIMS_PL_DIAG" # substitute P and M in P2020M00001 into right format for Helix @@ -346,62 +348,62 @@ create_pdf_violin_plots <- function(pdf_dir, patient_id, metab_perpage, top_meta } else { prefix <- "R_" } - + pdf(paste0(pdf_dir, "/", prefix, patient_id_sub, suffix, ".pdf"), onefile = TRUE, width = plot_width, height = plot_height) - + # page headers: page_headers <- names(metab_perpage) - + # put table into PDF file, if not empty if (!is.null(dim(top_metab_pt))) { max_rows_per_page <- 35 total_rows <- nrow(top_metab_pt) number_of_pages <- ceiling(total_rows / max_rows_per_page) - + # get the names and numbers in the table aligned table_theme <- ttheme_default(core = list(fg_params = list(hjust = 0, x = 0.05, fontsize = 6)), colhead = list(fg_params = list(fontsize = 8, fontface = "bold"))) - + for (page in seq(number_of_pages)) { start_row <- (page - 1) * max_rows_per_page + 1 end_row <- min(page * max_rows_per_page, total_rows) page_data <- top_metab_pt[start_row:end_row, ] - + table_grob <- tableGrob(page_data, theme = table_theme, rows = NULL) - + grid.arrange( table_grob, top = paste0("Top deviating metabolites for patient: ", patient_id) ) } } - + # violin plots for (metab_class in names(metab_perpage)) { # extract list of metabolites to plot on a page metab_zscores_df <- metab_perpage[[metab_class]] # extract original data for patient of interest (pt_name) before cut-offs patient_zscore_df <- metab_zscores_df %>% filter(Sample == patient_id) - - # Remove patient column and change Z-score. If under -5 to -5 and if above 20 to 20. + + # Remove patient column and change Z-score. If under -5 to -5 and if above 20 to 20. metab_zscores_df <- metab_zscores_df %>% filter(Sample != patient_id) %>% mutate(Z_score = pmin(pmax(Z_score, -5), 20)) - + # subtitle per page sub_perpage <- gsub("_", " ", metab_class) # for IEM plots, put subtitle on two lines sub_perpage <- gsub("probability", "\nprobability", sub_perpage) - - # draw violin plot. + + # draw violin plot. ggplot_object <- create_violin_plot(metab_zscores_df, patient_zscore_df, sub_perpage, patient_id) - + suppressWarnings(print(ggplot_object)) } - + # add explanation of violin plots, version number etc. plot(NA, xlim = c(0, 5), ylim = c(0, 5), bty = "n", xaxt = "n", yaxt = "n", xlab = "", ylab = "") if (length(explanation) > 0) { @@ -411,7 +413,7 @@ create_pdf_violin_plots <- function(pdf_dir, patient_id, metab_perpage, top_meta text(-0.2, text_y_position, explanation[line_index], pos = 4, cex = 0.5) } } - + # close the PDF file dev.off() } @@ -420,7 +422,7 @@ create_pdf_violin_plots <- function(pdf_dir, patient_id, metab_perpage, top_meta #' #' @param metab_zscores_df: dataframe with Z-scores for all samples (dataframe) #' @param patient_zscore_df: dataframe with Z-scores for the specified patient (dataframe) -#' @param sub_perpage: subtitle of the page (string) +#' @param sub_perpage: subtitle of the page (string) #' @param patient_id: the patient id of the selected patient (string) #' #' @returns ggpplot_object: a violin plot of metabolites that highlights the selected patient (ggplot object) @@ -429,11 +431,11 @@ create_violin_plot <- function(metab_zscores_df, patient_zscore_df, sub_perpage, circlesize <- 0.8 # Set colors for the violinplot: green, blue, blue/purple, purple, orange, red colors_plot <- c("#22E4AC", "#00B0F0", "#504FFF", "#A704FD", "#F36265", "#DA0641") - + y_order <- attr(metab_zscores_df, "y_order") metab_zscores_df$HMDB_name <- rev(factor(metab_zscores_df$HMDB_name, levels = rev(y_order))) patient_zscore_df$HMDB_name <- rev(factor(patient_zscore_df$HMDB_name, levels = rev(y_order))) - + ggplot_object <- ggplot(metab_zscores_df, aes(x = Z_score, y = HMDB_name)) + # Make violin plots geom_violin(scale = "width", na.rm = TRUE) + @@ -441,53 +443,48 @@ create_violin_plot <- function(metab_zscores_df, patient_zscore_df, sub_perpage, geom_point(data = patient_zscore_df, aes(color = Z_score), size = 3.5 * circlesize, shape = 22, fill = "white", na.rm = TRUE) + # Add the Z-score at the right side of the plot - geom_text( - data = patient_zscore_df, - aes(16, label = paste0("Z=", round(Z_score, 2))), - hjust = "left", vjust = +0.2, size = 3, na.rm = TRUE) + + geom_text(data = patient_zscore_df, + aes(16, label = paste0("Z=", round(Z_score, 2))), + hjust = "left", vjust = +0.2, size = 3, na.rm = TRUE) + # Set colour for the Z-score of the selected patient - scale_fill_gradientn( - colors = colors_plot, values = NULL, space = "Lab", na.value = "grey50", guide = "colourbar", - aesthetics = "colour" - ) + + scale_fill_gradientn(colors = colors_plot, values = NULL, space = "Lab", + na.value = "grey50", guide = "colourbar", aesthetics = "colour") + # Add labels to the axis labs(x = "Z-scores", y = "Metabolites", subtitle = sub_perpage, color = "z-score") + # Add a title to the page ggtitle(label = paste0("Results for patient ", patient_id)) + - # Set theme: size and font type of y-axis labels, remove legend and make the - theme( - axis.text.y = element_text(family = "Courier", size = 6), - legend.position = "none", - plot.caption = element_text(size = rel(fontsize)) - ) + + # Set theme: size and font type of y-axis labels, remove legend and make the + theme(axis.text.y = element_text(family = "Courier", size = 6), + legend.position = "none", + plot.caption = element_text(size = rel(fontsize))) + # Set y-axis to set order scale_y_discrete(limits = y_order) + # Limit the x-axis to between -5 and 20 xlim(-5, 20) + # Set grey vertical lines at -2 and 2 geom_vline(xintercept = c(-2, 2), col = "grey", lwd = 0.5, lty = 2) - - + + return(ggplot_object) } #' Run the dIEM algorithm (DOI: 10.3390/ijms21030979) #' -#' @param expected_biomarkers_df: table with information for HMDB codes about IEMs (dataframe) +#' @param expected_biomarkers_df: table with information for HMDB codes about IEMs (dataframe) #' @param zscore_patients: dataframe containing Z-scores for patient (dataframe) #' #' @returns probability_score: a dataframe with probability scores for IEMs for each patient (dataframe) run_diem_algorithm <- function(expected_biomarkers_df, zscore_patients_df, sample_cols) { # Rank the metabolites for each patient individually - ranking_patients <- zscore_patients_df %>% + ranking_patients <- zscore_patients_df %>% mutate(across(-c(HMDB_code, HMDB_name), rank_patient_zscores)) - + ranking_patients <- merge(x = expected_biomarkers_df, y = ranking_patients, by.x = c("HMDB_code"), by.y = c("HMDB_code")) - + zscore_expected_df <- merge(x = expected_biomarkers_df, y = zscore_patients_df, by.x = c("HMDB_code"), by.y = c("HMDB_code")) - + # Change Z-score to zero for specific cases zscore_expected_df <- zscore_expected_df %>% mutate(across( all_of(sample_cols), @@ -497,34 +494,31 @@ run_diem_algorithm <- function(expected_biomarkers_df, zscore_patients_df, sampl TRUE ~ .x ) )) - + # Sort both dataframes on HMDB_code for calculating the metabolite score zscore_expected_df <- zscore_expected_df[order(zscore_expected_df$HMDB_code), ] ranking_patients <- ranking_patients[order(ranking_patients$HMDB_code), ] - + # Set up dataframe for the metabolite score, copy zscore_expected_df for biomarker info metabolite_score_info <- zscore_expected_df # Calculate metabolite score: Z-score/(Rank * 0.9) metabolite_score_info[sample_cols] <- zscore_expected_df[sample_cols] / (ranking_patients[sample_cols] * 0.9) - + # Calculate the weighted score: metabolite_score * Total_Weight - metabolite_weight_score <- metabolite_score_info %>% + metabolite_weight_score <- metabolite_score_info %>% mutate(across( all_of(sample_cols), ~ .x * Total_Weight )) %>% arrange(desc(Disease), desc(Absolute_Weight)) - + # Calculate the probability score for each disease - Mz combination probability_score <- metabolite_weight_score %>% - filter( - !duplicated(select(., Disease, M.z)) | - !duplicated(select(., Disease, M.z), fromLast = FALSE) - ) %>% - group_by(Disease) %>% + filter(!duplicated(select(., Disease, M.z)) | + !duplicated(select(., Disease, M.z), fromLast = FALSE)) %>% + group_by(Disease) %>% summarise(across(all_of(sample_cols), sum), .groups = "drop") - - + # Set probability score to 0 for Z-scores == 0 for (sample_col in sample_cols) { # Get indexes of Zscore that equal 0 @@ -534,9 +528,9 @@ run_diem_algorithm <- function(expected_biomarkers_df, zscore_patients_df, sampl # Set probabilty of these diseases to 0 probability_score[probability_score$Disease %in% diseases_zero, sample_col] <- 0 } - + colnames(probability_score) <- gsub("_Zscore", "_prob_score", colnames(probability_score)) - + return(probability_score) } @@ -548,15 +542,15 @@ run_diem_algorithm <- function(expected_biomarkers_df, zscore_patients_df, sampl rank_patient_zscores <- function(zscore_col) { # Create ranking column with default NA values ranking <- rep(NA_real_, length(zscore_col)) - + # Get indexes for negative and positive rows neg_indexes <- which(zscore_col <= 0) pos_indexes <- which(zscore_col > 0) - + # Rank the negative and positive Zscores ranking[neg_indexes] <- dense_rank(zscore_col[neg_indexes]) ranking[pos_indexes] <- dense_rank(-zscore_col[pos_indexes]) - + return(ranking) } @@ -565,12 +559,12 @@ rank_patient_zscores <- function(zscore_col) { #' @param probability_score: a dataframe containing probability scores for each patient (dataframe) #' @param output_dir: location where to save the Excel file (string) #' @param run_name: name of the run, for the file name (string) -save_prob_scores_to_Excel <- function(probability_score, output_dir, run_name) { +save_prob_scores_to_excel <- function(probability_score, output_dir, run_name) { # Create conditional formatting for output Excel sheet. Colors according to values. wb <- createWorkbook() addWorksheet(wb, "Probability Scores") writeData(wb, "Probability Scores", probability_score) - conditionalFormatting(wb, "Probability Scores", cols = 2:ncol(probability_score), rows = 1:nrow(probability_score), + conditionalFormatting(wb, "Probability Scores", cols = 2:ncol(probability_score), rows = seq_len(nrow(probability_score)), type = "colourScale", style = c("white", "#FFFDA2", "red"), rule = c(1, 10, 100)) saveWorkbook(wb, file = paste0(output_dir, "/dIEM_algoritme_output_", run_name, ".xlsx"), overwrite = TRUE) rm(wb) From 8094aede0a4bdfe3998e4fcee546785cbd214540 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 21 Aug 2025 12:15:08 +0200 Subject: [PATCH 04/30] Added new package for unit testing --- .github/workflows/dims_test.yml | 2 +- DIMS/tests/testthat.R | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dims_test.yml b/.github/workflows/dims_test.yml index fa2a8aa2..b547d126 100644 --- a/.github/workflows/dims_test.yml +++ b/.github/workflows/dims_test.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v4 - name: Install dependencies - run: Rscript -e "install.packages(c('testthat', 'withr'))" + run: Rscript -e "install.packages(c('testthat', 'withr', 'vdiffr'))" - name: Run tests run: Rscript tests/testthat.R diff --git a/DIMS/tests/testthat.R b/DIMS/tests/testthat.R index 3f13a069..636acf7d 100644 --- a/DIMS/tests/testthat.R +++ b/DIMS/tests/testthat.R @@ -1,6 +1,7 @@ # Run all unit tests library(testthat) library(withr) +library(vdiffr) # enable snapshots local_edition(3) From 62160b2095f45a79bbf43d64ababe2b577db1a22 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 21 Aug 2025 12:17:34 +0200 Subject: [PATCH 05/30] Added unit tests and associated files for GenerateViolinPlots --- .../generate_qc_output/test_barplot.new.png | Bin 0 -> 46638 bytes .../test_excel_dIEM.xlsx | Bin 0 -> 6754 bytes .../violin-plot-p2025m1.svg | 74 ++++ .../violin_pdf_P2025M1.new.pdf | Bin 0 -> 28962 bytes .../violin_pdf_P2025M1.pdf | Bin 0 -> 28962 bytes .../make_test_data_GenerateViolinPlots.R | 290 +++++++++++++ .../test_acyl_carnitines_controls.txt | 11 + .../fixtures/test_acyl_carnitines_df.txt | 21 + .../test_acyl_carnitines_patients.txt | 11 + .../fixtures/test_crea_gua_controls.txt | 11 + .../testthat/fixtures/test_crea_gua_df.txt | 21 + .../fixtures/test_crea_gua_patients.txt | 11 + .../fixtures/test_df_metabs_helix.txt | 16 + .../fixtures/test_expected_biomarkers_df.txt | 16 + .../fixtures/test_intensities_zscore_df.txt | 7 + .../test_acyl_carnitines.txt | 4 + .../test_metabolite_groups/test_crea_gua.txt | 3 + .../fixtures/test_probability_score_df.txt | 8 + .../fixtures/test_zscore_patient_df.txt | 31 ++ .../testthat/test_generate_violin_plots.R | 397 ++++++++++++++++++ 20 files changed, 932 insertions(+) create mode 100644 DIMS/tests/testthat/_snaps/generate_qc_output/test_barplot.new.png create mode 100644 DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx create mode 100644 DIMS/tests/testthat/_snaps/generate_violin_plots/violin-plot-p2025m1.svg create mode 100644 DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.new.pdf create mode 100644 DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.pdf create mode 100644 DIMS/tests/testthat/fixtures/make_test_data_GenerateViolinPlots.R create mode 100644 DIMS/tests/testthat/fixtures/test_acyl_carnitines_controls.txt create mode 100644 DIMS/tests/testthat/fixtures/test_acyl_carnitines_df.txt create mode 100644 DIMS/tests/testthat/fixtures/test_acyl_carnitines_patients.txt create mode 100644 DIMS/tests/testthat/fixtures/test_crea_gua_controls.txt create mode 100644 DIMS/tests/testthat/fixtures/test_crea_gua_df.txt create mode 100644 DIMS/tests/testthat/fixtures/test_crea_gua_patients.txt create mode 100644 DIMS/tests/testthat/fixtures/test_df_metabs_helix.txt create mode 100644 DIMS/tests/testthat/fixtures/test_expected_biomarkers_df.txt create mode 100644 DIMS/tests/testthat/fixtures/test_intensities_zscore_df.txt create mode 100644 DIMS/tests/testthat/fixtures/test_metabolite_groups/test_acyl_carnitines.txt create mode 100644 DIMS/tests/testthat/fixtures/test_metabolite_groups/test_crea_gua.txt create mode 100644 DIMS/tests/testthat/fixtures/test_probability_score_df.txt create mode 100644 DIMS/tests/testthat/fixtures/test_zscore_patient_df.txt create mode 100644 DIMS/tests/testthat/test_generate_violin_plots.R diff --git a/DIMS/tests/testthat/_snaps/generate_qc_output/test_barplot.new.png b/DIMS/tests/testthat/_snaps/generate_qc_output/test_barplot.new.png new file mode 100644 index 0000000000000000000000000000000000000000..58f016c18244ebec324251a250268171c93e9633 GIT binary patch literal 46638 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 C+t(xj literal 0 HcmV?d00001 diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx b/DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..fa6d905118380c37c24a2dd07c5009e9e5c1a20f GIT binary patch literal 6754 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Z=0.31 +Z=2.34 + + + + +metab3 +metab1 + + + + + + + + +-5 +0 +5 +10 +15 +20 +Z-scores +Metabolites +test acyl carnitines +Results for patient P2025M1 + + diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.new.pdf b/DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.new.pdf new file mode 100644 index 0000000000000000000000000000000000000000..fc5c3d6b710f85266785259b8e01c0c48e6b2e1b GIT binary patch literal 28962 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 amu=|i% select(HMDB_code, HMDB_name, any_of(test_patient_cols)) + + expect_type(combine_metab_info_zscores(test_metab_list_all, test_zscore_patients_df), "list") + expect_identical(names(combine_metab_info_zscores(test_metab_list_all, test_zscore_patients_df)), + c("test_acyl_carnitines", "test_crea_gua")) + + expect_identical(colnames(combine_metab_info_zscores(test_metab_list_all, test_zscore_patients_df)$test_acyl_carnitines), + c("HMDB_name", "Sample", "Z_score")) + expect_identical((combine_metab_info_zscores(test_metab_list_all, + test_zscore_patients_df)$test_acyl_carnitines$Z_score), + c(0.31, 2.34, 2.45, 1.45, 2.14, -1.44, 12.18, -0.18, 3.22, -3.18)) + expect_identical(as.character(combine_metab_info_zscores(test_metab_list_all, + test_zscore_patients_df)$test_acyl_carnitines$Sample), + c("P2025M1", "P2025M1", "P2025M2", "P2025M2", "P2025M3", + "P2025M3", "P2025M4", "P2025M4", "P2025M5", "P2025M5")) + expect_equal(nchar(combine_metab_info_zscores(test_metab_list_all, + test_zscore_patients_df)$test_acyl_carnitines$HMDB_name[1]), + 45) + + expect_identical(colnames(combine_metab_info_zscores(test_metab_list_all, test_zscore_patients_df)$test_crea_gua), + c("HMDB_name", "Sample", "Z_score")) + expect_identical((combine_metab_info_zscores(test_metab_list_all, + test_zscore_patients_df)$test_crea_gua$Z_score), + c(0.84, -0.46, -0.15, -1.51, -0.78, 1.68, 0.84, 1.48, 0.47, 1.18)) + expect_identical(as.character(combine_metab_info_zscores(test_metab_list_all, + test_zscore_patients_df)$test_crea_gua$Sample), + c("P2025M1", "P2025M1", "P2025M2", "P2025M2", "P2025M3", + "P2025M3", "P2025M4", "P2025M4", "P2025M5", "P2025M5")) + expect_equal(nchar(combine_metab_info_zscores(test_metab_list_all, + test_zscore_patients_df)$test_crea_gua$HMDB_name[1]), + 45) +}) + +testthat::test_that("Combine patient and control data for each page of the violinplot pdf", { + test_acyl_carnitines_pat <- read.delim(test_path("fixtures/", "test_acyl_carnitines_patients.txt")) + test_acyl_carnitines_ctrl <- read.delim(test_path("fixtures/", "test_acyl_carnitines_controls.txt")) + + test_crea_gua_pat <- read.delim(test_path("fixtures/", "test_crea_gua_patients.txt")) + test_crea_gua_ctrl <- read.delim(test_path("fixtures/", "test_crea_gua_controls.txt")) + + test_metab_interest_sorted <- list(test_acyl_carnitines_pat, test_crea_gua_pat) + names(test_metab_interest_sorted) <- c("test_acyl_carnitines", "test_crea_gua") + + test_metab_interest_contr <- list(test_acyl_carnitines_ctrl, test_crea_gua_ctrl) + names(test_metab_interest_contr) <- c("test_acyl_carnitines", "test_crea_gua") + + test_nr_plots_perpage <- 1 + test_nr_pat <- 5 + test_nr_contr <- 5 + + expect_type(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, test_nr_contr), + "list") + expect_equal(length(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, test_nr_contr)), + 4) + expect_identical(names(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, test_nr_contr)), + c("test_acyl_carnitines_1", "test_acyl_carnitines_2", "test_crea_gua_1", "test_crea_gua_2")) + expect_identical(unique(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, + test_nr_contr)$test_acyl_carnitines_1$HMDB_name), + c("metab1 ")) + expect_identical(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, + test_nr_contr)$test_acyl_carnitines_1$Sample, + c("P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5", "C101.1", "C102.1", "C103.1", "C104.1", "C105.1")) + + test_nr_plots_perpage <- 2 + + expect_equal(length(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, test_nr_contr)), + 2) + expect_identical(names(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, test_nr_contr)), + c("test_acyl_carnitines_1", "test_crea_gua_1")) + expect_identical(unique(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, + test_nr_contr)$test_acyl_carnitines_1$HMDB_name), + c("metab1 ", "metab3 ")) + expect_identical(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, + test_nr_contr)$test_acyl_carnitines_1$Sample, + c("P2025M1", "P2025M1", "P2025M2", "P2025M2", "P2025M3", + "P2025M3", "P2025M4", "P2025M4", "P2025M5", "P2025M5", + "C101.1", "C101.1", "C102.1", "C102.1", "C103.1", "C103.1", "C104.1", "C104.1", "C105.1", "C105.1")) +}) + +testthat::test_that("Generate a dataframe with information for Helix", { + test_acyl_carnitines_pat <- read.delim(test_path("fixtures/", "test_acyl_carnitines_patients.txt")) + test_crea_gua_pat <- read.delim(test_path("fixtures/", "test_crea_gua_patients.txt")) + + test_metab_interest_sorted <- list(test_acyl_carnitines_pat, test_crea_gua_pat) + names(test_metab_interest_sorted) <- c("test_acyl_carnitines", "test_crea_gua") + + test_acyl_carnitines_df <- read.delim(test_path("fixtures/test_metabolite_groups/", "test_acyl_carnitines.txt")) + test_crea_gua_df <- read.delim(test_path("fixtures/test_metabolite_groups/", "test_crea_gua.txt")) + + test_metab_list_all <- list(test_acyl_carnitines_df, test_crea_gua_df) + names(test_metab_list_all) <- c("test_acyl_carnitines", "test_crea_gua") + + expect_identical(colnames(get_patient_data_to_helix(test_metab_interest_sorted, test_metab_list_all)), + c("HMDB_name", "Sample", "Z_score", "Helix_naam", "high_zscore", "low_zscore")) + expect_equal(dim(get_patient_data_to_helix(test_metab_interest_sorted, test_metab_list_all)), + c(15, 6)) + expect_identical(unique(get_patient_data_to_helix(test_metab_interest_sorted, test_metab_list_all)$HMDB_name), + c("metab1", "metab4", "metab11")) + expect_false("ratio1" %in% get_patient_data_to_helix(test_metab_interest_sorted, test_metab_list_all)$HMDB_name) + expect_equal(get_patient_data_to_helix(test_metab_interest_sorted, test_metab_list_all)$Z_score, + c(0.31, 2.45, 2.14, 12.18, 3.22, 0.84, -0.46, -0.15, -1.51, -0.78, 1.68, 0.84, 1.48, 0.47, 1.18)) +}) + +testthat::test_that("Check for diagnostic patients", { + test_patient_column <- c("P2025M1", "P2025M2", "P2025M3", "P2025M4", "C101.1", "C102.1", "P2025D1", "P225M1") + + expect_equal(is_diagnostic_patient(test_patient_column), c(TRUE, TRUE, TRUE, TRUE, FALSE, FALSE, FALSE, FALSE)) + expect_equal(length(is_diagnostic_patient(test_patient_column)), 8) +}) + +testthat::test_that("Adding labnummer and Onderzoeksnummer to the Helix dataframe", { + test_df_metabs_helix <- read.delim(test_path("fixtures/", "test_df_metabs_helix.txt")) + + test_df_metabs_helix <- test_df_metabs_helix %>% + group_by(Sample) %>% + mutate(Vial = cur_group_id()) %>% + ungroup() + + expect_true("labnummer" %in% colnames(add_lab_id_and_onderzoeksnr(test_df_metabs_helix))) + expect_true("Onderzoeksnummer" %in% colnames(add_lab_id_and_onderzoeksnr(test_df_metabs_helix))) + expect_identical(unique(add_lab_id_and_onderzoeksnr(test_df_metabs_helix)$labnummer), + c("2025M1", "2025M2", "2025M3", "2025M4", "2025M5")) + expect_identical(unique(add_lab_id_and_onderzoeksnr(test_df_metabs_helix)$Onderzoeksnummer), + c("MB2025/1", "MB2025/2", "MB2025/3", "MB2025/4", "MB2025/5")) +}) + +testthat::test_that("Make the output for Helix", { + test_protocol_name <- "test_protocol_name" + + test_df_metabs_helix <- read.delim(test_path("fixtures/", "test_df_metabs_helix.txt")) + + expect_equal(dim(output_for_helix(test_protocol_name, test_df_metabs_helix)), + c(15, 6)) + expect_identical(colnames(output_for_helix(test_protocol_name, test_df_metabs_helix)), + c("Vial", "labnummer", "Onderzoeksnummer", "Protocol", "Name", "Amount")) + expect_identical(unique(output_for_helix(test_protocol_name, test_df_metabs_helix)$Protocol), + "test_protocol_name") + expect_identical(unique(output_for_helix(test_protocol_name, test_df_metabs_helix)$labnummer), + c("2025M1", "2025M2", "2025M3", "2025M4", "2025M5")) + expect_identical(unique(output_for_helix(test_protocol_name, test_df_metabs_helix)$Onderzoeksnummer), + c("MB2025/1", "MB2025/2", "MB2025/3", "MB2025/4", "MB2025/5")) + expect_equal(output_for_helix(test_protocol_name, test_df_metabs_helix)$Amount, + c(0.31, 2.45, 2.14, 12.18, 3.22, 0.84, -0.46, -0.15, -1.51, -0.78, 1.68, 0.84, 1.48, 0.47, 1.18)) +}) + +testthat::test_that("Create a dataframe with all metabolites that exceed the min and max Z-score cutoff", { + test_df_metabs_helix <- read.delim(test_path("fixtures/", "test_df_metabs_helix.txt")) + test_patient_id <- "P2025M1" + + expect_equal(dim(prepare_alarmvalues(test_patient_id, test_df_metabs_helix)), + c(2, 2)) + expect_equal(colnames(prepare_alarmvalues(test_patient_id, test_df_metabs_helix)), + c("Metabolite", "Z-score")) + + test_patient_id <- "P2025M2" + expect_equal(dim(prepare_alarmvalues(test_patient_id, test_df_metabs_helix)), + c(4, 2)) + expect_equal(prepare_alarmvalues(test_patient_id, test_df_metabs_helix)$`Z-score`, + c("", "2.45", "", "-1.51")) + + test_patient_id <- "P2025M4" + expect_equal(dim(prepare_alarmvalues(test_patient_id, test_df_metabs_helix)), + c(3, 2)) + expect_equal(prepare_alarmvalues(test_patient_id, test_df_metabs_helix)$`Z-score`, + c("", "12.18", "")) +}) + +testthat::test_that("Create a dataframe with the top 20 highest and top 10 lowest metabolites", { + test_zscore_patient_df <- read.delim(test_path("fixtures/", "test_zscore_patient_df.txt")) + test_patient_id <- "P2025M1" + + expect_equal(dim(prepare_toplist(test_patient_id, test_zscore_patient_df)), + c(32, 3)) + expect_equal(colnames(prepare_toplist(test_patient_id, test_zscore_patient_df)), + c("HMDB_ID", "Metabolite", "Z-score")) + expect_equal(prepare_toplist(test_patient_id, test_zscore_patient_df)$HMDB_ID, + c("Increased", "HMDB030", "HMDB029", "HMDB028", "HMDB027", "HMDB026", "HMDB025", "HMDB024", "HMDB023", + "HMDB022", "HMDB021", "HMDB020", "HMDB019", "HMDB018", "HMDB017", "HMDB016", "HMDB015", "HMDB014", "HMDB013", + "HMDB012", "HMDB011", "Decreased", "HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB005", "HMDB006", + "HMDB007", "HMDB008", "HMDB009", "HMDB010")) + expect_equal(prepare_toplist(test_patient_id, test_zscore_patient_df)$`Z-score`, + c("", "30", "29", "28", "27", "26", "25", "24", "23", "22", "21", "20", "19", "18", "17", "16", "15", + "14", "13", "12", "11", "", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10")) + + test_patient_id <- "P2025M2" + + expect_equal(prepare_toplist(test_patient_id, test_zscore_patient_df)$Metabolite, + c("", "metab1", "metab2", "metab3", "metab4", "metab5", "metab6", "metab7", "metab8", "metab9", "metab10", + "metab11", "metab12", "metab13", "metab14", "metab15", "metab16", "metab17", "metab18", "metab19", "metab20", + "", "metab30", "metab29", "metab28", "metab27", "metab26", "metab25", "metab24", "metab23", "metab22", + "metab21")) + expect_equal(prepare_toplist(test_patient_id, test_zscore_patient_df)$`Z-score`, + c("", "-1", "-2", "-3", "-4", "-5", "-6", "-7", "-8", "-9", "-10", "-11", "-12", "-13", "-14", "-15", + "-16", "-17", "-18", "-19", "-20", "", "-30", "-29", "-28", "-27", "-26", "-25", "-24", "-23", "-22", "-21")) +}) + +testthat::test_that("Create a pdf with a table of top metabolites and violin plots", { + local_edition(3) + temp_dir <- "./" + dir.create(paste0(temp_dir, "violin_plots/")) + + test_pdf_dir <- paste0(temp_dir, "violin_plots/") + test_patient_id <- "P2025M1" + test_explanation <- "Unit test Generate Violin Plots" + + test_acyl_carnitines_df <- read.delim(test_path("fixtures/", "test_acyl_carnitines_df.txt")) + attr(test_acyl_carnitines_df, "y_order") <- rev(unique(test_acyl_carnitines_df$HMDB_name)) + test_crea_gua_df <- read.delim(test_path("fixtures/", "test_crea_gua_df.txt")) + attr(test_crea_gua_df, "y_order") <- rev(unique(test_crea_gua_df$HMDB_name)) + + test_metab_perpage <- list(test_acyl_carnitines_df, test_crea_gua_df) + names(test_metab_perpage) <- c("test_acyl_carnitines", "test_crea_gua") + + test_top_metab_pt <- data.frame( + Metabolite = c("Increased", "metab1", "Decreased", "metab11"), + `Z-score` = c("", "2.45", "", "-1.51") + ) + + expect_silent(create_pdf_violin_plots(test_pdf_dir, test_patient_id, + test_metab_perpage, test_top_metab_pt, test_explanation)) + + expect_true(file.exists(paste0(test_pdf_dir, "R_P2025M1.pdf"))) + expect_snapshot_file(paste0(test_pdf_dir, "R_P2025M1.pdf"), "violin_pdf_P2025M1.pdf") + + unlink(test_pdf_dir, recursive = TRUE) +}) + +testthat::test_that("Create a violin plot", { + test_patient_id <- "P2025M1" + test_sub_perpage <- "test acyl carnitines" + + test_acyl_carnitines_df <- read.delim(test_path("fixtures/", "test_acyl_carnitines_df.txt")) + attr(test_acyl_carnitines_df, "y_order") <- rev(unique(test_acyl_carnitines_df$HMDB_name)) + + test_patient_zscore_df <- test_acyl_carnitines_df %>% filter(Sample == test_patient_id) + + test_metab_zscores_df <- test_acyl_carnitines_df %>% filter(Sample != test_patient_id) + + expect_silent(create_violin_plot(test_metab_zscores_df, test_patient_zscore_df, test_sub_perpage, test_patient_id)) + + expect_doppelganger("violin_plot_P2025M1", create_violin_plot(test_metab_zscores_df, test_patient_zscore_df, + test_sub_perpage, test_patient_id)) +}) + +testthat::test_that("Run dIEM algorithm", { + test_expected_biomarkers_df <- read.delim(test_path("fixtures/", "test_expected_biomarkers_df.txt")) + test_zscore_patient_df <- read.delim(test_path("fixtures/", "test_zscore_patient_df.txt")) + test_sample_cols <- c("P2025M1", "P2025M2", "P2025M3", "P2025M4") + + expect_equal(dim(run_diem_algorithm(test_expected_biomarkers_df, test_zscore_patient_df, test_sample_cols)), + c(7, 5)) + expect_identical(colnames(run_diem_algorithm(test_expected_biomarkers_df, test_zscore_patient_df, test_sample_cols)), + c("Disease", "P2025M1", "P2025M2", "P2025M3", "P2025M4")) + expect_identical(run_diem_algorithm(test_expected_biomarkers_df, test_zscore_patient_df, test_sample_cols)$Disease, + c("Disease A", "Disease B", "Disease C", "Disease D", "Disease E", "Disease F", "Disease G")) + expect_equal(run_diem_algorithm(test_expected_biomarkers_df, test_zscore_patient_df, test_sample_cols)$P2025M1, + c(10.94172, 0.95343, 12.12121, 0.00000, 44.28850, 0.00000, -38.70370), tolerance = 0.0001) +}) + +testthat::test_that("Ranking Z-scores for a patient", { + test_zscore_col <- c(1, 5, 6, 2, 7, -2, 3) + + expect_equal(length(rank_patient_zscores(test_zscore_col)), 7) + + expect_identical(rank_patient_zscores(test_zscore_col), + c(6, 3, 2, 5, 1, 1, 4)) + + test_zscore_col <- c(3, 2, 1, 3) + + expect_identical(rank_patient_zscores(test_zscore_col), + c(1, 2, 3, 1)) + + test_zscore_col <- c(-1, -2, -3, -4) + + expect_identical(rank_patient_zscores(test_zscore_col), + c(4, 3, 2, 1)) +}) + +testthat::test_that("Saving the probability score dataframe as an Excel file", { + local_edition(3) + test_probability_score_df <- read.delim(test_path("fixtures/", "test_probability_score_df.txt")) + test_output_dir <- "./test_excel" + dir.create(test_output_dir) + + test_run_name <- "test_run" + + expect_silent(save_prob_scores_to_excel(test_probability_score_df, test_output_dir, test_run_name)) + expect_true(file.exists(paste0(test_output_dir, "/dIEM_algoritme_output_", test_run_name, ".xlsx"))) + + expect_snapshot_file(paste0(test_output_dir, "/dIEM_algoritme_output_", test_run_name, ".xlsx"), "test_excel_dIEM.xlsx") + + unlink(test_output_dir, recursive = TRUE) +}) From cf9f3493759d9a57dcb038bcde8e914a9c6284ed Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 21 Aug 2025 13:54:56 +0200 Subject: [PATCH 06/30] Fixed snapshot issues GenerateViolinPlots --- .../test_excel_dIEM.xlsx | Bin 6754 -> 6752 bytes .../violin-plot-p2025m1.svg | 4 ++-- .../violin_pdf_P2025M1.new.pdf | Bin 28962 -> 0 bytes .../violin_pdf_P2025M1.pdf | Bin 28962 -> 28932 bytes 4 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.new.pdf diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx b/DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx index fa6d905118380c37c24a2dd07c5009e9e5c1a20f..e1a75d59aa1f5b86eb270ba6265b0a264a876978 100644 GIT binary patch delta 842 zcmaE4^1y^Qz?+#xgn@&DgCQwSbR(|!XK!-ERsN7UbeeI-fD0{fCYURDBd>Ntj)6BOpPl;Nzgjx5yaNnHDn4k6TS*D7A6}Kom zXmjb7Bp3#T8B~2#YSH^ADKYhAWlvhs*V_0mn=ek6GW@lxu0rbc^J9GSD;rFlwAV3t zlq}>FsZ2UXzNchEKnUZO*3T%FExwie2(eB;%*LVa^ zy=r2kFJ5`ntmmWUa@+`j0_BFObiStA-Y+PRSOcN z-fW!^#&vcf2;&V02NO87zjO7of*G9xhaggZLb?z}o$wZBIbZ}tC%n16^%VmHgE!Cy zZXgW6@W_ap|Ub`_?!yM?#DL}pIDB5i%PmnN^2f6dw@16~p zfpYtR?i4{$b7JyS31zSy+>%nZjdwX17%Fn~i%TkVQj7H}a&ypa>zTpN8pz7PuvnUb zK^VoP7n74Dm101)B^io5?*O{!ATt94KZ=?v0kHAq`9;}D`T5z{EGT!6_;4SnDFx_M nWfaZ!lOISLfxR>B%oL$~py8IlFqc44lRw!|N}EkZ93%(;2{r_w delta 840 zcmaE0^2mfYz?+#xgn@&DgJESW|3+Q~MrI(r*?@5cm@)Y_(+iNmNzIe_}?mtn`K7q{=Hi+?{e+8K2-hwICuZrzTC+*O`lJ_ z5Spmt&Y6+83;x+wStECPxfj2@CPSzXmzK^3nR9Z^Iq@lX_VN42KhbW}XJ5CEA;6oRBi&%pK}kji1`Q?#ARj4AH_NbU zK?2p2trNny$}R+9yyDcs%h)XduDl<;DXB3@0Pn-uVwpLtD*sw-4w}5fnA2CO?)?jsmHXecs1=0ccJ-3j+f$ikkZz3=9=H`o$%cIjO~Z6}dU+ zPUxAz&l#NBLl1gAFBpHf4?*JNqkePvjA4N^g - - + + Z=0.31 diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.new.pdf b/DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.new.pdf deleted file mode 100644 index fc5c3d6b710f85266785259b8e01c0c48e6b2e1b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28962 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 From bd128ddb18a967912b63d49e2d9c35a5bb662d6d Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 21 Aug 2025 14:04:10 +0200 Subject: [PATCH 07/30] Changes to snapshots --- .../generate_qc_output/test_barplot.new.png | Bin 46638 -> 0 bytes .../test_excel_dIEM.xlsx | Bin 6752 -> 6752 bytes .../violin_pdf_P2025M1.pdf | Bin 28932 -> 28932 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 DIMS/tests/testthat/_snaps/generate_qc_output/test_barplot.new.png diff --git a/DIMS/tests/testthat/_snaps/generate_qc_output/test_barplot.new.png b/DIMS/tests/testthat/_snaps/generate_qc_output/test_barplot.new.png deleted file mode 100644 index 58f016c18244ebec324251a250268171c93e9633..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46638 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 C+t(xj diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx b/DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx index e1a75d59aa1f5b86eb270ba6265b0a264a876978..e9ce8ca1f1818b9e66940f6917ebdec53e0065be 100644 GIT binary patch delta 502 zcmaE0^1y^Az?+#xgn@&DgCQVcBab2@Q$WIIea4j_dh#u%mmumWvub^;_hAEx*8g57 zYiHLj=$TNqV3Rg`lkm~q6ZYJ0$`zVaUiSC{#D8v(+VzlNt_bT#RBil~kCJdU)yA4^P%I_e(hmDXLvb>3<=y)bZUy zVa4uEou{t-@=LJ!C0puL*CBhssQ!F@CcDNI2S@j)S7|bqA7_+=yK7l0-#xfp*uj%a zwzir4xc%#6rPll`lnL@My}=G(&I~$*K+LWv(7PHvHM}pZ|#ro7cI9~p>i$JV#kha{hy-l+UJBV(fs_$ zSnaHMW@Jv+q^GK@SAGBZw7cf;%cpzJiU)YJb7HC&Q4>g_Z)w2cjyZINZ-DFdfBa=FP2U(IQP@yVaL;r*_-(}E?Ptj zhfPsoUOmUGEPV3wvs&>JbQW!i+IG&#*H7T&Cc#sz6+Gvh9heQh1*Ee#xqq2+X~nk- zCnHpDs-?bmQa6-6+*h^o-c!De(E4fSTbQRrEn32?`(3ziPG!u`diN|-#lMPMlpVCW zbW0KpgTf4|J}R~7{gafKda|-7t>|lQ{Fluar%M_B+ErH}b^7@+KKYdmCQjPxm^?}r zatiX_vT&@w!}+vQTY24OzG&uO-%2+AopvPrVzErgv{wZ-Ket@l{>f>SaWsSW^qOa1~n!I2ISD(EX}G74o(lY zE--zCT^LNiR2xDXP$^rm>L Date: Thu, 21 Aug 2025 14:20:54 +0200 Subject: [PATCH 08/30] Fixed snapshot issues, second try --- .../generate_violin_plots/test_excel_dIEM.xlsx | Bin 6752 -> 0 bytes .../violin_pdf_P2025M1.pdf | Bin 28932 -> 0 bytes .../tests/testthat/test_generate_violin_plots.R | 10 ++++++---- 3 files changed, 6 insertions(+), 4 deletions(-) delete mode 100644 DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx delete mode 100644 DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.pdf diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx b/DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx deleted file mode 100644 index e9ce8ca1f1818b9e66940f6917ebdec53e0065be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6752 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 M%!qtqB)vKPAMFfoo&W#< diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.pdf b/DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.pdf deleted file mode 100644 index 799d6504f3363a2fa7a32ff102881aa2cd4cbaba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28932 zcma&N3p~@`|2W>2uGZDvR!I^WNvPQ>m5|n5MH{K)lDU;(m&&_TV#OOtSyE9h3kjJm ziIK9(J!}|;VYb=*_J8$xe1G5HEQ7(8LEWM*2gC$p!Vd(*T;8zt!0sJ(I~?tt zci7tl35Ok4%b?zvOaH%hr-Ea`fL13qxJLy8}(!{#%+2&{J^{mp4G&0%8KfFhLuj#{z;bM{jTd z5Ke6PpADS;w+4Ul{$fX64v+aSbf_C9JO;={Z*cxkbLff7(U|K|fdIXo{|mjp2>)X^ zj$958in+RBr;GCj=%L`S7@z~tLt((!9Jm~axpeuj*wHb-kX>7beb$(99Mf*MiosS} z&6ME>XPsU(TRGD(vb9IQv zX-;QjP|KPI^mY6~4Kz6@Mz^JzK$cxCheL zEi5|;yENziGN&vSvZvAQ5OH(cNMJtpb2VZOS5S5iM(r-)f8SbkakBg-qH&EV@v*y2 zZQ?p<9ON6?^Wsh$5##WVtBVsoXgREK!uA{TRpqba$-gCwq-8LH7yynH0Ur`fRUz1eh=VDGK>bDPERIfdgBs}%`IsZ@C z5NfwbJY$9n_;F)@gGqCf<5@9i0p_1FKwtlB?*B`3UH)5f{)guN9~IHw?*FEV|H1fQ zRMFnvai{(NT^UQBx3*cdTz}3LuM0Z7^6UfU<-w&(edo-RzXT|76o&VU}v><$k?J;)M%1hqy?A!}x!TBbGrsE78$`B`mK z8`&#fEvTXpvFf3AG*K%hiYmIR*u-z#v+=H4wZ<2skvkEGXPppQUlm0p#*hhW5@k+N zfCNa2$N(a>ABs?CdugXqp%hKc51$|8SVG2)&64h|6VcbS;rAv)wxeQ|ftyOSKqE(d zx-(bPE#3YKRCP>vd&5#~n{&L}Pr+*~z`!$(!?|+!KACgg8%-QPVgI1$pN|NN5Ga8K zbTjkm$W`#8vRVB(nCEV-2+pnA0heAJlkd(ic>rNK+>UDklYQ&5R^V7Sykr#V$2-im zXOz#nl#7N$rQg2hCHQ>dE_ER0^4%c-gHcTl31HC897P_Pa5!UIwgt`#sLh>>#R?HFnNQ*=~;aa*7A;&B|(^K#)1cea$iZHm}s&*^TAA92pTjvMVukqa_3rM0Fpi zB~=}4bCglXGuz(#%w8QC=B(wEJUKL+(ISA$H~H>v-@2IZ5pK-?ynjim+%aP4#+h4& z%+CjO7+4+D5wE4~8?cYPK=9i<76HCcF|RZ;b}nz`PVh!J$hbc}Ed-US-NObXED&BT`NxZFRzhF7Yg5rgNFCrETSOn zSt6?M?BqyzTy2^gLLZEC-GhQL7vtw;t$$|E`iHT>Hqn%iG+_=IYH!0kDoZf9_k&UL zW?!Btq~0C!`3Hjpze%CygRXm1Fv+cgp(y#o*p(!V8|3pfT}a~shB|!Q&DFQnroQq~ zAnk8jxp!Q27k;yLG{7F=r%DKm4?>>TsEbEF-uBtT4Qs;k<16w&gP!s`pf%6VM>Jv8 z1ih^NU8*npQw49%g6flDs)X?d&$3*zyYM5ZGq^3guxG(@L$j_BgnxO z)KHsy0a1(96+hn><;STF0UGvb9r65U-*ZeA&V`_)Kmus8VJKV<2z;Io44KZ4a{rcO z0TL*DLQGfKQF9@-#u$nGG$%#w@)k6B7mpgTu@5YIZX#PkYGFECKg@z zoiwTT_kg^Url;uY+B!IgQtinf0pj?uiNtAf!)eY+HS0e(Zjmk9u~(_R!n;j5+c6>3 z7GY*n4ve-vuG~H3_Rx{i`g5EN%`HdNG&Jcg$YRXh`*+eBMFb$sMb}b4Y)d8*zcqjf zc?0jsg&)Ch&Ex;Yk|Z_w3O}$;W1kFPK@G2Y=7^f$480VD5WuCz{Acyv!Z)K%DLzV% z9{??$bSO^{0*B-p^WRfn3-nrj)0|+m5mVk;TCIf=ieIvtx^-L3kMqw=1WlsTGuXv!A z+5r|j#|Lyh37|QzV9+9NfX+@a<6YW!ls7_N4WQx55HYke23&67qAJ zOq8tvhLOehU=0f@gplj!Rt&O_2E-gswI#a5ysLM;EO8?mx9 ztw61gXvcEY*8sz)W&_pFY%o^=1$+U*%kbaGGpQIe)_p3lBrJTwFjDS;P!(LsFGCO z0`$Ng;K-Lki3(T52r>}<1VSOUu7;-OAAJL0V@Wg6;8a0NS1FV8Y_L4 z=Kf)RB0;Bv+8I?62RZRtGs=^I7(!YyrLzU;e5h(-ozk0g;5B|olrwe58Zq^Hgq>8k zj*00(g=88?(xxgJid?7an(kyuUSajy-7OV61`$x!)s+{sH zQ^d7ya&9I>GbiB}sECbKdQdqp7Uru_%*uT=#Brpoy)E{`Y(c^ldppoY_23a|5N`29 zL23_EubpT}Bgop#r|#=(=9VaHw==RX;(dFnjfkFO2Xi&9YF)17d0Bfa&qvmt%KSK6 z@SJ~jXR#sC!Kab`sm6*>_5Ej3_7~RQX?8-B1YPSS?L%*|`BMh3?la86alm3f4ULhv zw+m)$iSe0O%>~We2Q9YxNYhC^u$G@=0~^Jam3Y3eAZWR^u$7#f3c4ZR?%w^7tdx2SVyB$| z7DEa6@rM39Hjqr=0voCg#p!t}*m(x6ne!C6Yo1TyafE{mLxwZah(sg!{YT~jIkQZ($+NYU& zT84_zdnkXs#xH}EMlcKi<{nH0x|Ai_2oMqyevTrc6zTI@t|N>OWH9O*xdqPmYkq=h z7mpzMQ-W_~mW4z&l8n;E5XdP0W)O8u8IPAp#Z2sHsUnDq*F%$~3v)@zX_0{1CYpyJ z`!H~L7;+YmvUn`T(n;{xV%>POe`Ed}T$xRJz?{S<@^LN;`9bh2WTM{9T5ex95}(n- zbYy>yr9G1`p>^*ORYLiQr_8}29$C)H;?RHVMpUVGHuouN?OA&EKJ3r!)D2Ae;R);N z9Cyo5;$xcBQ4OmG-yEe}ts8ejzY{O&FXq2~oJAqqv&A8;-!3u&93C04Ai9R}qYyI* ze^?yC%9$&GhZd|{pm<+`F4z4W0QrrY#~>gR7ErGd_d+Bei3bxF_5bzoXp(KyMzbI5 zBDVUf;o4=2gZf$Mq!V0)! zN`7=Kv>GjwmLWEC6AM8Ne2ga~Tt)PR+!gh@DF$V4h2DchxULSuu4kYeU_T)u@ew#5 zxore~qyg~NF8o14n^<-l?MN?%#v}{R_IYj-D`7cF>jf+mBN}6NaY0(6H zFtt;zcq`C{K#Vbw8vIqN6PaxR%{35-FLBvLO*zkqP-Bcx?AeoV4LH4QxoWd=m+<*P zfS-A%`6FLDao%n^pk1ZzC+Zf;kAnmsNvEOn!KIkOa&i)HVy*cNakOD%)by8ZaScd|m_PIF2!gS7Qx zt0BZG5>%BmDY&COc7ami$Qq7+@mg?~U@?)%%R9w)BXlf$d;!zpmJ4{*PKe2r=2G2l z#+Z+)V!@cZ>)KxNA*+!ZR7es+T$r_~78L^c_+mddPDWi(yiJte8~D0zCe{jpX0S ziE)xb^-iEiMmw^GM9EEkctRJpmd8ySaqQNenI&F;R0G)3=f9JLrLoV9G2%#NyScM! zVwmI2sS}gDIX~biN0zVlFus|uVAyDhwYfW5wCGaML9~%d?OwN~SNzmVqn(v^=rW|9 zvUd3eey;_;E8#|?!Am&dFhp33stlp_it{ao*8}H~!!+`2L3=IA(Fz>J3w{f-nzHC@ z-vJjERxNhNUk3aOe#UHp8Tz;@RMsWk)#&pQ^<4b<9@C~3>Lti=ceOvOBu}Oo@grR$ zPQVI3Y2vKGr~Dvw>p-5R1-Z)A$P=zRu?lC7*GJ-MMTd!OEXH)YL-!Pe;YgEwrOe5bos3 z#$yokI3+A-CuW{4iLj{Yq+0oB9i~#XqgkqWSf!6(zr=2%96Kw7*Z$E>%NM$_7C|C{VZL6LYyw{!@MFmMASTc z!ajdxe4!%w@4h)?P1#D^g;N)+&e<1~;Bpuse6`wbvTC8Fbm4x;n`yL|cZ;pYKk@;q z@%M2`^YTc{>l( zAa@me-A-pF!C#P@xa2#l{rb^M@rG6CwB^uhmZr^t<%`ZEFbT@z9iU}LGqdbb+~iK= zvbGt_9r#1V4En#J@IPq)ni*mUQp$eFAqlnzy8P%2;|_cUC1|p0x=M3KU0t__^@eaE zLV(Q!75e-ZT^csAA)Zu!R(B=5LmT$vOcgid?SQ+NxgiZwVxE{za)W&0`F1c@bP-+? zxU4$FTZpkAfc0~L`H~P4A&wk(tEwv{ghfPao#ygtQ5V$xX2HxpZC#68LJXufng$MCZA@8v2ox!9mG*kS;uZlfdaIYeCaw~tV`D(=&@1*18s%mH5%+I|pi}e|odODmK7Bt6jcNT3U zZ+>L++1-h;ucm!}GMYCE^Qs6_gSf2Z=ZxgAB9iUJ=yKsy{QCiL<%L@M$+xRUGfcBj zUUbYgq80J8mp;3|4j!(^4T~M!`38L>a?RaeF+E4%ajR=jvhHIYhF4|y`9HSHi;be? zrqqSf+d0W+FZ_rGx1Q%;{AYhy)s34^{C3YfUb@Kr@`AGJ_qs2`oOsdeL#eGeV|yc} zq>k!w>l(eszij+=X75_iai)>$zWtDlt*)jqTk$E%`%?u^CFMFrH?JhW zXz@Q0^|7bD*V*le{R3eP{rqm%n$c+iJbqmWQ&#fe>T2WcTim892W|RB>zSvH)p|4KKl*iJ z1EwdpmYMZeigUL>;%V6#EqiXB-r%Kc+w^LEUN^2faiC>quP__)`tzlGyMlan9KHJ_ z`lCewa>K|}m5b52mYT+qT)*LH$yDx<;E^e#J;8(#>ZUYXacghU)GLdeEGf@jf&oUirnA)BO^N9 zinF5Cez6;hmy2A{J34msb<3Lie!u%Lc}qdRsXS-J>MQH|GyKm+x-vGo81*vWuef11 zay|B4!>Df6y<-2yd3!`}(?*I{9)o>u!{r|^nu4Z3J2(<%7x${U!-gQ-#%}x7_$UHQ zfFy6%Gg=-2x=a0a5e_}t4~zd!X#`ef4xV~zda2FbIZ?#xpBG=*a=Z+7A7yT2?ty=L zW)zZ>xDm5kNRusn410d=m*r()$=rpQ`NEhj>&P$WOM7-3xaq}xN(*&tso>T<9rW`~QkFl>d1E2Y{P8mTvFYZnmvy?CmQ>jW(G70O{>;IjN#1dNJPUZ<=C1e`X?-~Jit1H8 z=(OG=E;lP_*WPRus&{l;S+bQ~a+n;mz$bbwNVJNl+2v3Ymr(b4Z)5q7U2gyQmpJ7- z0G-)_sc3|-N7~~nlSfvPsyuV|Kw~ptu~9SI;z}3Qr&yGabzd1Zbsn)_v@FxUEVype z*Q4kWppiQN^w*cJ>-QF(nG!AOP3FXvy)v<(F9z+m(FRw1H8Ew<1BVucYRD$7gJw4b z;Iyhh*8w+Se|AOENN-i2P27Cz-{uwbi{&-h+2;H~+f?qJ0LWO5)tAIop~MrOcd3bP zxe~|#)}w{P{jv{L?^}HM$Cc$~ak@T{F!r^$MurEqI zL8JcLv9p^UUM&J!Igg|mn+ltj=*4vy%w`|SWZ)(t7i1UCN*p3P2FxPrV** zadaN}Wlq`lcmrwBhXO56I4nC`(xRlX6;eV+(Pb?)GLN34z%1WWXp z)$5WqWdxN9@ITk}Cq~bzKD*B*1~;d9MW{c!R_ic9RQmAs(QKYB{DFITy=Wny^29=p zjs*7Eg-nntgZf?nH>DasD7fVl{}3VCM{FEjSOp4;vyd6y0A0B0hClnILK zrxfkT9|8M)ApFt*UnZlp*}&lHK<(8-=M7(MFa16*iTWYY4JC~1$)9;Oznzub_U`!_3qLO*Rn)+;Qq6#PgD51h42ts zax>Xk?Eh`Kb{*_%EA{Ii?XkFWwq#B9z1o`lz98`MpW9j0>L@>BXBSXFL8Nl^I;Uq%^F8z1&|KfMGTZWj7AHuN{HFib2s%cO7V zC9OsmI(ngGkkc9IgRI2b>de84pEuK)>IC^{OkDNJh$4Z-DcNu*Q|)s ztO+mqzNj5CU4Ob*>XTiXh%o8S=VS~lUBhUzr;z6raZoPh3lR*sqh~e#^ zWu?Q_tW4?}-noG^1N4c-n6ep4_KaZGlYu)Uu{GP#4N_Pr5qE)Y+Hl`?D2=BZ0XcDM*}d$?&{Jv)bW2yQr0e|em__& z?=X}Dk|Y<;nG!VLJeh@G-#yOAT}>=}oNK-k{T$w+P zB|{eH`G9NcDDG{1!F&IhI)+y`UwMDQ?*#e?L*zd{n23PtKWdYTF3q2aNAS09sN*Lt z&KId^XYF>g`00!D#cEpq-nlki{eR$N%a`wOPk@&VR@8A#mct;T$BT0(T+#VAMQuU) zVVP1~(7r2p-ooW}B?#YQ@nOZ+1)jXMD*6c+x{!2yRh&3uF-cy+w7F`E!3Wj96w{N7 z#PkPzz7Zg-JQsyJu){N*#kgt|-fpLciLf>G$A z>UjJ?+y*E=$&Zr1ce<2oHhi0!)L3Yq~WKuk4D)$`v2_u31b zRrOYJ$Cs|s=r}haHd7jv8T192v=;JwA2Ryu?^wCp;H>)uG~*OU9?F>qRr+|trqS!m zhRMS2&qI`-Wb2t{%ZMPqAKo}MV-rmixF$(^)`HSQEKc*esD4EEJX(J_t$~b?7MgyteRVx&=hsdc&5${K!5MQvb{j%J&H-Xn&w`vmGkuskg7Aa$cs zV-9|72DyhlC=uPm-c_40_5k;Wt3fRRS{GEB8S_5Y^M>k*FDr%p53G*J2n_@6$xUYq zOqvp+#&bCPdIdT63`Tz%Fw)DvT?waJ?a6i6j?}#2oc`4t%{hmHn(@C0G^QYthKD_e zvywHnhcWlCt`L6;s+*ZC?1hmk3=B%!iR0j1hTT(j5AEwsF*r2PoML$hKHFph35$y- zh`(9aXJ#du6XIQli$;sE!KvbLGi05q;7mypA3HVr$Wl3F##~7guv&q8N8E@Rl;#JI zP&~lE&MW3pebC{MYM9&%M28_Fw4cQbxfCq^Q5C$|7cw${`#jogFs@-igZ!=aCtA&C&onOB2uI9 z1KAleF5Z5S_LGNxfP2}^SSN2X)xjgzo4fX>2|(l_&Q6#AlZ-1|KTpF?ltJrlO{BY3>AoR&5pOxdLC zy29;M?Wg5aQF{V2__kH)XXFv(!e0%-Q^=RN_?pwaZ&hfexlk~AV}~^w9nSqAlz+hZ zf$SRJX^*+?t$9BA&R}cRT9kOvsOI&~PLrSTP>L^Uuku=5c-?~OJrI!6e$wsfG-F0_ zPyOC9AGM%3-l+5BPxy9$@% zUD=ublfVaXPt0WPJw@72+yKyBhRqH}*fJ@bmA}v@j=LVIL5Cc#TcOIvl0Ags>H}$Y zTYI`GMRVh_?8RbA33QnukAF+>T`J) zf9XM7hjnz%#V!ZV#{yn{0D^V&e5YHWaUtr6`1i9+D??!>eNldVXyFAviYM!yc(3@+ zC?WWixc_YCM^Xc;4+T99O1*qu9QDosszi}*oe6W`9mk|z6LgVJ=4wyeob0Umftc)V zReL>u7n^i$haZGQR}IA%6=o%!d#y%6->{(f3;%$xT=65{St2IfKVGR26I_nJR_$!) zjQH%SjhC7`F6gLb7b53{dqxHAU5}|+zZdmU{d&uy>T|!0^6CAzbQoq2>8G;Pqm!$N z!kSw4L9{JXQ~s~Qd{XUI8$vGZMbG%eE*_8>`!)74fEoMf!#lK{bckeyA~k{T9*l}u z@2=G8I}{?wTHJB^gc+IiCVhz#7l~Ykt+iTA^vIKW9htm3pXuJP{L7i%Y5S9=-GQ9> z4Hq4s2vu2g9bqSvx%BwRkVyQy^Vb0yew{51cDXeV@`LZ)`Q$%A`SXvVet-Q**ud>| z!c3Qu?Nvs1sMDG)xR%=LT;G*PN7nZvB391SyU2d|HS$sh9~m&6&Y|Yi?<3Ns9F9kS z|K^WhJMsQ*&mb292W3(3X-CkiEZdIgCN2eDZxKg8Or8xi+^Nff3NqZ6`>u8`+T1t9 z673s^T9NRnGgbJMNTE;ps7B9J4%>e}mzeHMJ5WLoJN0hQ7IBvT1FbOk&%7e|Dq-V= z+dq@WoiDB3*pO+A&Y}+Q`eGX=VRYo3ZOtdICR)85k;-Lh2?BKK38i^;?YXFX$s`VR z{umH}!-HPG+-*=KUV}4n+z6$_7HrX~YNrLVsQ4QNrGMZ?I;Qm=D$Va$Zv6TmMF)W) zJKYhrEYr0(wd1MU0_IqJo1Z1l>T10P9u5y5o?@NDWh1(!S)bCKKb;k4%@eB2@o6w+ zK}bi6U9W*ua1qakBkot^+!ka7mD5dMZ3(zr>1n4)t5h@5698^thqm*{1mPX%oyfV9rp>~K|k`N%nuC|OV z1I%DX`T!>bYtlXnhm-`5bMlCelNtWsD%6V82DIIx)wkY4*Wo@I&^TRawn~LTl)VtL z;c5j0Jt0Ptg**erws-N1XYH30BF(faLDjnQRI~Zk3-+g;KY92US{CBPMrak52xX@w z4!W%_S4+@NL*QyL(VC)CbRy`o&fhWGUl8?qkM-fjdsUsW6taq>y?wET9$D5x>CFyhmVNErH@RTqI z?c5{NjFMTYc@GW*}+weA1s z!-xO3fbG9e6HaXSKRg|<|DTT+mO=mP?SQ?#i@nSLL%=rPi;wZRKA<8z(EbERT;MY{ ztUY#d=~C+(=I53PZw_v;|I}ljGD;zz51l^E?bo|~Xwh5k+~i5_@ygg~X(?JGn{e02 zelX}ng%Y79ljdA3)WMa5|B2RC$Oft)TA2WdvaqxwU z&bQ)cn}Z3TOUdp;oh39K9BJ8kQd%346@;_qL_?cQDXXuY?#H?> z8oTETuZl+-&ALu!&QfCk+~&_O!XM=sZII=j&aRFLvXFu8L|nNI{&V-uOeGgSbMq#e z(hBjP5WT+C%7ym4cqT2V(5WM^%pfbMcX)Be5i?)5N8A@Q7;;Pu-K+J6jAS+&BW7+T zY_c^7#ffEkf&pfZF+xRixt~SHR3`3uvyr+svIe`x39nT=+X>IDf^(1H&N$5O#^Ty& zsNIp)w?en%ww&cim-X9eY?Ar9r0Yi=X1Ac47K+e4h%XhZs*;@qb0UK#Or+y%=~r(y z-3qIh+bH`^+)QC=o>@ma!e1`M`V|ZMWK*hLTkHQ}Cg1Gaf%;mZJ7Y*(M5YH_8a_)( zG>DXn+EPAO=)f<|bCyl}KctW=1T$YVIM$Xr{T~Hez^=0!H_y(APuxF<@qdUB)afP? zL2XucG6SrRjuvdAS&;jMz`pv$eD(;t!Jf7lWzXXGo#Ttdb#wSn|+t zjIkkfBT(K zm8Qa2v`yS@wd)|dnQex>GCaopVG;LyAv6W5$t@A|c2_kq;vIz;%0?)tZS*zzCRzKaU6_{G-WkGKQ%7n(0r7AnHz(aNSv6rJA-KpLV^D<5sO1 zI6G%(711SgTxc+nJ|#G)cxB`>L7otduqVXR!|eOP%VPx`p>&Z}pH4hXUW8Y2jTB$I zTj+hlykw$Nv&{;PtFq)OURQ3Qtk7xuL+|Yt;rP;xa$lADQp{xHH5gkMbW^aE9zSR+ zf1;w<%6q*0zp=v!h9m4mWuqYih7Lb4I40%qr>fQe(qr)jBm94i*XU?dN7e8ax^5CR zf8)<4J>}To`TK#cmh#vw{8-u&tU^A>4pt}F$zoYzn5?+lCIDlG`+jh6Mli@uOhf~C zT|c#d^+QjC8ezwY*=B06eA4u!PQlt z-|KR*xggDRAA+rrPZeH&Oa$c;wC}#=K|Jp5GU&0L-H?et?If2DRuj{S+;Ua#2)m{Z z;k3>o!tt&vHiCYZQ^3a<)77|HzGo)#)Dd7ZoYsLN25tp_Y!#tE?6QPc@5Lz~S2@DF z5*qj+*1B{EX*A4cFbufcz?y_oB~CEYlx`fT)EFv{m|gwsD;No})^!ElbzL&WbK@lO zA%Nwo-U=ExKKc@AJs5^nD65*12@(H@l)G2n?^b84mcte{kmFe7U=WawD6(w17k({JRyjo*4H>>vIq|*&l%i~e7`&mr}3WpckCC?hs&2@CXz7Hm*L#7qxKH&&2PjNYgP_diT(G9ZEg%NS7(F`ua|)G@Qa2~)X$ zOD3h5lq_86sTDo!a-v?_AATBNw}I?Z?FX|$eq>(r4Zlf#JtAFW)QeqfiGZjQ=CbA(^@Hf~+BHu!-*+OMLMeu@F&$&+k~q! z{0Ch=u2v-gEJMg}A15bHkYXvZSqRp5jGG24ieJ&2*r_Bf?~E7Vbmdh+qV`9K^!;SB zBLEphyxmm&#|Gj8IHzwJzN7H0Zv`>RP9{~6=T^^d(u;E#XZmNWVLgfd=@%ag9H$&% zOE_LV*4~0wSBMm=+(q`V1r6j|=LOe$5<3+ita3M3>@`&R7Dy9^J?uCtn9M;u)9pcg z^GCDCP~u<5qhL=|()-pSZ|96l6N#fb%NxjVTNGix!3PZ8m?vRxdk9w_SZ+L0nPSh9 z90BsqtQb30_U|v``K+&bl=aAMjw<>a>At=w>U3ThI#3pG5iO$;Ncy1w>?a70hqx(l565E=)RNV<*G6VJ-tG^&O~0M{(DIgc1S#I4 zsf0~?t8y9$<3I^JQ0rv3M&d5HeknS&&ROL;$aZPS<^5-@0D_ZIxYs8i2WXoVF=JVo z)8>nE+f+Uv?Z@`jNz_Mmy+**5mH^l(_uRo9xcKTAYO(8{MFu0i66`Ghacn#GA6$Qv zAz$U@16wNWVlO0)HFHP!w_Kk*m3s(Y?fin^J+ORjO{E z(5ZpVzQM2Vw(9XGEkYpwBz6j#N&(BkzxldmYGW_?g5XuWJm@$1ZQmTZT(?5*E!bRx z|1!(S5zW5TN{(u>80)&!XEU^7&o4Uso0X>Vn5pRiQn9e{9IrJX3*2Vy&F zkklP*O?RdOrZ#{J@PSPCq5op)~15IU>!{O|bcUm2TU&pnH%*X^22P@Y$s`*XV)ez7-!n zd(S;`-L($+Ng&F@rhUzvJw~mQslPMlW($&rJ)BSoph}*ahyDE{cCSF{LtA*i@FmWD zM;Eq4dJN_fa=1q>2(cmfHGL=f&=J;%lXbbhDr>yUI^<*rGyKSF*D!Z>it! z3YPjC@mD_AROvpqAXU7@^%jFLzOw%ZCJAzrmyEvIItooeX3_0f0w;m_>`r2(M?YG% z!taN+a*cr~`j>2k?`ohBkTT`kDa*kLzKe3@Szd{HNxu!Ujj*^h)4+?9EaebRJ4 zPgp?j)v=k$@#jgn-T!Qs$hzn>RCl+u+h&UA&Jw6;+OW+8GB@2(JI41mM%b{cnkdPY zv^MwO;xQqWHWvs8Hh3G){Sr#t6!;moVroYlD*JO zWVyla;nl5%913DzLVi}3I&x70v{jKJ@pL!3%huJD$jd;V>4%g1Pz$tYy4{;mlZU+| zpWTm3c9VGw)?}w3us85R@IC0q?%iL8X?mK2Pr4+AvMLvZZNhu=BbRZ81yz^v$VS%a z8)8{quTo42Cf#TzvO$`RUS*>sjGu~bVNI)NeEu|;U2K!UWUYWe1>$toErKe2f=f>6DG)suTJ9F2KT7NAPV~(O`+Ul@ z2Z*mJp_pIf2?pX^x7LMMX{-GSnPeb(B~_p4v&e__^gVsVH}uX}vE~h;E4HG}omj#j zQhO6iA;i}T^&q=$3h`K=xIsG#1awcxS%JnuEA}6U!C@hGhV# z&4m1Nct-SX&u+~lp9ZFNhOk(nX;^~4qU7#eq==xM5coAS1P`PV`{&RAn4xg4DaR+S z1z5*6lDO8a{-o)1jrkktMV!`0bk10xA#=v=WH@fmNl38|j4BntAQFFm8^tbHPyO3e z+h&Mutjb=SCVPe34h5r5toWcwQOGvufO^{Oyg2rR}62BGfW&3cg{Xv^Fd4lhZn1AQHkQZr6 zV%60@-$P&K-jwH}E`Z+Qj!{NiAh(F2aYFY&_R|L98QM~bcO+qn%J;?;WVFa1bRF<* z-+v!OkJkDly#WT9V8vPaj=>S$Ap0Wz9nNH`5(^PUzUOYDfB9@bRX3pm%cMPtt@8b| zP@!8JNUxXFD0fvjSWj7vt_>jGpi^4lb#jkcemc}x7!*!;CO^_ccS0;pu8K@HF>S% z2^R1$=SVc|{;j0kFr*cFrJo8mizz1-<6@KGk?`l5?C zQ63}HCe(oU%N2NT5l)lK6s3a{R4^i8Bq>57d&W|frOB4vD4`OAu_VjLz7J*wv%lkc-skuKJ-_Gu{NDGS z567A7T<5;-bKS=|*Y{qo?~th`N{}qK{9SgiR3gWT^*OxE5nT|T9!+bM^3p^|kd=63 zhuc0#{h*}3t<{9?P|9S*%^5dvJ5#jM4sQ^OteaR{FZDKpb*1@cZBtx$fg?tcY&5

Q*wRoZ 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

ck} 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 Date: Thu, 21 Aug 2025 14:25:49 +0200 Subject: [PATCH 09/30] Fixing snapshot issue, third try --- .../generate_violin_plots/test_excel_dIEM.xlsx | Bin 0 -> 6753 bytes .../violin_pdf_P2025M1.pdf | Bin 0 -> 28932 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx create mode 100644 DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.pdf diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx b/DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..1f0e614f3b852b516ef4227f531a9ba209f3b2fc GIT binary patch literal 6753 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 NV@2c>3;Ffw{{Wb>Y9#;w literal 0 HcmV?d00001 diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.pdf b/DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e285f615d4af8007ef593d0be114b08a93dce122 GIT binary patch literal 28932 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

`OU!;?OU&?5Z~#p;cHR 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 I=0<$~3u7WaRsaA1 literal 0 HcmV?d00001 From 936f292fc5fa1bca1797f41ace7783db0108cd7a Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 21 Aug 2025 14:50:47 +0200 Subject: [PATCH 10/30] Fixed snapshot issue, fourth try --- .github/workflows/dims_test.yml | 2 +- DIMS/tests/testthat.R | 1 + .../testthat/_snaps/generate_violin_plots.md | 21 ++++++++++++++++++ .../test_excel_dIEM.xlsx | Bin 6753 -> 0 bytes .../violin_pdf_P2025M1.pdf | Bin 28932 -> 0 bytes .../testthat/test_generate_violin_plots.R | 6 +++-- 6 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 DIMS/tests/testthat/_snaps/generate_violin_plots.md delete mode 100644 DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx delete mode 100644 DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.pdf diff --git a/.github/workflows/dims_test.yml b/.github/workflows/dims_test.yml index b547d126..e8c4078c 100644 --- a/.github/workflows/dims_test.yml +++ b/.github/workflows/dims_test.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v4 - name: Install dependencies - run: Rscript -e "install.packages(c('testthat', 'withr', 'vdiffr'))" + run: Rscript -e "install.packages(c('testthat', 'withr', 'vdiffr', 'pdftools'))" - name: Run tests run: Rscript tests/testthat.R diff --git a/DIMS/tests/testthat.R b/DIMS/tests/testthat.R index 636acf7d..6e5a4962 100644 --- a/DIMS/tests/testthat.R +++ b/DIMS/tests/testthat.R @@ -2,6 +2,7 @@ library(testthat) library(withr) library(vdiffr) +library(pdftools) # enable snapshots local_edition(3) diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots.md b/DIMS/tests/testthat/_snaps/generate_violin_plots.md new file mode 100644 index 00000000..5df67e7a --- /dev/null +++ b/DIMS/tests/testthat/_snaps/generate_violin_plots.md @@ -0,0 +1,21 @@ +# Create a pdf with a table of top metabolites and violin plots + + Code + content_pdf_violinplots + Output + [1] "Top deviating metabolites for patient: P2025M1\n Metabolite Z.score\n Increased\n metab1 2.45\n Decreased\n metab11 −1.51\n" + [2] " Results for patient P2025M1\n test acyl carnitines\n metab1 Z=2.34\nMetabolites\n metab3 Z=0.31\n −5 0 5 10 15 20\n Z−scores\n" + [3] " Results for patient P2025M1\n test crea gua\n metab4 Z=−0.46\nMetabolites\n metab11 Z=0.84\n −5 0 5 10 15 20\n Z−scores\n" + [4] " Unit test Generate Violin Plots\nUnit test Generate Violin Plots\n" + +# Saving the probability score dataframe as an Excel file + + Disease P2025M1 P2025M2 P2025M3 P2025M4 + 1 Disease A 10.900 -10.9 49.90 -49.9 + 2 Disease B 0.953 0.0 2.29 0.0 + 3 Disease C 12.100 0.0 0.00 12.1 + 4 Disease D 0.000 -12.5 0.00 18.2 + 5 Disease E 44.300 0.0 0.00 28.1 + 6 Disease F 0.000 -77.4 -77.40 0.0 + 7 Disease G -38.700 38.7 38.70 -38.7 + diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx b/DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx deleted file mode 100644 index 1f0e614f3b852b516ef4227f531a9ba209f3b2fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6753 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 NV@2c>3;Ffw{{Wb>Y9#;w diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.pdf b/DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.pdf deleted file mode 100644 index e285f615d4af8007ef593d0be114b08a93dce122..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28932 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

`OU!;?OU&?5Z~#p;cHR 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 I=0<$~3u7WaRsaA1 diff --git a/DIMS/tests/testthat/test_generate_violin_plots.R b/DIMS/tests/testthat/test_generate_violin_plots.R index e80df979..55a2dac1 100644 --- a/DIMS/tests/testthat/test_generate_violin_plots.R +++ b/DIMS/tests/testthat/test_generate_violin_plots.R @@ -325,7 +325,8 @@ testthat::test_that("Create a pdf with a table of top metabolites and violin plo out_pdf_violinplots <- file.path(test_pdf_dir, "R_P2025M1.pdf") expect_true(file.exists(out_pdf_violinplots)) - expect_snapshot_file(out_pdf_violinplots, "violin_pdf_P2025M1.pdf") + content_pdf_violinplots <- pdftools::pdf_text(out_pdf_violinplots) + expect_snapshot(content_pdf_violinplots) unlink(test_pdf_dir, recursive = TRUE) }) @@ -393,7 +394,8 @@ testthat::test_that("Saving the probability score dataframe as an Excel file", { expect_silent(save_prob_scores_to_excel(test_probability_score_df, test_output_dir, test_run_name)) expect_true(file.exists(out_excel_file)) - expect_snapshot_file(out_excel_file, "test_excel_dIEM.xlsx") + content_excel_file <- openxlsx::read.xlsx(out_excel_file, sheet = 1) + expect_snapshot_output(content_excel_file) unlink(test_output_dir, recursive = TRUE) }) From 926ad6d0c0985b677501eed5da072229e808444d Mon Sep 17 00:00:00 2001 From: ALuesink Date: Mon, 8 Sep 2025 13:14:48 +0200 Subject: [PATCH 11/30] Fixed issue if P1001 is present but no Z-scores --- DIMS/GenerateQCOutput.R | 1 + 1 file changed, 1 insertion(+) diff --git a/DIMS/GenerateQCOutput.R b/DIMS/GenerateQCOutput.R index 321c6ec0..5a439d5c 100644 --- a/DIMS/GenerateQCOutput.R +++ b/DIMS/GenerateQCOutput.R @@ -328,6 +328,7 @@ if (length(sst_colnrs) > 0) { } else { sst_list_intensities <- sst_list[, intensity_col_ids] } +sst_list_intensities <- as.data.frame(sst_list_intensities) for (col_nr in seq_len(ncol(sst_list_intensities))) { sst_list_intensities[, col_nr] <- as.numeric(sst_list_intensities[, col_nr]) if (grepl("Zscore", colnames(sst_list_intensities)[col_nr])) { From b1a38581172cc113cd2986e38ddd23cfa86157fa Mon Sep 17 00:00:00 2001 From: ALuesink Date: Mon, 8 Sep 2025 16:32:47 +0200 Subject: [PATCH 12/30] print statement for testing --- DIMS/GenerateViolinPlots.R | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DIMS/GenerateViolinPlots.R b/DIMS/GenerateViolinPlots.R index 847c4cb5..801f61bb 100644 --- a/DIMS/GenerateViolinPlots.R +++ b/DIMS/GenerateViolinPlots.R @@ -101,6 +101,8 @@ zscore_controls_df <- intensities_zscore_ratios_df %>% select(HMDB_code, HMDB_na colnames(zscore_patients_df) <- gsub("_Zscore", "", colnames(zscore_patients_df)) colnames(zscore_controls_df) <- gsub("_Zscore", "", colnames(zscore_controls_df)) +cat(colnames(expected_biomarkers_df)) + expected_biomarkers_df <- expected_biomarkers_df %>% rename(HMDB_code = HMDB.code, HMDB_name = Metabolite) expected_biomarkers_info <- expected_biomarkers_df %>% From ba47db588619c69b0852a736163c6c832dc91169 Mon Sep 17 00:00:00 2001 From: Anne Luesink Date: Thu, 11 Sep 2025 09:29:05 +0200 Subject: [PATCH 13/30] Removed duplicated line & print statement --- DIMS/GenerateViolinPlots.R | 4 ---- 1 file changed, 4 deletions(-) diff --git a/DIMS/GenerateViolinPlots.R b/DIMS/GenerateViolinPlots.R index 801f61bb..224b1f70 100644 --- a/DIMS/GenerateViolinPlots.R +++ b/DIMS/GenerateViolinPlots.R @@ -101,8 +101,6 @@ zscore_controls_df <- intensities_zscore_ratios_df %>% select(HMDB_code, HMDB_na colnames(zscore_patients_df) <- gsub("_Zscore", "", colnames(zscore_patients_df)) colnames(zscore_controls_df) <- gsub("_Zscore", "", colnames(zscore_controls_df)) -cat(colnames(expected_biomarkers_df)) - expected_biomarkers_df <- expected_biomarkers_df %>% rename(HMDB_code = HMDB.code, HMDB_name = Metabolite) expected_biomarkers_info <- expected_biomarkers_df %>% @@ -155,8 +153,6 @@ for (metabolite_dir in metabolite_dirs) { } #### Run the IEM algorithm ######### -expected_biomarkers_df <- expected_biomarkers_df %>% rename(HMDB_code = HMDB.code, HMDB_name = Metabolite) - diem_probability_score <- run_diem_algorithm(expected_biomarkers_df, zscore_patients_df, patients) save_prob_scores_to_excel(diem_probability_score, output_dir, run_name) From 3875aaf30adfb48db96be45f2de4bd02c4266c99 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Tue, 28 Oct 2025 14:12:34 +0100 Subject: [PATCH 14/30] Fix for error dIEM plots --- DIMS/GenerateViolinPlots.R | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/DIMS/GenerateViolinPlots.R b/DIMS/GenerateViolinPlots.R index 224b1f70..38c0a105 100644 --- a/DIMS/GenerateViolinPlots.R +++ b/DIMS/GenerateViolinPlots.R @@ -176,14 +176,21 @@ for (patient_id in patients) { if (nrow(patient_top_iems_probs) > 0) { top_iems <- patient_top_iems_probs %>% pull(Disease) # Get the metabolites for each IEM and their probability + metabs_iems <- list() metabs_iems_names <- c() - metabs_iems <- lapply(top_iems, function(iem) { + for (iem in top_iems) { iem_probablity <- patient_top_iems_probs %>% filter(Disease == iem) %>% pull(!!sym(patient_id)) metabs_iems_names <- c(metabs_iems_names, paste0(iem, ", probability score ", iem_probablity)) - metab_iem <- expected_biomarkers_df %>% filter(Disease == iem) %>% select(HMDB_code, HMDB_name) - return(metab_iem) - }) - names(metabs_iems) <- metabs_iems_names + metab_iem_df <- expected_biomarkers_df %>% filter(Disease == iem) %>% select(HMDB_code, HMDB_name) + metabs_iems[[iem]] <- metab_iem_df + } + # metabs_iems <- lapply(top_iems, function(iem) { + # iem_probablity <- patient_top_iems_probs %>% filter(Disease == iem) %>% pull(!!sym(patient_id)) + # metabs_iems_names <- c(metabs_iems_names, paste0(iem, ", probability score ", iem_probablity)) + # metab_iem <- expected_biomarkers_df %>% filter(Disease == iem) %>% select(HMDB_code, HMDB_name) + # return(metab_iem) + # }) + # names(metabs_iems) <- metabs_iems_names # Get the Z-scores with metabolite information metab_iem_sorted <- combine_metab_info_zscores(metabs_iems, zscore_patients_df) From c7edbb54cf4200e1e74aab94fa2f007df3b9d92e Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 4 Dec 2025 11:10:14 +0100 Subject: [PATCH 15/30] Removed unnecessary params --- DIMS/GenerateQCOutput.nf | 1 - 1 file changed, 1 deletion(-) diff --git a/DIMS/GenerateQCOutput.nf b/DIMS/GenerateQCOutput.nf index eb17301f..73f772e9 100644 --- a/DIMS/GenerateQCOutput.nf +++ b/DIMS/GenerateQCOutput.nf @@ -23,7 +23,6 @@ process GenerateQCOutput { Rscript ${baseDir}/CustomModules/DIMS/GenerateQCOutput.R $init_file \ $analysis_id \ $params.matrix \ - $params.zscore \ $params.sst_components_file \ $params.export_scripts_dir """ From f5210be1ec996595c83184477ea4d80eea1c7ad2 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 4 Dec 2025 11:10:50 +0100 Subject: [PATCH 16/30] Removed unnecessary params, added comments and changed variable names --- DIMS/GenerateQCOutput.R | 57 ++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/DIMS/GenerateQCOutput.R b/DIMS/GenerateQCOutput.R index 1bc3e65c..605c8db0 100644 --- a/DIMS/GenerateQCOutput.R +++ b/DIMS/GenerateQCOutput.R @@ -12,7 +12,6 @@ cmd_args <- commandArgs(trailingOnly = TRUE) init_file <- cmd_args[1] project <- cmd_args[2] dims_matrix <- cmd_args[3] -z_score <- cmd_args[4] sst_components_file <- cmd_args[5] export_scripts_dir <- cmd_args[6] @@ -38,11 +37,9 @@ dir.create(paste0(outdir, "/plots"), showWarnings = FALSE) control_label <- "C" #### CHECK NUMBER OF CONTROLS #### -if (z_score == 1) { - file_name <- "Check_number_of_controls.txt" - min_num_controls <- 25 - check_number_of_controls(outlist, min_num_controls, file_name) -} +file_name <- "Check_number_of_controls.txt" +min_num_controls <- 25 +check_number_of_controls(outlist, min_num_controls, file_name) #### INTERNAL STANDARDS #### is_list <- outlist[grep("Internal standard", outlist[, "relevance"], fixed = TRUE), ] @@ -314,30 +311,38 @@ is_list_intensities <- get_is_intensities(is_list, int_cols = intensity_col_ids) is_neg_intensities <- get_is_intensities(outlist_tot_neg, is_codes = is_codes) is_pos_intensities <- get_is_intensities(outlist_tot_pos, is_codes = is_codes) -# SST components. -sst_comp <- read.csv(sst_components_file, header = TRUE, sep = "\t") -sst_list <- outlist %>% filter(HMDB_code %in% sst_comp$HMDB_ID) -sst_colnrs <- grep("P1001", colnames(sst_list)) - -if (length(sst_colnrs) > 0) { - sst_list_intensities <- sst_list[, sst_colnrs] - control_col_ids <- grep(control_label, colnames(sst_list), fixed = TRUE) - control_list_intensities <- sst_list[, control_col_ids] - control_list_cv <- calc_coefficient_of_variation(control_list_intensities) - sst_list_intensities <- cbind(sst_list_intensities, CV_controls = control_list_cv[, "CV_perc"]) +# SST components +sst_components <- read.csv(sst_components_file, header = TRUE, sep = "\t") +sst_metabolites_df <- outlist %>% filter(HMDB_code %in% sst_components$HMDB_ID) +sst_sample_column_index <- grep("P1001", colnames(sst_metabolites_df)) + +# Check if SST mix sample(s) are present +if (length(sst_sample_column_index) > 0) { + # Get the SST intensities of the controls, calculate the coefficient of variation + # and add to SST mix intensities + sst_sample_intensities_df <- sst_metabolites_df[, sst_sample_column_index] + control_col_ids <- grep(control_label, colnames(sst_metabolites_df), fixed = TRUE) + control_sst_intensities_df <- sst_metabolites_df[, control_col_ids] + control_sst_metabolites_cv <- calc_coefficient_of_variation(control_sst_intensities_df) + sst_intensities_df <- cbind(sst_sample_intensities_df, CV_controls = control_sst_metabolites_cv[, "CV_perc"]) } else { - sst_list_intensities <- sst_list[, intensity_col_ids] + # Use intensities when there is not SST mix sample added + sst_intensities_df <- sst_metabolites_df[, intensity_col_ids] } -sst_list_intensities <- as.data.frame(sst_list_intensities) -for (col_nr in seq_len(ncol(sst_list_intensities))) { - sst_list_intensities[, col_nr] <- as.numeric(sst_list_intensities[, col_nr]) - if (grepl("Zscore", colnames(sst_list_intensities)[col_nr])) { - sst_list_intensities[, col_nr] <- round(sst_list_intensities[, col_nr], 2) + +sst_intensities_df <- as.data.frame(sst_intensities_df) +for (col_nr in seq_len(ncol(sst_intensities_df))) { + # Change column type to numeric + sst_intensities_df[, col_nr] <- as.numeric(sst_intensities_df[, col_nr]) + if (grepl("Zscore", colnames(sst_intensities_df)[col_nr])) { + # Round numeric value of Z-score columns to 2 decimal places + sst_intensities_df[, col_nr] <- round(sst_intensities_df[, col_nr], 2) } else { - sst_list_intensities[, col_nr] <- round(sst_list_intensities[, col_nr]) + # Round numeric value of intensity columns to an intiger + sst_intensities_df[, col_nr] <- round(sst_intensities_df[, col_nr]) } } -sst_list_intensities <- cbind(SST_comp_name = sst_list$HMDB_name, sst_list_intensities) +sst_intensities_df <- cbind(SST_comp_name = sst_metabolites_df$HMDB_name, sst_intensities_df) # Create Excel file wb <- createWorkbook("IS_SST") @@ -351,7 +356,7 @@ addWorksheet(wb, "IS neg") openxlsx::writeData(wb, sheet = 3, is_neg_intensities) setColWidths(wb, 3, cols = 1, widths = 24) addWorksheet(wb, "SST components") -openxlsx::writeData(wb, sheet = 4, sst_list_intensities) +openxlsx::writeData(wb, sheet = 4, sst_intensities_df) setColWidths(wb, 4, cols = 1:3, widths = 24) xlsx_name <- paste0(outdir, "/", project, "_IS_SST.xlsx") openxlsx::saveWorkbook(wb, xlsx_name, overwrite = TRUE) From 204351e0577af543639a87a0c6b7e9fc639532fa Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 4 Dec 2025 11:20:26 +0100 Subject: [PATCH 17/30] Removed old, unused code --- DIMS/GenerateViolinPlots.R | 8 -------- 1 file changed, 8 deletions(-) diff --git a/DIMS/GenerateViolinPlots.R b/DIMS/GenerateViolinPlots.R index 38c0a105..2f32e3bc 100644 --- a/DIMS/GenerateViolinPlots.R +++ b/DIMS/GenerateViolinPlots.R @@ -184,14 +184,6 @@ for (patient_id in patients) { metab_iem_df <- expected_biomarkers_df %>% filter(Disease == iem) %>% select(HMDB_code, HMDB_name) metabs_iems[[iem]] <- metab_iem_df } - # metabs_iems <- lapply(top_iems, function(iem) { - # iem_probablity <- patient_top_iems_probs %>% filter(Disease == iem) %>% pull(!!sym(patient_id)) - # metabs_iems_names <- c(metabs_iems_names, paste0(iem, ", probability score ", iem_probablity)) - # metab_iem <- expected_biomarkers_df %>% filter(Disease == iem) %>% select(HMDB_code, HMDB_name) - # return(metab_iem) - # }) - # names(metabs_iems) <- metabs_iems_names - # Get the Z-scores with metabolite information metab_iem_sorted <- combine_metab_info_zscores(metabs_iems, zscore_patients_df) metab_iem_controls <- combine_metab_info_zscores(metabs_iems, zscore_controls_df) From 01d76f92b1643f11bb2f9280be521fcc728d8404 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 26 Feb 2026 09:50:23 +0100 Subject: [PATCH 18/30] Refactor functions --- DIMS/GenerateExcel.R | 4 +- DIMS/GenerateViolinPlots.R | 225 ++---- DIMS/export/generate_violin_plots_functions.R | 717 ++++++++++++++---- 3 files changed, 615 insertions(+), 331 deletions(-) diff --git a/DIMS/GenerateExcel.R b/DIMS/GenerateExcel.R index 53662649..efb83600 100644 --- a/DIMS/GenerateExcel.R +++ b/DIMS/GenerateExcel.R @@ -45,7 +45,7 @@ peaks_in_list <- which(rownames(outlist) %in% rlvnc$HMDB_key) outlist_subset <- outlist[peaks_in_list, ] outlist_subset$HMDB_key <- rownames(outlist_subset) outlist <- outlist_subset %>% - left_join(rlvnc %>% rename(sec_HMBD_ID_rlvnc = sec_HMDB_ID), by = "HMDB_key") + left_join(rlvnc %>% rename(sec_HMDB_ID_rlvnc = sec_HMDB_ID), by = "HMDB_key") rownames(outlist) <- outlist$HMDB_key # filter out all irrelevant HMDBs @@ -144,7 +144,7 @@ if (z_score == 1) { filter(HMDB_key %in% metab_list_helix) %>% left_join(., metab_df_helix, by = join_by(HMDB_code == HMDB_code)) %>% select( - -c(HMDB_key, sec_HMBD_ID_rlvnc, name, relevance, descr, origin, fluids, tissue, disease, pathway), + -c(HMDB_key, sec_HMDB_ID_rlvnc, name, relevance, descr, origin, fluids, tissue, disease, pathway), -all_of(control_col_idx), -all_of(patient_col_idx) ) %>% relocate(c(HMDB_code, H_Name, avg_ctrls, sd_ctrls), .after = plots) %>% diff --git a/DIMS/GenerateViolinPlots.R b/DIMS/GenerateViolinPlots.R index 2f32e3bc..2b742be0 100644 --- a/DIMS/GenerateViolinPlots.R +++ b/DIMS/GenerateViolinPlots.R @@ -6,6 +6,8 @@ library(ggplot2) suppressPackageStartupMessages(library("gridExtra")) library(stringr) +options(digits = 16) + # define parameters cmd_args <- commandArgs(trailingOnly = TRUE) @@ -20,190 +22,77 @@ file_explanation <- cmd_args[6] source(paste0(export_scripts_dir, "generate_violin_plots_functions.R")) # load dataframe with intensities and Z-scores for all samples intensities_zscore_df <- get(load("outlist.RData")) +rm(outlist) # read input files -ratios_metabs_df <- read.csv(file_ratios_metabolites, sep = ";", stringsAsFactors = FALSE) +metabolites_ratios_df <- read.csv(file_ratios_metabolites, sep = ";", stringsAsFactors = FALSE) expected_biomarkers_df <- read.csv(file_expected_biomarkers_iem, sep = ";", stringsAsFactors = FALSE) +expected_biomarkers_df <- expected_biomarkers_df %>% + rename(HMDB_code = HMDB.code, + HMDB_name = Metabolite) explanation_violin_plot <- readLines(file_explanation) - -## Set global variables -output_dir <- "./" # path: output folder for dIEM and violin plots -top_number_iem_diseases <- 5 # number of diseases that score highest in algorithm to plot -threshold_iem <- 5 # probability score cut-off for plotting the top diseases -ratios_cutoff <- -5 # z-score cutoff of axis on the left for top diseases -nr_plots_perpage <- 20 # number of violin plots per page in PDF +# Set global variables +top_number_iem_diseases <- 5 # number of diseases that score highest in algorithm to plot +threshold_iem <- 5 # probability score cut-off for plotting the top diseases +nr_plots_perpage <- 20 # number of violin plots per page in PDF zscore_cutoff <- 5 -xaxis_cutoff <- 20 protocol_name <- "DIMS_PL_DIAG" - -# Remove columns, move HMDB_code & HMDB_name column to the front, change intensity columns to numeric -intensities_zscore_df <- intensities_zscore_df %>% - select(-c(plots, HMDB_name_all, HMDB_ID_all, sec_HMDB_ID, HMDB_key, sec_HMBD_ID_rlvnc, name, - relevance, descr, origin, fluids, tissue, disease, pathway, nr_ctrls)) %>% - relocate(c(HMDB_code, HMDB_name)) %>% - rename(mean_controls = avg_ctrls, sd_controls = sd_ctrls) %>% - mutate(across(!c(HMDB_name, HMDB_code), as.numeric)) - -# Get the controls and patient IDs, select the intensity columns -controls <- colnames(intensities_zscore_df)[grepl("^C", colnames(intensities_zscore_df)) & - !grepl("_Zscore$", colnames(intensities_zscore_df))] -control_intensities_cols_index <- which(colnames(intensities_zscore_df) %in% controls) -nr_of_controls <- length(controls) - -patients <- colnames(intensities_zscore_df)[grepl("^P", colnames(intensities_zscore_df)) & - !grepl("_Zscore$", colnames(intensities_zscore_df))] -patient_intensities_cols_index <- which(colnames(intensities_zscore_df) %in% patients) -nr_of_patients <- length(patients) - -intensity_cols_index <- c(control_intensities_cols_index, patient_intensities_cols_index) -intensity_cols <- colnames(intensities_zscore_df)[intensity_cols_index] - -#### Calculate ratios of intensities for metabolites #### -# Prepare empty data frame to fill with ratios -ratio_zscore_df <- data.frame(matrix( - ncol = ncol(intensities_zscore_df), - nrow = nrow(ratios_metabs_df) -)) -colnames(ratio_zscore_df) <- colnames(intensities_zscore_df) - -# put HMDB info into first two columns of ratio_zscore_df -ratio_zscore_df$HMDB_code <- ratios_metabs_df$HMDB.code -ratio_zscore_df$HMDB_name <- ratios_metabs_df$Ratio_name - -for (row_index in seq_len(nrow(ratios_metabs_df))) { - numerator_intensities <- get_intentities_for_ratios(ratios_metabs_df, row_index, - intensities_zscore_df, "HMDB_numerator", intensity_cols) - denominator_intensities <- get_intentities_for_ratios(ratios_metabs_df, row_index, - intensities_zscore_df, "HMDB_denominator", intensity_cols) - # calculate intensity ratios - ratio_zscore_df[row_index, intensity_cols_index] <- log2(numerator_intensities / denominator_intensities) -} -# Calculate means and SD's of the calculated ratios for Controls -ratio_zscore_df[, "mean_controls"] <- apply(ratio_zscore_df[, control_intensities_cols_index], 1, mean) -ratio_zscore_df[, "sd_controls"] <- apply(ratio_zscore_df[, control_intensities_cols_index], 1, sd) - -# Calculate Zscores for the ratios -samples_zscore_columns <- get_zscore_columns(colnames(intensities_zscore_df), intensity_cols) -ratio_zscore_df[, samples_zscore_columns] <- (ratio_zscore_df[, intensity_cols] - ratio_zscore_df[, "mean_controls"]) / - ratio_zscore_df[, "sd_controls"] - -intensities_zscore_ratios_df <- rbind(intensities_zscore_df, ratio_zscore_df) - +number_of_metabolites <- list( + highest = 20, + lowest = 10 +) + +control_ids <- get_colnames_samples(intensities_zscore_df, "C") +patient_ids <- get_colnames_samples(intensities_zscore_df, "P") +all_sample_ids <- c(control_ids, patient_ids) +number_of_samples <- list( + controls = length(control_ids), + patients = length(patient_ids) +) + +# Add Z-scores for ratios to intensities_zscore_df dataframe +intensities_zscore_ratios_df <- add_zscores_ratios_to_df(intensities_zscore_df, metabolites_ratios_df, all_sample_ids) # for debugging: -save(intensities_zscore_ratios_df, file = paste0(output_dir, "/outlist_with_ratios.RData")) +save(intensities_zscore_ratios_df, file = "./outlist_with_ratios.RData") # Select only the cols with zscores of the patients -zscore_patients_df <- intensities_zscore_ratios_df %>% select(HMDB_code, HMDB_name, any_of(paste0(patients, "_Zscore"))) -zscore_controls_df <- intensities_zscore_ratios_df %>% select(HMDB_code, HMDB_name, any_of(paste0(controls, "_Zscore"))) +zscore_patients_df <- intensities_zscore_ratios_df %>% + select(HMDB_code, HMDB_name, any_of(paste0(patient_ids, "_Zscore"))) %>% + rename_with(~ str_remove(.x, "_Zscore"), .cols = contains("_Zscore")) +zscore_controls_df <- intensities_zscore_ratios_df %>% + select(HMDB_code, HMDB_name, any_of(paste0(control_ids, "_Zscore"))) %>% + rename_with(~ str_remove(.x, "_Zscore"), .cols = contains("_Zscore")) #### Make violin plots ##### -# preparation -colnames(zscore_patients_df) <- gsub("_Zscore", "", colnames(zscore_patients_df)) -colnames(zscore_controls_df) <- gsub("_Zscore", "", colnames(zscore_controls_df)) - -expected_biomarkers_df <- expected_biomarkers_df %>% rename(HMDB_code = HMDB.code, HMDB_name = Metabolite) - -expected_biomarkers_info <- expected_biomarkers_df %>% - select(c(Disease, HMDB_code, HMDB_name)) %>% - distinct(Disease, HMDB_code, .keep_all = TRUE) - -metabolite_dirs <- list.files(path = path_metabolite_groups, full.names = FALSE, recursive = FALSE) -for (metabolite_dir in metabolite_dirs) { - # create a directory for the output PDFs - pdf_dir <- paste(output_dir, metabolite_dir, sep = "/") - dir.create(pdf_dir, showWarnings = FALSE) - - metab_list_all <- get_list_metabolites(paste(path_metabolite_groups, metabolite_dir, sep = "/")) - - # prepare list of metabolites; max nr_plots_perpage on one page - metab_interest_sorted <- combine_metab_info_zscores(metab_list_all, zscore_patients_df) - metab_interest_controls <- combine_metab_info_zscores(metab_list_all, zscore_controls_df) - metab_perpage <- prepare_data_perpage(metab_interest_sorted, metab_interest_controls, - nr_plots_perpage, nr_of_patients, nr_of_controls) - - # for Diagnostics metabolites to be saved in Helix - if (grepl("Diagnost", pdf_dir)) { - # get table that combines DIMS results with stofgroepen/Helix table - dims_helix_table <- get_patient_data_to_helix(metab_interest_sorted, metab_list_all) - - # check if run contains Diagnostics patients (e.g. "P2024M"), not for research runs - if (any(is_diagnostic_patient(dims_helix_table$Sample))) { - # get output file for Helix - output_helix <- output_for_helix(protocol_name, dims_helix_table) - # write output to file - path_helixfile <- paste0(output_dir, "output_Helix_", run_name, ".csv") - write.csv(output_helix, path_helixfile, quote = FALSE, row.names = FALSE) - } - } - - # make violin plots per patient - for (patient_id in patients) { - # for category Diagnostics, make list of metabolites that exceed alarm values for this patient - # for category Other, make list of top highest and lowest Z-scores for this patient - if (grepl("Diagnost", pdf_dir)) { - top_metabs_patient <- prepare_alarmvalues(patient_id, dims_helix_table) - } else { - top_metabs_patient <- prepare_toplist(patient_id, zscore_patients_df) - } - - # generate normal violin plots - create_pdf_violin_plots(pdf_dir, patient_id, metab_perpage, top_metabs_patient, explanation_violin_plot) - } - -} +make_and_save_violin_plot_pdfs( + zscore_patients_df, + zscore_controls_df, + path_metabolite_groups, + nr_plots_perpage, + number_of_samples, + run_name, + protocol_name, + explanation_violin_plot, + number_of_metabolites +) #### Run the IEM algorithm ######### -diem_probability_score <- run_diem_algorithm(expected_biomarkers_df, zscore_patients_df, patients) - -save_prob_scores_to_excel(diem_probability_score, output_dir, run_name) +diem_probability_score <- run_diem_algorithm(expected_biomarkers_df, zscore_patients_df, patient_col_names) +save_prob_scores_to_excel(diem_probability_score, run_name) #### Generate dIEM plots ######### -diem_plot_dir <- paste(output_dir, "dIEM_plots", sep = "/") -dir.create(diem_plot_dir) - -colnames(diem_probability_score) <- gsub("_Zscore", "", colnames(diem_probability_score)) -patient_no_iem <- c() - -for (patient_id in patients) { - # Select the top IEMs and filter on the IEM threshold - patient_top_iems_probs <- diem_probability_score %>% - select(c(Disease, !!sym(patient_id))) %>% - arrange(desc(!!sym(patient_id))) %>% - slice(1:top_number_iem_diseases) %>% - filter(!!sym(patient_id) >= threshold_iem) - - if (nrow(patient_top_iems_probs) > 0) { - top_iems <- patient_top_iems_probs %>% pull(Disease) - # Get the metabolites for each IEM and their probability - metabs_iems <- list() - metabs_iems_names <- c() - for (iem in top_iems) { - iem_probablity <- patient_top_iems_probs %>% filter(Disease == iem) %>% pull(!!sym(patient_id)) - metabs_iems_names <- c(metabs_iems_names, paste0(iem, ", probability score ", iem_probablity)) - metab_iem_df <- expected_biomarkers_df %>% filter(Disease == iem) %>% select(HMDB_code, HMDB_name) - metabs_iems[[iem]] <- metab_iem_df - } - # Get the Z-scores with metabolite information - metab_iem_sorted <- combine_metab_info_zscores(metabs_iems, zscore_patients_df) - metab_iem_controls <- combine_metab_info_zscores(metabs_iems, zscore_controls_df) - # Get a list of dataframes for each IEM - diem_metab_perpage <- prepare_data_perpage(metab_iem_sorted, metab_iem_controls, - nr_plots_perpage, nr_of_patients, nr_of_controls) - # Get a dataframe of the top metabolites - top_metabs_patient <- prepare_toplist(patient_id, zscore_patients_df) - - # Generate and save dIEM violin plots - create_pdf_violin_plots(diem_plot_dir, patient_id, diem_metab_perpage, top_metabs_patient, explanation_violin_plot) - - } else { - patient_no_iem <- c(patient_no_iem, patient_id) - } -} +patient_no_iem <- make_and_save_diem_plots( + diem_probability_score, + patient_ids, + expected_biomarkers_df, + zscore_patients_df, + zscore_controls_df, + nr_plots_perpage, + number_of_samples, + number_of_metabolites +) if (length(patient_no_iem) > 0) { - patient_no_iem <- c(paste0("The following patient(s) did not have dIEM probability scores higher than ", - threshold_iem, " :"), - patient_no_iem) - write(file = paste0(output_dir, "missing_probability_scores.txt"), patient_no_iem) + save_patient_no_iem(threshold_iem, patient_no_iem) } diff --git a/DIMS/export/generate_violin_plots_functions.R b/DIMS/export/generate_violin_plots_functions.R index 0ae7edff..5ae656b3 100644 --- a/DIMS/export/generate_violin_plots_functions.R +++ b/DIMS/export/generate_violin_plots_functions.R @@ -1,187 +1,441 @@ -#' Getting the intensities for calculating ratio Z-scores +#' Preparing the intensities and Z-score dataframe. +#' Certain columns are removed, the HMDB_code and HMDB_name column are moved forward, +#' the avg_ctrls and sd_ctrls columns are renamed and the column type of all columns containing numbers +#' is changed to numeric. #' -#' @param ratios_metabs_df: dataframe with HMDB codes for the ratios (dataframe) -#' @param row_index: index of the row in the ratios_metabs_df (integer) -#' @param intensities_zscore_df: dataframe with intensities for each sample (dataframe) -#' @param fraction_side: either numerator or denominator, which side of the fraction (string) -#' @param intensity_cols: names of the columns that contain the intensities (string) +#' @param intensities_zscore_df: dataframe with intensities, Z-scores and metabolite information for all samples #' -#' @returns fraction_side_intensity: a vector of intensities (vector of integers) -get_intentities_for_ratios <- function(ratios_metabs_df, row_index, intensities_zscore_df, fraction_side, intensity_cols) { - fraction_side_hmdb_ids <- ratios_metabs_df[row_index, fraction_side] - if (grepl("plus", fraction_side_hmdb_ids)) { - fraction_side_hmdb_id_list <- strsplit(fraction_side_hmdb_ids, "plus")[[1]] - fraction_side_intensity_list <- intensities_zscore_df %>% - filter(HMDB_code %in% fraction_side_hmdb_id_list) %>% - select(any_of(intensity_cols)) - fraction_side_intensity <- apply(fraction_side_intensity_list, 2, sum) - } else if (fraction_side_hmdb_ids == "one") { - fraction_side_intensity <- 1 - } else { - fraction_side_intensity <- intensities_zscore_df %>% - filter(HMDB_code == fraction_side_hmdb_ids) %>% - select(any_of(intensity_cols)) - } - return(fraction_side_intensity) +#' @returns intensities_zscore_df: a dataframe containing intensities, Z-scores, HMDB IDs, HMDB names and +#' the mean and average of all controls +prepare_intensities_zscore_df <- function(intensities_zscore_df) { + intensities_zscore_df <- intensities_zscore_df %>% + select(-c( + plots, HMDB_name_all, HMDB_ID_all, sec_HMDB_ID, HMDB_key, sec_HMDB_ID_rlvnc, name, + relevance, descr, origin, fluids, tissue, disease, pathway, nr_ctrls + )) %>% + relocate(c(HMDB_code, HMDB_name)) %>% + rename(mean_controls = avg_ctrls, sd_controls = sd_ctrls) %>% + mutate(across(!c(HMDB_name, HMDB_code), as.numeric)) + return(intensities_zscore_df) } -#' Get the sample IDs for columns that have Z-score and intensities +#' Get all column names containing a specific prefix. +#' Find all column names containing a specific prefix, e.g. "P", and remove the _Zscore suffix from the names +#' +#' @param dataframe: dataframe containing multiple columns with Z-scores +#' @param sample_label: a string of a prefix to be searched in the column names, e.g. "P" or "C". +#' +#' @returns sample_colnames: a vector of column names all containing the prefix. +get_colnames_samples <- function(dataframe, sample_label) { + sample_colnames <- unique(gsub("_Zscore", "", grepv(paste0("^", sample_label), colnames(dataframe)))) + return(sample_colnames) +} + +#' Add Zscores for multiple ratios to the dataframe +#' +#' @param outlist: dataframe containing intensities and Z-scores for all controls and patients +#' @param metabolites_ratios_df: dataframe containing numerators and denominators for all ratios +#' @param all_sample_ids: vector of sample IDS, controls and patients +#' +#' @returns intensities_zscore_ratios_df: dataframe containing intensities and Z-scores for all controls and patients +#' for all metabolites and ratios +add_zscores_ratios_to_df <- function(outlist, metabolites_ratios_df, all_sample_ids) { + intensities_zscores_df <- prepare_intensities_zscore_df(outlist) + + # calculate Z-scores for the ratios + zscore_ratios_df <- calculate_zscore_ratios(metabolites_ratios_df, intensities_zscores_df, all_sample_ids) + intensities_zscore_ratios_df <- rbind(intensities_zscores_df, zscore_ratios_df) + + return(intensities_zscore_ratios_df) +} + +#' Calculate Z-scores for ratios #' -#' @param colnames_zscore: vector of sample IDs from the dataframe containing Z-scores (vector of strings) -#' @param intensity_cols: vector of sample IDs form the dataframe containing intensities (vector of strings) +#' @param metabolites_ratios_df: dataframe containing numerators and denominators for all ratios +#' @param intensities_zscores_df: dataframe containing intensities and Z-scores for all controls and patients +#' @param intensity_col_names: vector of sample IDS, controls and patients #' -#' @returns: vector of sample IDs that are in both input vectors (vector of strings) -get_zscore_columns <- function(colnames_zscore, intensity_cols) { - sample_intersect <- intersect(paste0(intensity_cols, "_Zscore"), grep("_Zscore", colnames_zscore, value = TRUE)) - return(sample_intersect) +#' @returns zscore_ratios_df: dataframe containing Z-scores for all ratios for all samples +calculate_zscore_ratios <- function(metabolites_ratios_df, intensities_zscores_df, intensity_col_names) { + zscore_ratios_df <- data.frame(matrix( + ncol = ncol(intensities_zscores_df), + nrow = nrow(metabolites_ratios_df) + )) + colnames(zscore_ratios_df) <- colnames(intensities_zscores_df) + + # put HMDB info into first two columns of ratio_zscore_df + zscore_ratios_df$HMDB_code <- metabolites_ratios_df$HMDB.code + zscore_ratios_df$HMDB_name <- metabolites_ratios_df$Ratio_name + + intensity_cols_index <- which(colnames(zscore_ratios_df) %in% intensity_col_names) + for (row_index in seq_len(nrow(metabolites_ratios_df))) { + # Get a list of intensities for the numerator + numerator_intensities <- get_intensities_fraction_side( + metabolites_ratios_df, + row_index, + intensities_zscores_df, + "HMDB_numerator", + intensity_col_names + ) + # Get a list of intensities for the denominator + denominator_intensities <- get_intensities_fraction_side( + metabolites_ratios_df, + row_index, + intensities_zscores_df, + "HMDB_denominator", + intensity_col_names + ) + # calculate the intensity ratio for each sample + zscore_ratios_df[row_index, intensity_cols_index] <- log2(numerator_intensities / denominator_intensities) + } + + control_intensities_cols_index <- grep("^C[^_]*$", colnames(intensities_zscores_df), perl = TRUE) + # Calculate means and SD's of the calculated ratios for Controls + zscore_ratios_df[, "mean_controls"] <- apply(zscore_ratios_df[, control_intensities_cols_index], 1, mean) + zscore_ratios_df[, "sd_controls"] <- apply(zscore_ratios_df[, control_intensities_cols_index], 1, sd) + + # Calculate Zscores for the ratios + samples_zscore_columns <- get_sample_ids_with_zscores(colnames(intensities_zscores_df), intensity_col_names) + intensity_ratios_df <- zscore_ratios_df[, intensity_col_names] + mean_ratios_controls <- zscore_ratios_df[, "mean_controls"] + sd_ratios_controls <- zscore_ratios_df[, "sd_controls"] + + zscore_ratios_df[, samples_zscore_columns] <- (intensity_ratios_df - mean_ratios_controls) / sd_ratios_controls + + return(zscore_ratios_df) +} + +#' Make and save violin plots for each patient in a PDF +#' +#' @param zscore_patients_df: dataframe with Z-scores for all patient samples +#' @param zscore_controls_df: dataframe with Z-scores for all control samples +#' @param path_metabolite_groups: string containing the path for the metabolite groups directories +#' @param nr_plots_perpage: integer containing the number of metabolites on a plot per page +#' @param number_of_samples: list containing the number of patient and control samples +#' @param run_name: string containing the run name +#' @param protocol_name: string containing the protocol name +#' @param explanation_violin_plot: vector of strings containing the explanation of the violin plots +#' @param number_of_metabolites: list containing the number of metabolites for the top and lowest table +make_and_save_violin_plot_pdfs <- function( + zscore_patients_df, + zscore_controls_df, + path_metabolite_groups, + nr_plots_perpage, + number_of_samples, + run_name, + protocol_name, + explanation_violin_plot, + number_of_metabolites) { + # Get all patient IDs + patient_col_names <- get_colnames_samples(zscore_patients_df, "P") + # get all files from metabolite_groups directory + metabolite_dirs <- list.files(path = path_metabolite_groups, full.names = FALSE, recursive = FALSE) + for (metabolite_dir in metabolite_dirs) { + # create a directory for the output PDFs + pdf_dir <- paste0("./", metabolite_dir) + dir.create(pdf_dir, showWarnings = FALSE) + + metab_list_all <- get_list_dataframes_from_dir(paste(path_metabolite_groups, metabolite_dir, sep = "/")) + metab_interest_patients <- merge_metabolite_info_zscores(metab_list_all, zscore_patients_df) + metab_interest_controls <- merge_metabolite_info_zscores(metab_list_all, zscore_controls_df) + metab_perpage <- get_data_per_metabolite_class( + metab_interest_patients, + metab_interest_controls, + nr_plots_perpage, + number_of_samples$patients, + number_of_samples$controls + ) + + # for Diagnostics metabolites to be saved in Helix + if (grepl("Diagnost", pdf_dir)) { + # get table that combines DIMS results with metabolite classes/Helix table + dims_helix_table <- prepare_helix_patient_data(metab_interest_patients, metab_list_all) + # check if run contains diagnostic patients (e.g. "P2024M") + if (any(is_diagnostic_patients(dims_helix_table$Sample))) { + # transform dataframe for Helix output + output_helix <- transform_metab_df_to_helix_df(protocol_name, dims_helix_table) + # save the DIMS Helix dataframe + path_helixfile <- paste0("./output_Helix_", run_name, ".csv") + write.csv(output_helix, path_helixfile, quote = FALSE, row.names = FALSE) + } + } + + # make violin plots per patient + for (patient_id in patient_col_names) { + if (grepl("Diagnost", pdf_dir)) { + # make list of metabolites that exceed alarm values for this patient + top_metabs_patient <- get_top_metabolites_df(patient_id, dims_helix_table) + } else { + # make list of top highest and lowest Z-scores for this patient + top_metabs_patient <- prepare_toplist( + patient_id, + zscore_patients_df, + number_of_metabolites$highest, + number_of_metabolites$lowest + ) + } + # generate normal violin plots + create_pdf_violin_plots(pdf_dir, patient_id, metab_perpage, top_metabs_patient, explanation_violin_plot) + } + } } #' Get a list with dataframes for all off the metabolite group in a directory #' -#' @param metab_group_dir: directory containing txt files with metabolites per group (string) +#' @param dir_with_subdirs: directory containing txt files with metabolites per group (string) #' -#' @returns: list with dataframes with info on metabolites (list of dataframes) -get_list_metabolites <- function(metab_group_dir) { +#' @returns list_of_dataframes: list with dataframes with info on metabolites (list of dataframes) +get_list_dataframes_from_dir <- function(dir_with_subdirs) { # get a list of all metabolite files - metabolite_files <- list.files(metab_group_dir, pattern = "*.txt", full.names = FALSE, recursive = FALSE) + txt_files_paths <- list.files(dir_with_subdirs, pattern = "*.txt", recursive = FALSE, full.names = TRUE) # put all metabolites into one list - metab_list_all <- lapply(paste(metab_group_dir, metabolite_files, sep = "/"), - read.table, sep = "\t", header = TRUE, quote = "") - names(metab_list_all) <- gsub(".txt", "", metabolite_files) + list_of_dataframes <- lapply(txt_files_paths, + read.table, + sep = "\t", header = TRUE, quote = "" + ) + names(list_of_dataframes) <- gsub(".txt", "", basename(txt_files_paths)) - return(metab_list_all) + return(list_of_dataframes) } -#' Combine patient Z-scores with metabolite info +#' Merge patient Z-scores with metabolite info #' -#' @param metab_list_all: list of dataframes with metabolite information for different stofgroepen (list) +#' @param list_df_metabolite_groups: list of dataframes with metabolite information for different metabolite classes (list) #' @param zscore_df: dataframe with metabolite Z-scores for all patient #' -#' @return: list of dataframes for each stofgroep with data for each metabolite and patient/control per row -combine_metab_info_zscores <- function(metab_list_all, zscore_df) { +#' @return list_dfs_metabs_info_zscores: list of dataframes for each metabolite class +#' containing info and zscores for all samples +merge_metabolite_info_zscores <- function(list_df_metabolite_groups, zscore_df) { # remove HMDB_name column and "_Zscore" from column (patient) names zscore_df <- zscore_df %>% - select(-HMDB_name) %>% - rename_with(~ str_remove(.x, "_Zscore"), .cols = contains("_Zscore")) + select(-HMDB_name) # put data into pages, max 20 violin plots per page in PDF - metab_interest_sorted <- list() + list_dfs_metabs_info_zscores <- list() - for (metab_class in names(metab_list_all)) { - metab_df <- metab_list_all[[metab_class]] - # Select HMDB_code and HMDB_name columns - metab_df <- metab_df %>% select(HMDB_code, HMDB_name) + for (metabolite_class in names(list_df_metabolite_groups)) { + # select the metabolite_class dataframe and select the HMDB_code and HMDB_name columns + metabolite_info_df <- list_df_metabolite_groups[[metabolite_class]] %>% select(HMDB_code, HMDB_name) - # Change the HMDB_name column so all names have 45 characters - metab_df <- metab_df %>% mutate(HMDB_name = case_when( - str_length(HMDB_name) > 45 ~ str_c(str_sub(HMDB_name, 1, 42), "..."), - str_length(HMDB_name) < 45 ~ str_pad(HMDB_name, 45, side = "right", pad = " "), - TRUE ~ HMDB_name - )) + # Pad or truncate the HMDB names + metabolite_info_df <- pad_truncate_hmdb_names(metabolite_info_df, 45, " ") # Join metabolite info with the Z-score dataframe - metab_interest <- metab_df %>% inner_join(zscore_df, by = "HMDB_code") %>% select(-HMDB_code) + metabolite_zscore_df <- metabolite_info_df %>% + inner_join(zscore_df, by = "HMDB_code") %>% + select(-HMDB_code) # put the data frame in long format - metab_interest_melt <- reshape2::melt(metab_interest, id.vars = "HMDB_name", variable.name = "Sample", - value.name = "Z_score") + metabolite_zscore_df_long <- reshape2::melt( + metabolite_zscore_df, + id.vars = "HMDB_name", + variable.name = "Sample", + value.name = "Z_score" + ) # Add the dataframe sorted on HMDB_name to a list - metab_interest_sorted[[metab_class]] <- metab_interest_melt + list_dfs_metabs_info_zscores[[metabolite_class]] <- metabolite_zscore_df_long } - return(metab_interest_sorted) + return(list_dfs_metabs_info_zscores) } #' Combine patient and control data for each page of the violinplot pdf #' -#' @param metab_interest_sorted: list of dataframes with data for each metabolite and patient (list) -#' @param metab_interest_contr: list of dataframes with data for each metabolite and control (list) -#' @param nr_plots_perpage: number of plots per page in the violinplot pdf (integer) -#' @param nr_pat: number of patients (integer) -#' @param nr_contr: number of controls (integer) +#' @param metab_interest_patients: list of dataframes with data for each metabolite and patient (list) +#' @param metab_interest_controls: list of dataframes with data for each metabolite and control (list) +#' @param number_of_plots_per_page: number of plots per page in the violinplot pdf (integer) +#' @param number_of_patients: number of patients (integer) +#' @param number_of_controls: number of controls (integer) #' -#' @return: list of dataframes with metabolite Z-scores for each patient and control, +#' @return list_metabolite_df_per_page: list of dataframes with metabolite Z-scores for each patient and control, #' the length of list is the number of pages for the violinplot pdf (list) -prepare_data_perpage <- function(metab_interest_sorted, metab_interest_contr, nr_plots_perpage, nr_pat, nr_contr) { - metab_perpage <- list() - metab_category <- c() - - for (metab_class in names(metab_interest_sorted)) { +get_data_per_metabolite_class <- function( + metab_interest_patients, + metab_interest_controls, + number_of_plots_per_page, + number_of_patients, + number_of_controls) { + list_metabolite_df_per_page <- list() + metabolite_categories <- c() + + for (metabolite_class in names(metab_interest_patients)) { # Get the data for patients and controls for the metab_interest_sorted list - metab_sort_patients_df <- metab_interest_sorted[[metab_class]] - metab_sort_controls_df <- metab_interest_contr[[metab_class]] - - # Calculate the number of pages - nr_pages <- ceiling(length(unique(metab_sort_patients_df$HMDB_name)) / nr_plots_perpage) - - # Get all metabolites and create list with HMDB naames of max nr_plots_perpage long - metabolites <- unique(metab_sort_patients_df$HMDB_name) - metabolites_in_chunks <- split(metabolites, ceiling(seq_along(metabolites) / nr_plots_perpage)) - nr_chunks <- length(metabolites_in_chunks) - - current_perpage <- lapply(metabolites_in_chunks, function(metab_name) { - patients_df <- metab_sort_patients_df %>% filter(HMDB_name %in% metab_name) - controls_df <- metab_sort_controls_df %>% filter(HMDB_name %in% metab_name) - - # Combine both dataframes - combined_df <- rbind(patients_df, controls_df) - - # Add empty dummy's to extend the number of metabs to the nr_plots_perpage - n_missing <- nr_plots_perpage - length(metab_name) - if (n_missing > 0) { - dummy_names <- paste0(" ", strrep(" ", seq_len(n_missing))) - metab_order <- c(metab_name, dummy_names) - } else { - metab_order <- metab_name - } - attr(combined_df, "y_order") <- rev(metab_order) + metabolite_class_patients_df <- metab_interest_patients[[metabolite_class]] + metabolite_class_controls_df <- metab_interest_controls[[metabolite_class]] + + # Get all metabolites and create list with HMDB names of max nr_plots_perpage long + metabolites <- unique(metabolite_class_patients_df$HMDB_name) + metabolites_in_chunks <- split(metabolites, ceiling(seq_along(metabolites) / number_of_plots_per_page)) + number_of_chunks_metabolites <- length(metabolites_in_chunks) + + # Get a list of plot data per page + page_plot_data_list <- get_list_page_plot_data( + metabolites_in_chunks, + metabolite_class_patients_df, + metabolite_class_controls_df, + number_of_plots_per_page + ) - return(combined_df) - }) # Add new items to main list - metab_perpage <- append(metab_perpage, current_perpage) + list_metabolite_df_per_page <- append(list_metabolite_df_per_page, page_plot_data_list) # create list of page headers - metab_category <- c(metab_category, paste(metab_class, seq(nr_chunks), sep = "_")) + metabolite_categories <- c(metabolite_categories, paste(metabolite_class, seq(number_of_chunks_metabolites), sep = "_")) } # add page headers to list - names(metab_perpage) <- metab_category + names(list_metabolite_df_per_page) <- metabolite_categories - return(metab_perpage) + return(list_metabolite_df_per_page) } #' Get patient data to be uploaded to Helix #' -#' @param metab_interest_sorted: list of dataframes with metabolite Z-scores for each sample/patient (list) -#' @param metab_list_all: list of tables with metabolites for Helix and violin plots (list) +#' @param list_dfs_metab_classes_zscores: list of dataframes with metabolite Z-scores for each sample/patient (list) +#' @param list_metabolite_classes: list of tables with metabolites for Helix and violin plots (list) #' -#' @return: dataframe with patient data with only metabolites for Helix and violin plots +#' @return df_zscores_to_helix: dataframe with patient data with only metabolites for Helix and violin plots #' with Helix name, high/low Z-score cutoffs -get_patient_data_to_helix <- function(metab_interest_sorted, metab_list_all) { +prepare_helix_patient_data <- function(list_dfs_metab_classes_zscores, list_metabolite_classes) { # Combine Z-scores of metab groups together - df_all_metabs_zscores <- bind_rows(metab_interest_sorted) + metabolite_zscore_dataframe <- bind_rows(list_dfs_metab_classes_zscores) # Change the Sample column to characters, trim HMDB_name and split HMDB_name in new column - df_all_metabs_zscores <- df_all_metabs_zscores %>% - mutate(Sample = as.character(Sample), - HMDB_name = str_trim(HMDB_name, "right"), - HMDB_name_split = str_split_fixed(HMDB_name, "nitine;", 2)[, 1]) + metabolite_zscore_dataframe <- metabolite_zscore_dataframe %>% + mutate( + Sample = as.character(Sample), + HMDB_name = str_trim(HMDB_name, "right"), + HMDB_name_split = str_split_fixed(HMDB_name, "nitine;", 2)[, 1] + ) - # Combine stofgroepen - dims_helix_table <- bind_rows(metab_list_all) + # Combine metabolite classes + dims_helix_metabolite_df <- bind_rows(list_metabolite_classes) - # Filter for Helix metabolites and split HMDB_name column for matching with df_all_metabs_zscores - dims_helix_table <- dims_helix_table %>% + # Filter for Helix metabolites and split HMDB_name column for matching with metabolite_zscore_dataframe + dims_helix_metabolite_df <- dims_helix_metabolite_df %>% filter(Helix == "ja") %>% mutate(HMDB_name_split = str_split_fixed(HMDB_name, "nitine;", 2)[, 1]) %>% select(HMDB_name_split, Helix_naam, high_zscore, low_zscore) # Filter DIMS results for metabolites for Helix and combine Helix info - df_metabs_helix <- df_all_metabs_zscores %>% - filter(HMDB_name_split %in% dims_helix_table$HMDB_name_split) %>% - left_join(dims_helix_table, by = join_by(HMDB_name_split)) %>% + df_zscores_to_helix <- metabolite_zscore_dataframe %>% + filter(HMDB_name_split %in% dims_helix_metabolite_df$HMDB_name_split) %>% + left_join(dims_helix_metabolite_df, by = join_by(HMDB_name_split)) %>% select(HMDB_name, Sample, Z_score, Helix_naam, high_zscore, low_zscore) - return(df_metabs_helix) + return(df_zscores_to_helix) +} + +#' Getting the intensities for calculating ratio Z-scores +#' Retrieving a vector of intensities for a particular fraction side of the ratios for all samples. +#' +#' @param ratios_metabs_df: dataframe with HMDB codes for the ratios (dataframe) +#' @param row_index: index of the row in the ratios_metabs_df (integer) +#' @param intensities_zscore_df: dataframe with intensities for each sample (dataframe) +#' @param fraction_side: either numerator or denominator, which side of the fraction (string) +#' @param intensity_cols: names of the columns that contain the intensities (string) +#' +#' @returns fraction_side_intensity: a vector of intensities (vector of integers) +get_intensities_fraction_side <- function(ratios_metabs_df, row_index, intensities_zscore_df, fraction_side, intensity_cols) { + # get the HMDB ID(s) for the given fraction side + fraction_side_hmdb_ids <- ratios_metabs_df[row_index, fraction_side] + if (grepl("plus", fraction_side_hmdb_ids)) { + # if fraction side contains "plus", split to get both HMDB IDs + fraction_side_hmdb_id_list <- strsplit(fraction_side_hmdb_ids, "plus")[[1]] + # get intensities for both HMDB IDs for all samples + fraction_side_intensity_list <- intensities_zscore_df %>% + filter(HMDB_code %in% fraction_side_hmdb_id_list) %>% + select(any_of(intensity_cols)) + # sum intensities to 1 intensity per samples + fraction_side_intensity <- apply(fraction_side_intensity_list, 2, sum) + } else if (fraction_side_hmdb_ids == "one") { + # set intensity to 1 + fraction_side_intensity <- 1 + } else { + # get intensities of the HMDB ID for all samples + fraction_side_intensity <- intensities_zscore_df %>% + filter(HMDB_code == fraction_side_hmdb_ids) %>% + select(any_of(intensity_cols)) + } + # vector of intensities for all samples + fraction_side_intensity <- as.numeric(fraction_side_intensity) + return(fraction_side_intensity) +} + +#' Get the sample IDs for columns that have Z-score and intensities +#' +#' @param colnames_zscore_cols: vector of sample IDs from the dataframe containing Z-scores (vector of strings) +#' @param colnames_intensity_cols: vector of sample IDs form the dataframe containing intensities (vector of strings) +#' +#' @returns colnames_intersect: vector of sample IDs that are in both input vectors, ending on "_Zscore" (vector of strings) +get_sample_ids_with_zscores <- function(colnames_zscore_cols, colnames_intensity_cols) { + colnames_intersect <- intersect( + paste0(colnames_intensity_cols, "_Zscore"), + grep("_Zscore", colnames_zscore_cols, value = TRUE) + ) + return(colnames_intersect) +} + +#' Pad or truncate HMDB names to a fixed width +#' Add spaces or remove HMDB name characters till the length of the name equals the 'width' +#' +#' @param metabolite_info_df: A dataframe containing a column `HMDB_name` (character). +#' @param width: Integer target width for the display names. Default is 45. +#' @param pad_character: Single character used for padding. Default is a space `" "`. +#' +#' @return metabolite_info_df: A dataframe where the HMDB names are transformed +pad_truncate_hmdb_names <- function(metabolite_info_df, width, pad_character) { + # Change the HMDB_name column so all names have 45 characters + # remove characters if name is longer and add "..." + # add empty spaces till 45 charachters if name is shorter + # keep the name if name is exactly 45 characters + keep_lenght <- width - 3 + metabolite_info_df <- metabolite_info_df %>% mutate(HMDB_name = case_when( + str_length(HMDB_name) > width ~ str_c(str_sub(HMDB_name, 1, keep_lenght), "..."), + str_length(HMDB_name) < width ~ str_pad(HMDB_name, width, side = "right", pad = pad_character), + TRUE ~ HMDB_name + )) + return(metabolite_info_df) +} + +#' Get a list of dataframes for each chunk +#' For each chunk, get a dataframe containing the metabolites in that chunk and add it to the list +#' +#' @param metabolites_in_chunks: list of vectors, each containing metabolites +#' @param metabolite_class_patients_df: dataframe of Z-scores for all patient +#' @param metabolite_class_controls_df: dataframe of Z-scores for all control +#' @param number_of_plots_per_page: integer containing the number of metabolites per plot per page +#' +#' @returns page_plot_data_list: a list of dataframes containing Z-scores +get_list_page_plot_data <- function( + metabolites_in_chunks, + metabolite_class_patients_df, + metabolite_class_controls_df, + number_of_plots_per_page) { + # For each chunk, get a dataframe containing the metabolites and add to a list + page_plot_data_list <- lapply(metabolites_in_chunks, function(metabolite_names_chunk) { + patients_df_chunk <- metabolite_class_patients_df %>% filter(HMDB_name %in% metabolite_names_chunk) + controls_df_chunk <- metabolite_class_controls_df %>% filter(HMDB_name %in% metabolite_names_chunk) + + # Combine both dataframes + patients_controls_df_chunk <- rbind(patients_df_chunk, controls_df_chunk) + metabolite_order <- make_metabolite_order(number_of_plots_per_page, metabolite_names_chunk) + + # Set the order of the metabolites for the violin plots + attr(patients_controls_df_chunk, "y_order") <- rev(metabolite_order) + + return(patients_controls_df_chunk) + }) +} + +make_metabolite_order <- function(number_of_plots_per_page, metabolite_names_chunk) { + # Add empty dummy's to extend the number of metabs to the nr_plots_perpage + number_of_plots_missing <- number_of_plots_per_page - length(metabolite_names_chunk) + if (number_of_plots_missing > 0) { + dummy_names <- paste0(" ", strrep(" ", seq_len(number_of_plots_missing))) + metabolite_order <- c(metabolite_names_chunk, dummy_names) + } else { + metabolite_order <- metabolite_names_chunk + } + return(metabolite_order) } #' Check for Diagnostics patients with correct patient number (e.g. starting with "P2024M") @@ -189,7 +443,7 @@ get_patient_data_to_helix <- function(metab_interest_sorted, metab_list_all) { #' @param patient_column: a column from dataframe with IDs (character vector) #' #' @return: a logical vector with TRUE or FALSE for each element (vector) -is_diagnostic_patient <- function(patient_column) { +is_diagnostic_patients <- function(patient_column) { diagnostic_patients <- grepl("^P[0-9]{4}M", patient_column) return(diagnostic_patients) @@ -201,9 +455,9 @@ is_diagnostic_patient <- function(patient_column) { #' @param df_metabs_helix: dataframe with metabolite Z-scores for patients (dataframe) #' #' @return: dataframe with patient metabolite Z-scores in correct format for Helix -output_for_helix <- function(protocol_name, df_metabs_helix) { +transform_metab_df_to_helix_df <- function(protocol_name, df_metabs_helix) { # Remove positive controls - df_metabs_helix <- df_metabs_helix %>% filter(is_diagnostic_patient(Sample)) + df_metabs_helix <- df_metabs_helix %>% filter(is_diagnostic_patients(Sample)) # Add 'Vial' column, each patient has unique ID df_metabs_helix <- df_metabs_helix %>% @@ -255,7 +509,7 @@ add_lab_id_and_onderzoeksnr <- function(df_metabs_helix) { #' @param dims_helix_table: dataframe with metabolite Z-scores for each patient and Helix info (dataframe) #' #' @return: dataframe with metabolites that exceed the min and max Z-score cutoffs for the selected patient -prepare_alarmvalues <- function(patient_name, dims_helix_table) { +get_top_metabolites_df <- function(patient_name, dims_helix_table) { # extract data for patient of interest (patient_name) patient_metabs_helix <- dims_helix_table %>% filter(Sample == patient_name) %>% @@ -266,15 +520,19 @@ prepare_alarmvalues <- function(patient_name, dims_helix_table) { if (nrow(patient_high_df) > 0 || nrow(patient_low_df) > 0) { # sort tables on zscore - patient_high_df <- patient_high_df %>% arrange(desc(Z_score)) %>% select(c(HMDB_name, Z_score)) - patient_low_df <- patient_low_df %>% arrange(Z_score) %>% select(c(HMDB_name, Z_score)) + patient_high_df <- patient_high_df %>% + arrange(desc(Z_score)) %>% + select(c(HMDB_name, Z_score)) + patient_low_df <- patient_low_df %>% + arrange(Z_score) %>% + select(c(HMDB_name, Z_score)) } # add lines for increased, decreased - extra_line1 <- c("Increased", "") - extra_line2 <- c("Decreased", "") + line_increased <- c("Increased", "") + line_decreased <- c("Decreased", "") # combine the two lists - top_metab_patient <- rbind(extra_line1, patient_high_df, extra_line2, patient_low_df) + top_metab_patient <- rbind(line_increased, patient_high_df, line_decreased, patient_low_df) # remove row names rownames(top_metab_patient) <- NULL @@ -292,19 +550,17 @@ prepare_alarmvalues <- function(patient_name, dims_helix_table) { #' @param top_lowest: the number of metabolites with the lowest Z-score to display in the table (numeric) #' #' @return: dataframe with 30 metabolites and Z-scores (dataframe) -prepare_toplist <- function(patient_id, zscore_patients) { - top_highest <- 20 - top_lowest <- 10 +prepare_toplist <- function(patient_id, zscore_patients, num_of_highest_metabolites, num_of_lowest_metabolites) { patient_df <- zscore_patients %>% select(HMDB_code, HMDB_name, !!sym(patient_id)) %>% arrange(!!sym(patient_id)) # Get lowest Zscores - patient_df_low <- patient_df[1:top_lowest, ] + patient_df_low <- patient_df[1:num_of_lowest_metabolites, ] patient_df_low <- patient_df_low %>% mutate(across(!!sym(patient_id), ~ round(.x, 2))) # Get highest Zscores - patient_df_high <- patient_df[nrow(patient_df):(nrow(patient_df) - top_highest + 1), ] + patient_df_high <- patient_df[nrow(patient_df):(nrow(patient_df) - num_of_highest_metabolites + 1), ] patient_df_high <- patient_df_high %>% mutate(across(!!sym(patient_id), ~ round(.x, 2))) # add lines for increased, decreased @@ -332,10 +588,16 @@ create_pdf_violin_plots <- function(pdf_dir, patient_id, metab_perpage, top_meta plot_height <- 9.6 plot_width <- 6 + # get the names and numbers in the table aligned + table_theme <- ttheme_default( + core = list(fg_params = list(hjust = 0, x = 0.05, fontsize = 6)), + colhead = list(fg_params = list(fontsize = 8, fontface = "bold")) + ) + # patient plots, create the PDF device patient_id_sub <- patient_id suffix <- "" - if (grepl("Diagnostics", pdf_dir) && is_diagnostic_patient(patient_id)) { + if (grepl("Diagnostics", pdf_dir) && is_diagnostic_patients(patient_id)) { prefix <- "MB" suffix <- "_DIMS_PL_DIAG" # substitute P and M in P2020M00001 into right format for Helix @@ -350,9 +612,10 @@ create_pdf_violin_plots <- function(pdf_dir, patient_id, metab_perpage, top_meta } pdf(paste0(pdf_dir, "/", prefix, patient_id_sub, suffix, ".pdf"), - onefile = TRUE, - width = plot_width, - height = plot_height) + onefile = TRUE, + width = plot_width, + height = plot_height + ) # page headers: page_headers <- names(metab_perpage) @@ -364,8 +627,10 @@ create_pdf_violin_plots <- function(pdf_dir, patient_id, metab_perpage, top_meta number_of_pages <- ceiling(total_rows / max_rows_per_page) # get the names and numbers in the table aligned - table_theme <- ttheme_default(core = list(fg_params = list(hjust = 0, x = 0.05, fontsize = 6)), - colhead = list(fg_params = list(fontsize = 8, fontface = "bold"))) + table_theme <- ttheme_default( + core = list(fg_params = list(hjust = 0, x = 0.05, fontsize = 6)), + colhead = list(fg_params = list(fontsize = 8, fontface = "bold")) + ) for (page in seq(number_of_pages)) { start_row <- (page - 1) * max_rows_per_page + 1 @@ -440,23 +705,31 @@ create_violin_plot <- function(metab_zscores_df, patient_zscore_df, sub_perpage, # Make violin plots geom_violin(scale = "width", na.rm = TRUE) + # Add Z-score for the selected patient, shape=22 gives square for patient of interest - geom_point(data = patient_zscore_df, aes(color = Z_score), - size = 3.5 * circlesize, shape = 22, fill = "white", na.rm = TRUE) + + geom_point( + data = patient_zscore_df, aes(color = Z_score), + size = 3.5 * circlesize, shape = 22, fill = "white", na.rm = TRUE + ) + # Add the Z-score at the right side of the plot - geom_text(data = patient_zscore_df, - aes(16, label = paste0("Z=", round(Z_score, 2))), - hjust = "left", vjust = +0.2, size = 3, na.rm = TRUE) + + geom_text( + data = patient_zscore_df, + aes(16, label = paste0("Z=", round(Z_score, 2))), + hjust = "left", vjust = +0.2, size = 3, na.rm = TRUE + ) + # Set colour for the Z-score of the selected patient - scale_fill_gradientn(colors = colors_plot, values = NULL, space = "Lab", - na.value = "grey50", guide = "colourbar", aesthetics = "colour") + + scale_fill_gradientn( + colors = colors_plot, values = NULL, space = "Lab", + na.value = "grey50", guide = "colourbar", aesthetics = "colour" + ) + # Add labels to the axis labs(x = "Z-scores", y = "Metabolites", subtitle = sub_perpage, color = "z-score") + # Add a title to the page ggtitle(label = paste0("Results for patient ", patient_id)) + # Set theme: size and font type of y-axis labels, remove legend and make the - theme(axis.text.y = element_text(family = "Courier", size = 6), - legend.position = "none", - plot.caption = element_text(size = rel(fontsize))) + + theme( + axis.text.y = element_text(family = "Courier", size = 6), + legend.position = "none", + plot.caption = element_text(size = rel(fontsize)) + ) + # Set y-axis to set order scale_y_discrete(limits = y_order) + # Limit the x-axis to between -5 and 20 @@ -470,7 +743,7 @@ create_violin_plot <- function(metab_zscores_df, patient_zscore_df, sub_perpage, #' Run the dIEM algorithm (DOI: 10.3390/ijms21030979) #' -#' @param expected_biomarkers_df: table with information for HMDB codes about IEMs (dataframe) +#' @param expected_biomarkers_df: dataframe with information for HMDB codes about IEMs (dataframe) #' @param zscore_patients: dataframe containing Z-scores for patient (dataframe) #' #' @returns probability_score: a dataframe with probability scores for IEMs for each patient (dataframe) @@ -479,11 +752,15 @@ run_diem_algorithm <- function(expected_biomarkers_df, zscore_patients_df, sampl ranking_patients <- zscore_patients_df %>% mutate(across(-c(HMDB_code, HMDB_name), rank_patient_zscores)) - ranking_patients <- merge(x = expected_biomarkers_df, y = ranking_patients, - by.x = c("HMDB_code"), by.y = c("HMDB_code")) + ranking_patients <- merge( + x = expected_biomarkers_df, y = ranking_patients, + by.x = c("HMDB_code"), by.y = c("HMDB_code") + ) - zscore_expected_df <- merge(x = expected_biomarkers_df, y = zscore_patients_df, - by.x = c("HMDB_code"), by.y = c("HMDB_code")) + zscore_expected_df <- merge( + x = expected_biomarkers_df, y = zscore_patients_df, + by.x = c("HMDB_code"), by.y = c("HMDB_code") + ) # Change Z-score to zero for specific cases zscore_expected_df <- zscore_expected_df %>% mutate(across( @@ -557,15 +834,133 @@ rank_patient_zscores <- function(zscore_col) { #' Save the probability score dataframe as an Excel file #' #' @param probability_score: a dataframe containing probability scores for each patient (dataframe) -#' @param output_dir: location where to save the Excel file (string) #' @param run_name: name of the run, for the file name (string) -save_prob_scores_to_excel <- function(probability_score, output_dir, run_name) { +save_prob_scores_to_excel <- function(probability_score, run_name) { # Create conditional formatting for output Excel sheet. Colors according to values. wb <- createWorkbook() addWorksheet(wb, "Probability Scores") writeData(wb, "Probability Scores", probability_score) - conditionalFormatting(wb, "Probability Scores", cols = 2:ncol(probability_score), rows = seq_len(nrow(probability_score)), - type = "colourScale", style = c("white", "#FFFDA2", "red"), rule = c(1, 10, 100)) - saveWorkbook(wb, file = paste0(output_dir, "/dIEM_algoritme_output_", run_name, ".xlsx"), overwrite = TRUE) + conditionalFormatting(wb, "Probability Scores", + cols = 2:ncol(probability_score), rows = seq_len(nrow(probability_score)), + type = "colourScale", style = c("white", "#FFFDA2", "red"), rule = c(1, 10, 100) + ) + saveWorkbook(wb, file = paste0("./dIEM_algoritme_output_", run_name, ".xlsx"), overwrite = TRUE) rm(wb) } + +#' Make and save dIEM plots +#' +#' @param diem_probability_score: dataframe with dIEM probability scores +#' @param patient_col_names: vector containing all patient column names +#' @param expected_biomarkers_df: dataframe with information for HMDB codes about IEMs +#' @param zscore_patients_df: dataframe containing Z-scores for all patients +#' @param zscore_controls_df: dataframe containing Z-scores for all controls +#' @param nr_plots_perpage: integer containing the number of metabolites per page +#' @param number_of_samples: list containing the number of patients and controls +#' @param number_of_metabolites: list containing the number of metabolites for the top and lowest table +#' +#' @returns patient_no_iem: vector of patient IDs that have no IEMs +make_and_save_diem_plots <- function( + diem_probability_score, + patient_col_names, + expected_biomarkers_df, + zscore_patients_df, + zscore_controls_df, + nr_plots_perpage, + number_of_samples, + number_of_metabolites) { + diem_plot_dir <- paste("./dIEM_plots", sep = "/") + dir.create(diem_plot_dir) + + patient_no_iem <- c() + + for (patient_id in patient_col_names) { + # Select the top IEMs and filter on the IEM threshold + patient_top_iems_probs <- diem_probability_score %>% + select(c(Disease, !!sym(patient_id))) %>% + arrange(desc(!!sym(patient_id))) %>% + slice(1:top_number_iem_diseases) %>% + filter(!!sym(patient_id) >= threshold_iem) + + if (nrow(patient_top_iems_probs) > 0) { + list_metabolites_top_iems <- get_probabilities_top_iems(patient_top_iems_probs, expected_biomarkers_df, patient_id) + + # Get the Z-scores with metabolite information + metabolites_iem_sorted <- merge_metabolite_info_zscores(list_metabolites_top_iems, zscore_patients_df) + metabolites_iem_controls <- merge_metabolite_info_zscores(list_metabolites_top_iems, zscore_controls_df) + + # Get a list of dataframes for each IEM + diem_metabolites_perpage <- get_data_per_metabolite_class( + metabolites_iem_sorted, + metabolites_iem_controls, + nr_plots_perpage, + number_of_samples$patients, + number_of_samples$controls + ) + # Get a dataframe of the top metabolites + top_metabolites_patient <- prepare_toplist( + patient_id, + zscore_patients_df, + number_of_metabolites$highest, + number_of_metabolites$lowest + ) + + # Generate and save dIEM violin plots + create_pdf_violin_plots( + diem_plot_dir, + patient_id, + diem_metabolites_perpage, + top_metabolites_patient, + explanation_violin_plot + ) + } else { + patient_no_iem <- c(patient_no_iem, patient_id) + } + } + return(patient_no_iem) +} + +#' Get the IEM probabilities for a patient for all diseases +#' +#' @param patient_top_iems_probs: dataframe containing the probability scores for diseases for a patient +#' @param expected_biomarkers_df: dataframe with information for HMDB codes about IEMs +#' @param patient_id: string containing the patien ID +#' +#' @returns list_metabolites_iems: list of dataframes containing the HMDB codes and names for all diseases +get_probabilities_top_iems <- function(patient_top_iems_probs, expected_biomarkers_df, patient_id) { + # Get the metabolites for each IEM and their probability + list_metabolites_iems <- list() + metabolites_iems_names <- c() + + for (iem in patient_top_iems_probs$Disease) { + # get the IEM probabilities for the selected patient + iem_probablity <- patient_top_iems_probs %>% + filter(Disease == iem) %>% + pull(!!sym(patient_id)) + metabolites_iems_names <- c(metabolites_iems_names, paste0(iem, ", probability score ", iem_probablity)) + # get the HMDB codes and names for the IEM of the selected patient + metabolites_iem_df <- expected_biomarkers_df %>% + filter(Disease == iem) %>% + select(HMDB_code, HMDB_name) + # Add for each IEM the HMDB codes and names to a list + list_metabolites_iems[[iem]] <- metabolites_iem_df + } + names(list_metabolites_iems) <- metabolites_iems_names + + return(list_metabolites_iems) +} + +#' Save a list of patient IDs to a text file +#' +#' @param threshold_iem: integer containing the IEM threshold +#' @param patient_no_iem: vector containing patient IDs +save_patient_no_iem <- function(threshold_iem, patient_no_iem) { + patient_no_iem <- c( + paste0( + "The following patient(s) did not have dIEM probability scores higher than ", + threshold_iem, " :" + ), + patient_no_iem + ) + write(file = paste0("./missing_probability_scores.txt"), patient_no_iem) +} From 0660b1e1816a9587e0906bcc8f4c7fed8dc84e13 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 26 Feb 2026 15:56:16 +0100 Subject: [PATCH 19/30] Moved variables to list --- DIMS/GenerateViolinPlots.R | 9 +++++++-- DIMS/export/generate_violin_plots_functions.R | 15 ++++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/DIMS/GenerateViolinPlots.R b/DIMS/GenerateViolinPlots.R index 2b742be0..cad5d83d 100644 --- a/DIMS/GenerateViolinPlots.R +++ b/DIMS/GenerateViolinPlots.R @@ -32,6 +32,10 @@ expected_biomarkers_df <- expected_biomarkers_df %>% explanation_violin_plot <- readLines(file_explanation) # Set global variables +iem_variables <- list( + top_number_iem_diseases = 5, + threshold_iem = 5 +) top_number_iem_diseases <- 5 # number of diseases that score highest in algorithm to plot threshold_iem <- 5 # probability score cut-off for plotting the top diseases nr_plots_perpage <- 20 # number of violin plots per page in PDF @@ -90,9 +94,10 @@ patient_no_iem <- make_and_save_diem_plots( zscore_controls_df, nr_plots_perpage, number_of_samples, - number_of_metabolites + number_of_metabolites, + iem_variables ) if (length(patient_no_iem) > 0) { - save_patient_no_iem(threshold_iem, patient_no_iem) + save_patient_no_iem(iem_variables$threshold_iem, patient_no_iem) } diff --git a/DIMS/export/generate_violin_plots_functions.R b/DIMS/export/generate_violin_plots_functions.R index 5ae656b3..83ad7cd7 100644 --- a/DIMS/export/generate_violin_plots_functions.R +++ b/DIMS/export/generate_violin_plots_functions.R @@ -426,6 +426,14 @@ get_list_page_plot_data <- function( }) } +#' Make the order of metabolites for the violin plots +#' Create the order of metabolites and add empty strings if the number of metabolites is lower than +#' the number of plots per page. +#' +#' @param number_of_plots_per_page: integer containing the number of metabolites per plot per page +#' @param metabolite_names_chunk: list of vectors, each containing metabolites +#' +#' @returns metabolite_order: a vector containing all metabolites and possibly empty strings make_metabolite_order <- function(number_of_plots_per_page, metabolite_names_chunk) { # Add empty dummy's to extend the number of metabs to the nr_plots_perpage number_of_plots_missing <- number_of_plots_per_page - length(metabolite_names_chunk) @@ -868,7 +876,8 @@ make_and_save_diem_plots <- function( zscore_controls_df, nr_plots_perpage, number_of_samples, - number_of_metabolites) { + number_of_metabolites, + iem_variables) { diem_plot_dir <- paste("./dIEM_plots", sep = "/") dir.create(diem_plot_dir) @@ -879,8 +888,8 @@ make_and_save_diem_plots <- function( patient_top_iems_probs <- diem_probability_score %>% select(c(Disease, !!sym(patient_id))) %>% arrange(desc(!!sym(patient_id))) %>% - slice(1:top_number_iem_diseases) %>% - filter(!!sym(patient_id) >= threshold_iem) + slice(1:iem_variables$top_number_iem_diseases) %>% + filter(!!sym(patient_id) >= iem_variables$threshold_iem) if (nrow(patient_top_iems_probs) > 0) { list_metabolites_top_iems <- get_probabilities_top_iems(patient_top_iems_probs, expected_biomarkers_df, patient_id) From 062698ca1628168182c3e1d7baab97c8f81d9985 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 26 Feb 2026 15:56:32 +0100 Subject: [PATCH 20/30] Added new unit tests --- DIMS/tests/testthat/test_generate_excel.R | 2 +- .../testthat/test_generate_violin_plots.R | 948 ++++++++++++++---- 2 files changed, 737 insertions(+), 213 deletions(-) diff --git a/DIMS/tests/testthat/test_generate_excel.R b/DIMS/tests/testthat/test_generate_excel.R index 7a1b4280..0ea67c95 100644 --- a/DIMS/tests/testthat/test_generate_excel.R +++ b/DIMS/tests/testthat/test_generate_excel.R @@ -42,7 +42,7 @@ testthat::test_that("Calculating Z-scores using different methods for excluding expect_identical(colnames(calculate_zscores(test_outlist, "_Zscore", control_intensities, NULL, intensity_col_ids, startcol)), c("plots", "C101.1", "C102.1", "C103.1", "C104.1", "C105.1", "C106.1", "C107.1", "C108.1", "C109.1", "C110.1", "C111.1", "C112.1", "P2.1", "P3.1", "HMDB_name", "HMDB_name_all", "HMDB_ID_all", "sec_HMDB_ID", - "HMDB_key", "sec_HMDB_ID_rlvc", "name", "relevance", "descr", "origin", "fluids", "tissue", "disease", + "HMDB_key", "sec_HMDB_ID_rlvnc", "name", "relevance", "descr", "origin", "fluids", "tissue", "disease", "pathway", "HMDB_code", "avg_ctrls", "sd_ctrls", "nr_ctrls", "C101.1_Zscore", "C102.1_Zscore", "C103.1_Zscore", "C104.1_Zscore", "C105.1_Zscore", "C106.1_Zscore", "C107.1_Zscore", "C108.1_Zscore", "C109.1_Zscore", "C110.1_Zscore", "C111.1_Zscore", "C112.1_Zscore", "P2.1_Zscore", "P3.1_Zscore")) diff --git a/DIMS/tests/testthat/test_generate_violin_plots.R b/DIMS/tests/testthat/test_generate_violin_plots.R index 55a2dac1..5d0a818a 100644 --- a/DIMS/tests/testthat/test_generate_violin_plots.R +++ b/DIMS/tests/testthat/test_generate_violin_plots.R @@ -1,12 +1,4 @@ # unit tests for GenerateViolinPlots -# functions: get_intentities_for_ratios, get_zscore_columns, -# get_list_metabolites, combine_metab_info_zscores, -# prepare_data_perpage, get_patient_data_to_helix -# is_diagnostic_patient, output_for_helix -# add_lab_id_and_onderzoeksnummer, prepare_alarmvalues -# prepare_toplist, create_pdf_violin_plots -# create_violin_plot, run_diem_algorithm -# rank_patient_zscores, save_prob_scores_to_Excel suppressPackageStartupMessages(library("dplyr")) library(reshape2) @@ -17,7 +9,7 @@ library(stringr) source("../../export/generate_violin_plots_functions.R") -testthat::test_that("Get intensities for calculating the ratios", { +testthat::test_that("get_intensities_fraction_side: Get intensities for calculating the ratios", { test_intensities_zscore_df <- read.delim(test_path("fixtures", "test_intensities_zscore_df.txt")) test_ratios_metabs_df <- data.frame( @@ -26,65 +18,99 @@ testthat::test_that("Get intensities for calculating the ratios", { HMDB_numerator = c("HMDB001", "HMDB002plusHMDB003", "HMDB004"), HMDB_denominator = c("HMDB011", "HMDB012", "one") ) - - test_intensity_cols <- c("C101.1", "C102.1", "C103.1", "C104.1", "C105.1", - "P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5") - - expect_equal(colnames(get_intentities_for_ratios(test_ratios_metabs_df, 1, test_intensities_zscore_df, - "HMDB_numerator", test_intensity_cols)), - c("C101.1", "C102.1", "C103.1", "C104.1", "C105.1", - "P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5")) - expect_equal(unname(get_intentities_for_ratios(test_ratios_metabs_df, 2, test_intensities_zscore_df, - "HMDB_numerator", test_intensity_cols)), - c(2500, 2625, 1750, 2525, 2700, 2500, 2625, 1750, 2525, 2700)) - expect_equal(get_intentities_for_ratios(test_ratios_metabs_df, 3, test_intensities_zscore_df, - "HMDB_denominator", test_intensity_cols), - 1) + expect_equal( + get_intensities_fraction_side( + test_ratios_metabs_df, + 2, + test_intensities_zscore_df, + "HMDB_numerator", + test_intensity_cols + ), + c(2500, 2625, 1750, 2525, 2700, 2500, 2625, 1750, 2525, 2700) + ) + expect_equal( + get_intensities_fraction_side( + test_ratios_metabs_df, + 3, + test_intensities_zscore_df, + "HMDB_denominator", + test_intensity_cols + ), + 1 + ) }) -testthat::test_that("Get samples that have both intensity and Z-score columns", { +testthat::test_that("get_sample_ids_with_zscores: Get samples that have both intensity and Z-score columns", { test_intensities_zscore_df <- read.delim(test_path("fixtures", "test_intensities_zscore_df.txt")) - test_intensity_cols <- c("C101.1", "C102.1", "C103.1", "C104.1", "C105.1", - "P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5") + test_intensity_cols <- c( + "C101.1", "C102.1", "C103.1", "C104.1", "C105.1", + "P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5" + ) - expect_equal(length(get_zscore_columns(colnames(test_intensities_zscore_df), test_intensity_cols)), - 10) - expect_equal(get_zscore_columns(colnames(test_intensities_zscore_df), test_intensity_cols), - c("C101.1_Zscore", "C102.1_Zscore", "C103.1_Zscore", "C104.1_Zscore", "C105.1_Zscore", - "P2025M1_Zscore", "P2025M2_Zscore", "P2025M3_Zscore", "P2025M4_Zscore", "P2025M5_Zscore")) + expect_equal( + length(get_sample_ids_with_zscores(colnames(test_intensities_zscore_df), test_intensity_cols)), + 10 + ) + expect_equal( + get_sample_ids_with_zscores(colnames(test_intensities_zscore_df), test_intensity_cols), + c( + "C101.1_Zscore", "C102.1_Zscore", "C103.1_Zscore", "C104.1_Zscore", "C105.1_Zscore", + "P2025M1_Zscore", "P2025M2_Zscore", "P2025M3_Zscore", "P2025M4_Zscore", "P2025M5_Zscore" + ) + ) test_intensity_cols <- c("C101.1", "C102.1", "C103.1", "C104.1", "C105.1", "P2025M1", "P2025M2", "P2025M3") - expect_equal(length(get_zscore_columns(colnames(test_intensities_zscore_df), test_intensity_cols)), - 8) - expect_equal(get_zscore_columns(colnames(test_intensities_zscore_df), test_intensity_cols), - c("C101.1_Zscore", "C102.1_Zscore", "C103.1_Zscore", "C104.1_Zscore", "C105.1_Zscore", - "P2025M1_Zscore", "P2025M2_Zscore", "P2025M3_Zscore")) + expect_equal( + length(get_sample_ids_with_zscores(colnames(test_intensities_zscore_df), test_intensity_cols)), + 8 + ) + expect_equal( + get_sample_ids_with_zscores(colnames(test_intensities_zscore_df), test_intensity_cols), + c( + "C101.1_Zscore", "C102.1_Zscore", "C103.1_Zscore", "C104.1_Zscore", "C105.1_Zscore", + "P2025M1_Zscore", "P2025M2_Zscore", "P2025M3_Zscore" + ) + ) }) -testthat::test_that("Get a list with dataframes from metabilite files in a directory", { - test_path_metabolite_groups <- test_path("fixtures/test_metabolite_groups") +testthat::test_that("get_list_dataframes_from_dir: Get a list with dataframes from metabilite files in a directory", { + test_path_metabolite_groups <- test_path("fixtures/test_metabolite_groups/Diagnostics") + + expect_type(get_list_dataframes_from_dir(test_path_metabolite_groups), "list") + expect_identical( + names(get_list_dataframes_from_dir(test_path_metabolite_groups)), + c("test_acyl_carnitines", "test_crea_gua") + ) - expect_type(get_list_metabolites(test_path_metabolite_groups), "list") - expect_identical(names(get_list_metabolites(test_path_metabolite_groups)), - c("test_acyl_carnitines", "test_crea_gua")) - - expect_identical(colnames(get_list_metabolites(test_path_metabolite_groups)$test_acyl_carnitines), - c("HMDB_code", "HMDB_name", "Helix", "Helix_naam", "high_zscore", "low_zscore")) - expect_identical(get_list_metabolites(test_path_metabolite_groups)$test_acyl_carnitines$HMDB_name, - c("metab1", "metab3", "ratio1")) - expect_identical(get_list_metabolites(test_path_metabolite_groups)$test_acyl_carnitines$Helix, - c("ja", "nee", "ja")) - - expect_identical(colnames(get_list_metabolites(test_path_metabolite_groups)$test_crea_gua), - c("HMDB_code", "HMDB_name", "Helix", "Helix_naam", "high_zscore", "low_zscore")) - expect_identical(get_list_metabolites(test_path_metabolite_groups)$test_crea_gua$HMDB_name, - c("metab4", "metab11")) - expect_identical(get_list_metabolites(test_path_metabolite_groups)$test_crea_gua$Helix, - c("ja", "ja")) + expect_identical( + colnames(get_list_dataframes_from_dir(test_path_metabolite_groups)$test_acyl_carnitines), + c("HMDB_code", "HMDB_name", "Helix", "Helix_naam", "high_zscore", "low_zscore") + ) + expect_identical( + get_list_dataframes_from_dir(test_path_metabolite_groups)$test_acyl_carnitines$HMDB_name, + c("metab1", "metab3", "ratio1") + ) + expect_identical( + get_list_dataframes_from_dir(test_path_metabolite_groups)$test_acyl_carnitines$Helix, + c("ja", "nee", "ja") + ) + + expect_identical( + colnames(get_list_dataframes_from_dir(test_path_metabolite_groups)$test_crea_gua), + c("HMDB_code", "HMDB_name", "Helix", "Helix_naam", "high_zscore", "low_zscore") + ) + expect_identical( + get_list_dataframes_from_dir(test_path_metabolite_groups)$test_crea_gua$HMDB_name, + c("metab4", "metab11") + ) + expect_identical( + get_list_dataframes_from_dir(test_path_metabolite_groups)$test_crea_gua$Helix, + c("ja", "ja") + ) }) -testthat::test_that("Combine metabolite info dataframe and Z-score dataframe", { +testthat::test_that("merge_metabolite_info_zscores: Combine metabolite info dataframe and Z-score dataframe", { test_acyl_carnitines_df <- read.delim(test_path("fixtures/test_metabolite_groups/", "test_acyl_carnitines.txt")) test_crea_gua_df <- read.delim(test_path("fixtures/test_metabolite_groups/", "test_crea_gua.txt")) @@ -95,38 +121,72 @@ testthat::test_that("Combine metabolite info dataframe and Z-score dataframe", { test_intensities_zscore_df <- read.delim(test_path("fixtures", "test_intensities_zscore_df.txt")) test_zscore_patients_df <- test_intensities_zscore_df %>% select(HMDB_code, HMDB_name, any_of(test_patient_cols)) - expect_type(combine_metab_info_zscores(test_metab_list_all, test_zscore_patients_df), "list") - expect_identical(names(combine_metab_info_zscores(test_metab_list_all, test_zscore_patients_df)), - c("test_acyl_carnitines", "test_crea_gua")) - - expect_identical(colnames(combine_metab_info_zscores(test_metab_list_all, test_zscore_patients_df)$test_acyl_carnitines), - c("HMDB_name", "Sample", "Z_score")) - expect_identical((combine_metab_info_zscores(test_metab_list_all, - test_zscore_patients_df)$test_acyl_carnitines$Z_score), - c(0.31, 2.34, 2.45, 1.45, 2.14, -1.44, 12.18, -0.18, 3.22, -3.18)) - expect_identical(as.character(combine_metab_info_zscores(test_metab_list_all, - test_zscore_patients_df)$test_acyl_carnitines$Sample), - c("P2025M1", "P2025M1", "P2025M2", "P2025M2", "P2025M3", - "P2025M3", "P2025M4", "P2025M4", "P2025M5", "P2025M5")) - expect_equal(nchar(combine_metab_info_zscores(test_metab_list_all, - test_zscore_patients_df)$test_acyl_carnitines$HMDB_name[1]), - 45) - - expect_identical(colnames(combine_metab_info_zscores(test_metab_list_all, test_zscore_patients_df)$test_crea_gua), - c("HMDB_name", "Sample", "Z_score")) - expect_identical((combine_metab_info_zscores(test_metab_list_all, - test_zscore_patients_df)$test_crea_gua$Z_score), - c(0.84, -0.46, -0.15, -1.51, -0.78, 1.68, 0.84, 1.48, 0.47, 1.18)) - expect_identical(as.character(combine_metab_info_zscores(test_metab_list_all, - test_zscore_patients_df)$test_crea_gua$Sample), - c("P2025M1", "P2025M1", "P2025M2", "P2025M2", "P2025M3", - "P2025M3", "P2025M4", "P2025M4", "P2025M5", "P2025M5")) - expect_equal(nchar(combine_metab_info_zscores(test_metab_list_all, - test_zscore_patients_df)$test_crea_gua$HMDB_name[1]), - 45) + expect_type(merge_metabolite_info_zscores(test_metab_list_all, test_zscore_patients_df), "list") + expect_identical( + names(merge_metabolite_info_zscores(test_metab_list_all, test_zscore_patients_df)), + c("test_acyl_carnitines", "test_crea_gua") + ) + + expect_identical( + colnames(merge_metabolite_info_zscores(test_metab_list_all, test_zscore_patients_df)$test_acyl_carnitines), + c("HMDB_name", "Sample", "Z_score") + ) + expect_identical( + (merge_metabolite_info_zscores( + test_metab_list_all, + test_zscore_patients_df + )$test_acyl_carnitines$Z_score), + c(0.31, 2.34, 2.45, 1.45, 2.14, -1.44, 12.18, -0.18, 3.22, -3.18) + ) + expect_identical( + as.character(merge_metabolite_info_zscores( + test_metab_list_all, + test_zscore_patients_df + )$test_acyl_carnitines$Sample), + c( + "P2025M1_Zscore", "P2025M1_Zscore", "P2025M2_Zscore", "P2025M2_Zscore", "P2025M3_Zscore", + "P2025M3_Zscore", "P2025M4_Zscore", "P2025M4_Zscore", "P2025M5_Zscore", "P2025M5_Zscore" + ) + ) + expect_equal( + nchar(merge_metabolite_info_zscores( + test_metab_list_all, + test_zscore_patients_df + )$test_acyl_carnitines$HMDB_name[1]), + 45 + ) + + expect_identical( + colnames(merge_metabolite_info_zscores(test_metab_list_all, test_zscore_patients_df)$test_crea_gua), + c("HMDB_name", "Sample", "Z_score") + ) + expect_identical( + (merge_metabolite_info_zscores( + test_metab_list_all, + test_zscore_patients_df + )$test_crea_gua$Z_score), + c(0.84, -0.46, -0.15, -1.51, -0.78, 1.68, 0.84, 1.48, 0.47, 1.18) + ) + expect_identical( + as.character(merge_metabolite_info_zscores( + test_metab_list_all, + test_zscore_patients_df + )$test_crea_gua$Sample), + c( + "P2025M1_Zscore", "P2025M1_Zscore", "P2025M2_Zscore", "P2025M2_Zscore", "P2025M3_Zscore", + "P2025M3_Zscore", "P2025M4_Zscore", "P2025M4_Zscore", "P2025M5_Zscore", "P2025M5_Zscore" + ) + ) + expect_equal( + nchar(merge_metabolite_info_zscores( + test_metab_list_all, + test_zscore_patients_df + )$test_crea_gua$HMDB_name[1]), + 45 + ) }) -testthat::test_that("Combine patient and control data for each page of the violinplot pdf", { +testthat::test_that("get_data_per_metabolite_class: Combine patient and control data for each page of the violinplot pdf", { test_acyl_carnitines_pat <- read.delim(test_path("fixtures/", "test_acyl_carnitines_patients.txt")) test_acyl_carnitines_ctrl <- read.delim(test_path("fixtures/", "test_acyl_carnitines_controls.txt")) @@ -143,45 +203,83 @@ testthat::test_that("Combine patient and control data for each page of the violi test_nr_pat <- 5 test_nr_contr <- 5 - expect_type(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, - test_nr_plots_perpage, test_nr_pat, test_nr_contr), - "list") - expect_equal(length(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, - test_nr_plots_perpage, test_nr_pat, test_nr_contr)), - 4) - expect_identical(names(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, - test_nr_plots_perpage, test_nr_pat, test_nr_contr)), - c("test_acyl_carnitines_1", "test_acyl_carnitines_2", "test_crea_gua_1", "test_crea_gua_2")) - expect_identical(unique(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, - test_nr_plots_perpage, test_nr_pat, - test_nr_contr)$test_acyl_carnitines_1$HMDB_name), - c("metab1 ")) - expect_identical(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, - test_nr_plots_perpage, test_nr_pat, - test_nr_contr)$test_acyl_carnitines_1$Sample, - c("P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5", "C101.1", "C102.1", "C103.1", "C104.1", "C105.1")) + expect_type( + get_data_per_metabolite_class( + test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, test_nr_contr + ), + "list" + ) + expect_equal( + length(get_data_per_metabolite_class( + test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, test_nr_contr + )), + 4 + ) + expect_identical( + names(get_data_per_metabolite_class( + test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, test_nr_contr + )), + c("test_acyl_carnitines_1", "test_acyl_carnitines_2", "test_crea_gua_1", "test_crea_gua_2") + ) + expect_identical( + unique(get_data_per_metabolite_class( + test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, + test_nr_contr + )$test_acyl_carnitines_1$HMDB_name), + c("metab1 ") + ) + expect_identical( + get_data_per_metabolite_class( + test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, + test_nr_contr + )$test_acyl_carnitines_1$Sample, + c("P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5", "C101.1", "C102.1", "C103.1", "C104.1", "C105.1") + ) test_nr_plots_perpage <- 2 - expect_equal(length(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, - test_nr_plots_perpage, test_nr_pat, test_nr_contr)), - 2) - expect_identical(names(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, - test_nr_plots_perpage, test_nr_pat, test_nr_contr)), - c("test_acyl_carnitines_1", "test_crea_gua_1")) - expect_identical(unique(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, - test_nr_plots_perpage, test_nr_pat, - test_nr_contr)$test_acyl_carnitines_1$HMDB_name), - c("metab1 ", "metab3 ")) - expect_identical(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, - test_nr_plots_perpage, test_nr_pat, - test_nr_contr)$test_acyl_carnitines_1$Sample, - c("P2025M1", "P2025M1", "P2025M2", "P2025M2", "P2025M3", - "P2025M3", "P2025M4", "P2025M4", "P2025M5", "P2025M5", - "C101.1", "C101.1", "C102.1", "C102.1", "C103.1", "C103.1", "C104.1", "C104.1", "C105.1", "C105.1")) + expect_equal( + length(get_data_per_metabolite_class( + test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, test_nr_contr + )), + 2 + ) + expect_identical( + names(get_data_per_metabolite_class( + test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, test_nr_contr + )), + c("test_acyl_carnitines_1", "test_crea_gua_1") + ) + expect_identical( + unique(get_data_per_metabolite_class( + test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, + test_nr_contr + )$test_acyl_carnitines_1$HMDB_name), + c("metab1 ", "metab3 ") + ) + expect_identical( + get_data_per_metabolite_class( + test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, + test_nr_contr + )$test_acyl_carnitines_1$Sample, + c( + "P2025M1", "P2025M1", "P2025M2", "P2025M2", "P2025M3", + "P2025M3", "P2025M4", "P2025M4", "P2025M5", "P2025M5", + "C101.1", "C101.1", "C102.1", "C102.1", "C103.1", "C103.1", "C104.1", "C104.1", "C105.1", "C105.1" + ) + ) }) -testthat::test_that("Generate a dataframe with information for Helix", { +testthat::test_that("prepare_helix_patient_data: Generate a dataframe with information for Helix", { test_acyl_carnitines_pat <- read.delim(test_path("fixtures/", "test_acyl_carnitines_patients.txt")) test_crea_gua_pat <- read.delim(test_path("fixtures/", "test_crea_gua_patients.txt")) @@ -194,25 +292,33 @@ testthat::test_that("Generate a dataframe with information for Helix", { test_metab_list_all <- list(test_acyl_carnitines_df, test_crea_gua_df) names(test_metab_list_all) <- c("test_acyl_carnitines", "test_crea_gua") - expect_identical(colnames(get_patient_data_to_helix(test_metab_interest_sorted, test_metab_list_all)), - c("HMDB_name", "Sample", "Z_score", "Helix_naam", "high_zscore", "low_zscore")) - expect_equal(dim(get_patient_data_to_helix(test_metab_interest_sorted, test_metab_list_all)), - c(15, 6)) - expect_identical(unique(get_patient_data_to_helix(test_metab_interest_sorted, test_metab_list_all)$HMDB_name), - c("metab1", "metab4", "metab11")) - expect_false("ratio1" %in% get_patient_data_to_helix(test_metab_interest_sorted, test_metab_list_all)$HMDB_name) - expect_equal(get_patient_data_to_helix(test_metab_interest_sorted, test_metab_list_all)$Z_score, - c(0.31, 2.45, 2.14, 12.18, 3.22, 0.84, -0.46, -0.15, -1.51, -0.78, 1.68, 0.84, 1.48, 0.47, 1.18)) + expect_identical( + colnames(prepare_helix_patient_data(test_metab_interest_sorted, test_metab_list_all)), + c("HMDB_name", "Sample", "Z_score", "Helix_naam", "high_zscore", "low_zscore") + ) + expect_equal( + dim(prepare_helix_patient_data(test_metab_interest_sorted, test_metab_list_all)), + c(15, 6) + ) + expect_identical( + unique(prepare_helix_patient_data(test_metab_interest_sorted, test_metab_list_all)$HMDB_name), + c("metab1", "metab4", "metab11") + ) + expect_false("ratio1" %in% prepare_helix_patient_data(test_metab_interest_sorted, test_metab_list_all)$HMDB_name) + expect_equal( + prepare_helix_patient_data(test_metab_interest_sorted, test_metab_list_all)$Z_score, + c(0.31, 2.45, 2.14, 12.18, 3.22, 0.84, -0.46, -0.15, -1.51, -0.78, 1.68, 0.84, 1.48, 0.47, 1.18) + ) }) -testthat::test_that("Check for diagnostic patients", { +testthat::test_that("is_diagnostic_patients: Check for diagnostic patients", { test_patient_column <- c("P2025M1", "P2025M2", "P2025M3", "P2025M4", "C101.1", "C102.1", "P2025D1", "P225M1") - expect_equal(is_diagnostic_patient(test_patient_column), c(TRUE, TRUE, TRUE, TRUE, FALSE, FALSE, FALSE, FALSE)) - expect_equal(length(is_diagnostic_patient(test_patient_column)), 8) + expect_equal(is_diagnostic_patients(test_patient_column), c(TRUE, TRUE, TRUE, TRUE, FALSE, FALSE, FALSE, FALSE)) + expect_equal(length(is_diagnostic_patients(test_patient_column)), 8) }) -testthat::test_that("Adding labnummer and Onderzoeksnummer to the Helix dataframe", { +testthat::test_that("add_lab_id_and_onderzoeksnr:Adding labnummer and Onderzoeksnummer to the Helix dataframe", { test_df_metabs_helix <- read.delim(test_path("fixtures/", "test_df_metabs_helix.txt")) test_df_metabs_helix <- test_df_metabs_helix %>% @@ -222,83 +328,163 @@ testthat::test_that("Adding labnummer and Onderzoeksnummer to the Helix datafram expect_true("labnummer" %in% colnames(add_lab_id_and_onderzoeksnr(test_df_metabs_helix))) expect_true("Onderzoeksnummer" %in% colnames(add_lab_id_and_onderzoeksnr(test_df_metabs_helix))) - expect_identical(unique(add_lab_id_and_onderzoeksnr(test_df_metabs_helix)$labnummer), - c("2025M1", "2025M2", "2025M3", "2025M4", "2025M5")) - expect_identical(unique(add_lab_id_and_onderzoeksnr(test_df_metabs_helix)$Onderzoeksnummer), - c("MB2025/1", "MB2025/2", "MB2025/3", "MB2025/4", "MB2025/5")) + expect_identical( + unique(add_lab_id_and_onderzoeksnr(test_df_metabs_helix)$labnummer), + c("2025M1", "2025M2", "2025M3", "2025M4", "2025M5") + ) + expect_identical( + unique(add_lab_id_and_onderzoeksnr(test_df_metabs_helix)$Onderzoeksnummer), + c("MB2025/1", "MB2025/2", "MB2025/3", "MB2025/4", "MB2025/5") + ) }) -testthat::test_that("Make the output for Helix", { +testthat::test_that("transform_metab_df_to_helix_df: Make the output for Helix", { test_protocol_name <- "test_protocol_name" test_df_metabs_helix <- read.delim(test_path("fixtures/", "test_df_metabs_helix.txt")) - expect_equal(dim(output_for_helix(test_protocol_name, test_df_metabs_helix)), - c(15, 6)) - expect_identical(colnames(output_for_helix(test_protocol_name, test_df_metabs_helix)), - c("Vial", "labnummer", "Onderzoeksnummer", "Protocol", "Name", "Amount")) - expect_identical(unique(output_for_helix(test_protocol_name, test_df_metabs_helix)$Protocol), - "test_protocol_name") - expect_identical(unique(output_for_helix(test_protocol_name, test_df_metabs_helix)$labnummer), - c("2025M1", "2025M2", "2025M3", "2025M4", "2025M5")) - expect_identical(unique(output_for_helix(test_protocol_name, test_df_metabs_helix)$Onderzoeksnummer), - c("MB2025/1", "MB2025/2", "MB2025/3", "MB2025/4", "MB2025/5")) - expect_equal(output_for_helix(test_protocol_name, test_df_metabs_helix)$Amount, - c(0.31, 2.45, 2.14, 12.18, 3.22, 0.84, -0.46, -0.15, -1.51, -0.78, 1.68, 0.84, 1.48, 0.47, 1.18)) + expect_equal( + dim(transform_metab_df_to_helix_df(test_protocol_name, test_df_metabs_helix)), + c(15, 6) + ) + expect_identical( + colnames(transform_metab_df_to_helix_df(test_protocol_name, test_df_metabs_helix)), + c("Vial", "labnummer", "Onderzoeksnummer", "Protocol", "Name", "Amount") + ) + expect_identical( + unique(transform_metab_df_to_helix_df(test_protocol_name, test_df_metabs_helix)$Protocol), + "test_protocol_name" + ) + expect_identical( + unique(transform_metab_df_to_helix_df(test_protocol_name, test_df_metabs_helix)$labnummer), + c("2025M1", "2025M2", "2025M3", "2025M4", "2025M5") + ) + expect_identical( + unique(transform_metab_df_to_helix_df(test_protocol_name, test_df_metabs_helix)$Onderzoeksnummer), + c("MB2025/1", "MB2025/2", "MB2025/3", "MB2025/4", "MB2025/5") + ) + expect_equal( + transform_metab_df_to_helix_df(test_protocol_name, test_df_metabs_helix)$Amount, + c(0.31, 2.45, 2.14, 12.18, 3.22, 0.84, -0.46, -0.15, -1.51, -0.78, 1.68, 0.84, 1.48, 0.47, 1.18) + ) }) -testthat::test_that("Create a dataframe with all metabolites that exceed the min and max Z-score cutoff", { +testthat::test_that("get_top_metabolites_df: Create a dataframe with all metabolites that exceed the min and max Z-score cutoff", { test_df_metabs_helix <- read.delim(test_path("fixtures/", "test_df_metabs_helix.txt")) test_patient_id <- "P2025M1" - expect_equal(dim(prepare_alarmvalues(test_patient_id, test_df_metabs_helix)), - c(2, 2)) - expect_equal(colnames(prepare_alarmvalues(test_patient_id, test_df_metabs_helix)), - c("Metabolite", "Z-score")) + expect_equal( + dim(get_top_metabolites_df(test_patient_id, test_df_metabs_helix)), + c(2, 2) + ) + expect_equal( + colnames(get_top_metabolites_df(test_patient_id, test_df_metabs_helix)), + c("Metabolite", "Z-score") + ) test_patient_id <- "P2025M2" - expect_equal(dim(prepare_alarmvalues(test_patient_id, test_df_metabs_helix)), - c(4, 2)) - expect_equal(prepare_alarmvalues(test_patient_id, test_df_metabs_helix)$`Z-score`, - c("", "2.45", "", "-1.51")) + expect_equal( + dim(get_top_metabolites_df(test_patient_id, test_df_metabs_helix)), + c(4, 2) + ) + expect_equal( + get_top_metabolites_df(test_patient_id, test_df_metabs_helix)$`Z-score`, + c("", "2.45", "", "-1.51") + ) test_patient_id <- "P2025M4" - expect_equal(dim(prepare_alarmvalues(test_patient_id, test_df_metabs_helix)), - c(3, 2)) - expect_equal(prepare_alarmvalues(test_patient_id, test_df_metabs_helix)$`Z-score`, - c("", "12.18", "")) + expect_equal( + dim(get_top_metabolites_df(test_patient_id, test_df_metabs_helix)), + c(3, 2) + ) + expect_equal( + get_top_metabolites_df(test_patient_id, test_df_metabs_helix)$`Z-score`, + c("", "12.18", "") + ) }) -testthat::test_that("Create a dataframe with the top 20 highest and top 10 lowest metabolites", { +testthat::test_that("prepare_toplist: Create a dataframe with the top 20 highest and top 10 lowest metabolites", { test_zscore_patient_df <- read.delim(test_path("fixtures/", "test_zscore_patient_df.txt")) test_patient_id <- "P2025M1" - - expect_equal(dim(prepare_toplist(test_patient_id, test_zscore_patient_df)), - c(32, 3)) - expect_equal(colnames(prepare_toplist(test_patient_id, test_zscore_patient_df)), - c("HMDB_ID", "Metabolite", "Z-score")) - expect_equal(prepare_toplist(test_patient_id, test_zscore_patient_df)$HMDB_ID, - c("Increased", "HMDB030", "HMDB029", "HMDB028", "HMDB027", "HMDB026", "HMDB025", "HMDB024", "HMDB023", - "HMDB022", "HMDB021", "HMDB020", "HMDB019", "HMDB018", "HMDB017", "HMDB016", "HMDB015", "HMDB014", "HMDB013", - "HMDB012", "HMDB011", "Decreased", "HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB005", "HMDB006", - "HMDB007", "HMDB008", "HMDB009", "HMDB010")) - expect_equal(prepare_toplist(test_patient_id, test_zscore_patient_df)$`Z-score`, - c("", "30", "29", "28", "27", "26", "25", "24", "23", "22", "21", "20", "19", "18", "17", "16", "15", - "14", "13", "12", "11", "", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10")) + test_num_of_highest_metabolites <- 20 + test_num_of_lowest_metabolites <- 10 + + expect_equal( + dim(prepare_toplist( + test_patient_id, + test_zscore_patient_df, + test_num_of_highest_metabolites, + test_num_of_lowest_metabolites + )), + c(32, 3) + ) + expect_equal( + colnames(prepare_toplist( + test_patient_id, + test_zscore_patient_df, + test_num_of_highest_metabolites, + test_num_of_lowest_metabolites + )), + c("HMDB_ID", "Metabolite", "Z-score") + ) + expect_equal( + prepare_toplist( + test_patient_id, + test_zscore_patient_df, + test_num_of_highest_metabolites, + test_num_of_lowest_metabolites + )$HMDB_ID, + c( + "Increased", "HMDB030", "HMDB029", "HMDB028", "HMDB027", "HMDB026", "HMDB025", "HMDB024", "HMDB023", + "HMDB022", "HMDB021", "HMDB020", "HMDB019", "HMDB018", "HMDB017", "HMDB016", "HMDB015", "HMDB014", "HMDB013", + "HMDB012", "HMDB011", "Decreased", "HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB005", "HMDB006", + "HMDB007", "HMDB008", "HMDB009", "HMDB010" + ) + ) + expect_equal( + prepare_toplist( + test_patient_id, + test_zscore_patient_df, + test_num_of_highest_metabolites, + test_num_of_lowest_metabolites + )$`Z-score`, + c( + "", "30", "29", "28", "27", "26", "25", "24", "23", "22", "21", "20", "19", "18", "17", "16", "15", + "14", "13", "12", "11", "", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" + ) + ) test_patient_id <- "P2025M2" - expect_equal(prepare_toplist(test_patient_id, test_zscore_patient_df)$Metabolite, - c("", "metab1", "metab2", "metab3", "metab4", "metab5", "metab6", "metab7", "metab8", "metab9", "metab10", - "metab11", "metab12", "metab13", "metab14", "metab15", "metab16", "metab17", "metab18", "metab19", "metab20", - "", "metab30", "metab29", "metab28", "metab27", "metab26", "metab25", "metab24", "metab23", "metab22", - "metab21")) - expect_equal(prepare_toplist(test_patient_id, test_zscore_patient_df)$`Z-score`, - c("", "-1", "-2", "-3", "-4", "-5", "-6", "-7", "-8", "-9", "-10", "-11", "-12", "-13", "-14", "-15", - "-16", "-17", "-18", "-19", "-20", "", "-30", "-29", "-28", "-27", "-26", "-25", "-24", "-23", "-22", "-21")) + expect_equal( + prepare_toplist( + test_patient_id, + test_zscore_patient_df, + test_num_of_highest_metabolites, + test_num_of_lowest_metabolites + )$Metabolite, + c( + "", "metab1", "metab2", "metab3", "metab4", "metab5", "metab6", "metab7", "metab8", "metab9", "metab10", + "metab11", "metab12", "metab13", "metab14", "metab15", "metab16", "metab17", "metab18", "metab19", "metab20", + "", "metab30", "metab29", "metab28", "metab27", "metab26", "metab25", "metab24", "metab23", "metab22", + "metab21" + ) + ) + expect_equal( + prepare_toplist( + test_patient_id, + test_zscore_patient_df, + test_num_of_highest_metabolites, + test_num_of_lowest_metabolites + )$`Z-score`, + c( + "", "-1", "-2", "-3", "-4", "-5", "-6", "-7", "-8", "-9", "-10", "-11", "-12", "-13", "-14", "-15", + "-16", "-17", "-18", "-19", "-20", "", "-30", "-29", "-28", "-27", "-26", "-25", "-24", "-23", "-22", "-21" + ) + ) }) -testthat::test_that("Create a pdf with a table of top metabolites and violin plots", { +testthat::test_that("create_pdf_violin_plots: Create a pdf with a table of top metabolites and violin plots", { local_edition(3) temp_dir <- "./" dir.create(paste0(temp_dir, "violin_plots/")) @@ -320,8 +506,13 @@ testthat::test_that("Create a pdf with a table of top metabolites and violin plo `Z-score` = c("", "2.45", "", "-1.51") ) - expect_silent(create_pdf_violin_plots(test_pdf_dir, test_patient_id, - test_metab_perpage, test_top_metab_pt, test_explanation)) + expect_silent(create_pdf_violin_plots( + test_pdf_dir, + test_patient_id, + test_metab_perpage, + test_top_metab_pt, + test_explanation + )) out_pdf_violinplots <- file.path(test_pdf_dir, "R_P2025M1.pdf") expect_true(file.exists(out_pdf_violinplots)) @@ -331,7 +522,7 @@ testthat::test_that("Create a pdf with a table of top metabolites and violin plo unlink(test_pdf_dir, recursive = TRUE) }) -testthat::test_that("Create a violin plot", { +testthat::test_that("create_violin_plot: Create a violin plot", { test_patient_id <- "P2025M1" test_sub_perpage <- "test acyl carnitines" @@ -344,58 +535,391 @@ testthat::test_that("Create a violin plot", { expect_silent(create_violin_plot(test_metab_zscores_df, test_patient_zscore_df, test_sub_perpage, test_patient_id)) - expect_doppelganger("violin_plot_P2025M1", create_violin_plot(test_metab_zscores_df, test_patient_zscore_df, - test_sub_perpage, test_patient_id)) + expect_doppelganger("violin_plot_P2025M1", create_violin_plot( + test_metab_zscores_df, test_patient_zscore_df, + test_sub_perpage, test_patient_id + )) }) -testthat::test_that("Run dIEM algorithm", { +testthat::test_that("run_diem_algorithm :Run dIEM algorithm", { test_expected_biomarkers_df <- read.delim(test_path("fixtures/", "test_expected_biomarkers_df.txt")) test_zscore_patient_df <- read.delim(test_path("fixtures/", "test_zscore_patient_df.txt")) test_sample_cols <- c("P2025M1", "P2025M2", "P2025M3", "P2025M4") - expect_equal(dim(run_diem_algorithm(test_expected_biomarkers_df, test_zscore_patient_df, test_sample_cols)), - c(7, 5)) - expect_identical(colnames(run_diem_algorithm(test_expected_biomarkers_df, test_zscore_patient_df, test_sample_cols)), - c("Disease", "P2025M1", "P2025M2", "P2025M3", "P2025M4")) - expect_identical(run_diem_algorithm(test_expected_biomarkers_df, test_zscore_patient_df, test_sample_cols)$Disease, - c("Disease A", "Disease B", "Disease C", "Disease D", "Disease E", "Disease F", "Disease G")) + expect_equal( + dim(run_diem_algorithm(test_expected_biomarkers_df, test_zscore_patient_df, test_sample_cols)), + c(7, 5) + ) + expect_identical( + colnames(run_diem_algorithm(test_expected_biomarkers_df, test_zscore_patient_df, test_sample_cols)), + c("Disease", "P2025M1", "P2025M2", "P2025M3", "P2025M4") + ) + expect_identical( + run_diem_algorithm(test_expected_biomarkers_df, test_zscore_patient_df, test_sample_cols)$Disease, + c("Disease A", "Disease B", "Disease C", "Disease D", "Disease E", "Disease F", "Disease G") + ) expect_equal(run_diem_algorithm(test_expected_biomarkers_df, test_zscore_patient_df, test_sample_cols)$P2025M1, - c(10.94172, 0.95343, 12.12121, 0.00000, 44.28850, 0.00000, -38.70370), tolerance = 0.0001) + c(10.94172, 0.95343, 12.12121, 0.00000, 44.28850, 0.00000, -38.70370), + tolerance = 0.0001 + ) }) -testthat::test_that("Ranking Z-scores for a patient", { +testthat::test_that("rank_patient_zscores: Ranking Z-scores for a patient", { test_zscore_col <- c(1, 5, 6, 2, 7, -2, 3) expect_equal(length(rank_patient_zscores(test_zscore_col)), 7) - expect_identical(rank_patient_zscores(test_zscore_col), - c(6, 3, 2, 5, 1, 1, 4)) + expect_identical( + rank_patient_zscores(test_zscore_col), + c(6, 3, 2, 5, 1, 1, 4) + ) test_zscore_col <- c(3, 2, 1, 3) - expect_identical(rank_patient_zscores(test_zscore_col), - c(1, 2, 3, 1)) + expect_identical( + rank_patient_zscores(test_zscore_col), + c(1, 2, 3, 1) + ) test_zscore_col <- c(-1, -2, -3, -4) - expect_identical(rank_patient_zscores(test_zscore_col), - c(4, 3, 2, 1)) + expect_identical( + rank_patient_zscores(test_zscore_col), + c(4, 3, 2, 1) + ) }) -testthat::test_that("Saving the probability score dataframe as an Excel file", { +testthat::test_that("save_prob_scores_to_excel: Saving the probability score dataframe as an Excel file", { local_edition(3) test_probability_score_df <- read.delim(test_path("fixtures/", "test_probability_score_df.txt")) - test_output_dir <- "./test_excel" - dir.create(test_output_dir) test_run_name <- "test_run" - out_excel_file <- file.path(test_output_dir, paste0("/dIEM_algoritme_output_", test_run_name, ".xlsx")) + out_excel_file <- file.path(paste0("dIEM_algoritme_output_", test_run_name, ".xlsx")) - expect_silent(save_prob_scores_to_excel(test_probability_score_df, test_output_dir, test_run_name)) + expect_silent(save_prob_scores_to_excel(test_probability_score_df, test_run_name)) expect_true(file.exists(out_excel_file)) content_excel_file <- openxlsx::read.xlsx(out_excel_file, sheet = 1) expect_snapshot_output(content_excel_file) - unlink(test_output_dir, recursive = TRUE) + file.remove(out_excel_file, recursive = TRUE) +}) + +testthat::test_that("prepare_intensities_zscore_df: Preparing the intensities and Z-score dataframe", { + test_intensities_zscore_df <- read.delim(test_path("fixtures/", "test_outlist.txt")) + test_intensities_zscore_df$nr_ctrls <- c(28, 27, 30, 25) + test_intensities_zscore_df$avg_ctrls <- c(16129.17, 1150, 1231.25, 4015.833) + test_intensities_zscore_df$sd_ctrls <- c(51606.27, 349.6752, 313.7249, 6152.479) + + prepare_intensities_zscore_df(test_intensities_zscore_df) + + expect_equal( + colnames(prepare_intensities_zscore_df(test_intensities_zscore_df)), + c( + "HMDB_code", "HMDB_name", "C101.1", "C102.1", "C103.1", "C104.1", "C105.1", "C106.1", "C107.1", "C108.1", + "C109.1", "C110.1", "C111.1", "C112.1", "P2.1", "P3.1", "mean_controls", "sd_controls" + ) + ) + + expect_equal( + unname(sapply(prepare_intensities_zscore_df(test_intensities_zscore_df), class)), + c( + "character", "character", "numeric", "numeric", "numeric", "numeric", "numeric", "numeric", "numeric", + "numeric", "numeric", "numeric", "numeric", "numeric", "numeric", "numeric", "numeric", "numeric" + ) + ) +}) + +testthat::test_that("get_colnames_samples: Get all column names containing a specific prefix", { + test_colnames <- c( + "HMDB_name", "HMDB_code", "P1001", "P1001_Zscore", "P1002", + "P1003", "P1004", "C101_Zscore", "C102", "C103" + ) + test_intensities_zscore_df <- read.delim(test_path("fixtures/", "test_intensities_zscore_df.txt")) + + expect_equal(get_colnames_samples(test_intensities_zscore_df, "P"), c("P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5")) + expect_equal(get_colnames_samples(test_intensities_zscore_df, "C"), c("C101.1", "C102.1", "C103.1", "C104.1", "C105.1")) +}) + +testthat::test_that("add_zscores_ratios_to_df: Add Zscores for multiple ratios to the dataframe", { + test_outlist_df <- read.delim(test_path("fixtures/GenerateViolinPlots", "test_outlist_df.txt")) + + test_metabolites_ratios_df <- data.frame( + HMDB.code = c("HMDB000TT1", "HMDB000TT2", "HMDB000TT3"), + Ratio_name = c("Test_ratio1", "Test_ratio2", "Test_ratio3"), + HMDB_numerator = c("HMDB001", "HMDB003plusHMDB003", "HMDB001plusHMDB003plusHMDB004"), + HMDB_denominator = c("HMDB002", "HMDB004", "one") + ) + + test_all_sample_ids <- c( + "C101.1", "C102.1", "C103.1", "C104.1", "C105.1", "P2025M1", "P2025M2", "P2025M3", + "P2025M4", "P2025M5" + ) + + expect_equal( + add_zscores_ratios_to_df(test_outlist_df, test_metabolites_ratios_df, test_all_sample_ids)$HMDB_code, + c("HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB011", "HMDB012", "HMDB000TT1", "HMDB000TT2", "HMDB000TT3") + ) + expect_equal( + add_zscores_ratios_to_df( + test_outlist_df, + test_metabolites_ratios_df, + test_all_sample_ids + )$C101.1, + c(1000, 1200, 1300, 1400, 1500, 1600, -0.2630344, -0.1069152, 11.8533096) + ) + expect_equal( + add_zscores_ratios_to_df( + test_outlist_df, + test_metabolites_ratios_df, + test_all_sample_ids + )$C101.1_Zscore, + c(0.45, 1.67, -1.86, 0.58, 2.47, -0.56, -0.5899371, 0.4858991, -0.4552026), + tolerance = 0.0001 + ) +}) + +testthat::test_that("calculate_zscore_ratios: Calculate Zscores for ratios", { + test_outlist_df <- read.delim(test_path("fixtures/GenerateViolinPlots", "test_outlist_df.txt")) + test_intensities_zscores_df <- prepare_intensities_zscore_df(test_outlist_df) + + test_metabolites_ratios_df <- data.frame( + HMDB.code = c("HMDB000TT1", "HMDB000TT2", "HMDB000TT3"), + Ratio_name = c("Test_ratio1", "Test_ratio2", "Test_ratio3"), + HMDB_numerator = c("HMDB001", "HMDB003plusHMDB003", "HMDB001plusHMDB003plusHMDB004"), + HMDB_denominator = c("HMDB002", "HMDB004", "one") + ) + + test_all_sample_ids <- c( + "C101.1", "C102.1", "C103.1", "C104.1", "C105.1", "P2025M1", "P2025M2", "P2025M3", + "P2025M4", "P2025M5" + ) + + expect_equal( + calculate_zscore_ratios(test_metabolites_ratios_df, test_outlist_df, test_all_sample_ids)$HMDB_code, + c("HMDB000TT1", "HMDB000TT2", "HMDB000TT3") + ) + expect_equal( + calculate_zscore_ratios(test_metabolites_ratios_df, test_outlist_df, test_all_sample_ids)$C101.1, + c(-0.2630344, -0.1069152, 11.8533096) + ) + expect_equal( + calculate_zscore_ratios(test_metabolites_ratios_df, test_outlist_df, test_all_sample_ids)$C101.1_Zscore, + c(-0.5899371, 0.4858991, -0.4552026), + tolerance = 0.0001 + ) +}) + +testthat::test_that("get_list_page_plot_data: Get a list of dataframes for each chunk", { + test_metabolite_class_patients_df <- read.delim(test_path("fixtures/GenerateViolinPlots", + "test_metabolite_class_patients_df.txt")) + test_metabolite_class_controls_df <- read.delim(test_path("fixtures/GenerateViolinPlots", + "test_metabolite_class_controls_df.txt")) + test_nr_plots_perpage <- 2 + test_metabolite_in_chunks <- list( + c("HMDB001", "HMDB002", "HMDB003"), + c("HMDB004", "HMDB011", "HMDB012"), + c("HMDB000TT1", "HMDB000TT2", "HMDB000TT3") + ) + + t <- get_list_page_plot_data( + test_metabolite_in_chunks, + test_metabolite_class_patients_df, + test_metabolite_class_controls_df, + test_nr_plots_perpage + ) + + expect_type(get_list_page_plot_data( + test_metabolite_in_chunks, + test_metabolite_class_patients_df, + test_metabolite_class_controls_df, + test_nr_plots_perpage + ), "list") + + expect_equal(length(get_list_page_plot_data( + test_metabolite_in_chunks, + test_metabolite_class_patients_df, + test_metabolite_class_controls_df, + test_nr_plots_perpage + )), 3) + + test_plot_data_list <- get_list_page_plot_data( + test_metabolite_in_chunks, + test_metabolite_class_patients_df, + test_metabolite_class_controls_df, + test_nr_plots_perpage + ) + for (num_chunk in seq_along(test_metabolite_in_chunks)) { + expect_equal(unique(test_plot_data_list[[num_chunk]]$HMDB_name), test_metabolite_in_chunks[[num_chunk]]) + expect_equal(unique(test_plot_data_list[[num_chunk]]$Sample), + c("P2025M1", "P2025M2", "P2025M3", "C101.1", "C102.1", "C103.1")) + } + +}) + +testthat::test_that("make_and_save_violin_plot_pdfs: Make and save violin plots for each patient in a PDF", { + test_zscore_patients_df <- read.delim(test_path("fixtures/GenerateViolinPlots", "test_zscore_patients_df.txt")) + test_zscore_controls_df <- read.delim(test_path("fixtures/GenerateViolinPlots", "test_zscore_controls_df.txt")) + test_path_metabolite_groups <- test_path("fixtures/test_metabolite_groups") + test_nr_plots_perpage <- 2 + test_number_of_samples <- list( + controls = 5, + patients = 5 + ) + test_run_name <- "unit_test" + test_protocol_name <- "UNIT_TEST_PROTOCOL" + test_explanation_violin_plot <- "Unit test violin plot pdfs" + test_number_of_metabolites <- list( + highest = 2, + lowest = 1 + ) + + expect_silent(make_and_save_violin_plot_pdfs( + test_zscore_patients_df, + test_zscore_controls_df, + test_path_metabolite_groups, + test_nr_plots_perpage, + test_number_of_samples, + test_run_name, + test_protocol_name, + test_explanation_violin_plot, + test_number_of_metabolites + )) + + patient_ids <- c("P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5") + for (patient_id in patient_ids) { + pdf_file_name_diagnotics <- file.path(paste0("Diagnostics/MB", gsub("^P|M", "", patient_id), "_DIMS_PL_DIAG.pdf")) + pdf_file_name_other <- file.path(paste0("Other/R_", patient_id, ".pdf")) + + expect_true(file.exists(pdf_file_name_diagnotics)) + expect_true(file.exists(pdf_file_name_other)) + } + expect_true(file.exists("output_Helix_unit_test.csv")) + + unlink(c("Diagnostics", 'Other'), recursive = TRUE) + file.remove("output_Helix_unit_test.csv") +}) + +testthat::test_that("get_probabilities_top_iems: Get the IEM probabilities for a patient for all diseases", { + test_patient_top_iems_probs <- data.frame( + Disease = c("Disease A", "Disease B", "Disease C", "Disease D"), + P2025M1 = c(100, 75, 50, 25) + ) + test_expected_biomarkers_df <- read.delim(test_path("fixtures", "test_expected_biomarkers_df.txt")) + test_patient_id <- "P2025M1" + + + expect_type(get_probabilities_top_iems(test_patient_top_iems_probs, test_expected_biomarkers_df, test_patient_id), + "list") + + expect_equal(length(get_probabilities_top_iems(test_patient_top_iems_probs, test_expected_biomarkers_df, test_patient_id)), + 4) + expect_equal(names(get_probabilities_top_iems(test_patient_top_iems_probs, test_expected_biomarkers_df, test_patient_id)), + c("Disease A, probability score 100", "Disease B, probability score 75", "Disease C, probability score 50", + "Disease D, probability score 25")) + + expect_equal(get_probabilities_top_iems(test_patient_top_iems_probs, + test_expected_biomarkers_df, + test_patient_id)$"Disease A, probability score 100", + data.frame( + HMDB_code = c("HMDB002", "HMDB012"), + HMDB_name = c("metab2", "metab12") + )) + +}) + +testthat::test_that("make_and_save_diem_plots: Make and save dIEM plots", { + test_diem_probability_score <- data.frame( + Disease = c("Disease A", "Disease B", "Disease C", "Disease D", "Disease E"), + P2025M1 = c(100, 75, 50, 25, 12.5), + P2025M2 = c(25, 0, 2, 8, 3), + P2025M3 = c(0, 1, 2, 3, 4) + ) + test_patient_ids <- c("P2025M1", "P2025M2", "P2025M3") + test_expected_biomarkers_df <- read.delim(test_path("fixtures", "test_expected_biomarkers_df.txt")) + test_zscore_patients_df <- read.delim(test_path("fixtures/GenerateViolinPlots", "test_zscore_patients_df.txt")) + test_zscore_controls_df <- read.delim(test_path("fixtures/GenerateViolinPlots", "test_zscore_controls_df.txt")) + test_nr_plots_perpage <- 2 + test_number_of_samples <- list( + controls = 5, + patients = 5 + ) + test_number_of_metabolites <- list( + highest = 2, + lowest = 1 + ) + test_iem_variables <- list( + top_number_iem_diseases = 5, + threshold_iem = 5 + ) + + expect_silent(make_and_save_diem_plots( + test_diem_probability_score, + test_patient_ids, + test_expected_biomarkers_df, + test_zscore_patients_df, + test_zscore_controls_df, + test_nr_plots_perpage, + test_number_of_samples, + test_number_of_metabolites, + test_iem_variables + )) + + expect_equal(make_and_save_diem_plots( + test_diem_probability_score, + test_patient_ids, + test_expected_biomarkers_df, + test_zscore_patients_df, + test_zscore_controls_df, + test_nr_plots_perpage, + test_number_of_samples, + test_number_of_metabolites, + test_iem_variables + ), "P2025M3") + + expect_true(file.exists("dIEM_plots/IEM_P2025M1.pdf")) + expect_true(file.exists("dIEM_plots/IEM_P2025M2.pdf")) + + unlink("dIEM_plots/", recursive = TRUE) +}) + +testthat::test_that("make_metabolite_order: Make the order of metabolites for the violin plots", { + test_metabolites_vector <- c("metab1", "metab2", "metab3") + test_num_plots_per_page <- 5 + + make_metabolite_order(test_num_plots_per_page, test_metabolites_vector) + + expect_equal(length(make_metabolite_order(test_num_plots_per_page, test_metabolites_vector)), 5) + expect_equal(make_metabolite_order(test_num_plots_per_page, test_metabolites_vector), + c("metab1", "metab2", "metab3", " ", " ")) +}) + +testthat::test_that("pad_truncate_hmdb_names: Pad or truncate HMDB names to a fixed width", { + test_dataframe <- data.frame( + HMDB_name = c("metab1", "metabolite2", "metabo3") + ) + test_width <- 10 + test_pad_character <- "+" + + expect_equal(nrow(pad_truncate_hmdb_names(test_dataframe, test_width, test_pad_character)), + 3) + expect_equal(pad_truncate_hmdb_names(test_dataframe, test_width, test_pad_character)$HMDB_name, + c("metab1++++", "metabol...", "metabo3+++")) + + test_df <- pad_truncate_hmdb_names(test_dataframe, test_width, test_pad_character) + for (row in seq(nrow(test_df))) { + expect_equal(nchar(test_df[row, "HMDB_name"]), 10) + } +}) + +testthat::test_that("save_patient_no_iem: Save a list of patient IDs to a text file", { + local_edition(3) + test_threshold_iem <- 5 + test_patient_no_iem <- c("Patient1", "Patient2") + + expect_silent(save_patient_no_iem(test_threshold_iem, test_patient_no_iem)) + + expect_true(file.exists("missing_probability_scores.txt")) + + expect_snapshot(save_patient_no_iem(test_threshold_iem, test_patient_no_iem)) }) From 19ed90786a17845c1ddf189c95a109211b758c66 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 26 Feb 2026 15:56:57 +0100 Subject: [PATCH 21/30] Changed and added new test dataframes --- .../make_test_data_GenerateViolinPlots.R | 108 ++++++++++++++++++ .../testthat/fixtures/make_test_outlist_df.R | 2 +- 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/DIMS/tests/testthat/fixtures/make_test_data_GenerateViolinPlots.R b/DIMS/tests/testthat/fixtures/make_test_data_GenerateViolinPlots.R index 0664e00b..e435fa35 100644 --- a/DIMS/tests/testthat/fixtures/make_test_data_GenerateViolinPlots.R +++ b/DIMS/tests/testthat/fixtures/make_test_data_GenerateViolinPlots.R @@ -1,5 +1,52 @@ ### Functions used to create mock dataframes used for unit testing of GenerateViolinPlots ### +make_outlist_df <- function() { + test_outlist_df <- data.frame( + plots = NA, + C101.1 = c(1000, 1200, 1300, 1400, 1500, 1600), + C102.1 = c(1100, 1700, 925, 1125, 1200, 1050), + C103.1 = c(1300, 750, 1000, 1220, 1100, 1200), + C104.1 = c(1650, 925, 1600, 1650, 1025, 1150), + C105.1 = c(180000, 1950, 750, 15050, 1100, 1300), + P2025M1 = c(1000, 1200, 1300, 1400, 1100, 975), + P2025M2 = c(1100, 1700, 925, 1125, 1050, 1175), + P2025M3 = c(1300, 750, 1000, 1220, 975, 1100), + P2025M4 = c(1650, 925, 1600, 1650, 1700, 1750), + P2025M5 = c(180000, 1950, 750, 15050, 10000, 1500), + HMDB_name = c("metab1", "metab2", "metab3", "metab4", "metab5", "metab6"), + HMDB_name_all = NA, + HMDB_ID_all = NA, + sec_HMDB_ID = NA, + HMDB_key = NA, + sec_HMDB_ID_rlvnc = NA, + name = NA, + relevance = NA, + descr = NA, + origin = NA, + fluids = NA, + tissue = NA, + disease = NA, + pathway = NA, + HMDB_code = c("HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB011", "HMDB012"), + avg_ctrls = c(37010, 1305, 1115, 4089, 1185, 1260), + sd_ctrls = c(79934.23, 508.80, 336.15, 6130.65, 186.75, 210.36), + nr_ctrls = c(25, 26, 27, 28, 29, 30), + C101.1_Zscore = c(0.45, 1.67, -1.86, 0.58, 2.47, -0.56), + C102.1_Zscore = c(2.89, 0.79, -1.88, 5.46, -0.68, 1.65), + C103.1_Zscore = c(0.54, -0.85, 1.58, 3.84, 0.84, -1.11), + C104.1_Zscore = c(0.53, 1.84, 0.35, -0.54, 1.48, 0.43), + C105.1_Zscore = c(3.46, -1.31, 0.14, -0.15, 1.48, 0.36), + P2025M1_Zscore = c(0.31, 1.84, 2.34, 0.84, -0.46, 0.14), + P2025M2_Zscore = c(2.45, 0.48, 1.45, -0.15, -1.51, 3.56), + P2025M3_Zscore = c(2.14, 0.15, -1.44, -0.78, 1.68, 0.51), + P2025M4_Zscore = c(12.18, 2.48, -0.18, 0.84, 1.48, -2.45), + P2025M5_Zscore = c(3.22, 0.48, -3.18, 0.47, 1.18, 2.14) + ) + rownames(test_outlist_df) <- c("HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB011", "HMDB012") + + write.table(test_outlist_df, file = "tests/testthat/fixtures/GenerateViolinPlots/test_outlist_df.txt", sep = "\t") +} + make_intensities_zscore_df <- function() { test_intensities_zscore_df <- data.frame( HMDB_code = c("HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB011", "HMDB012"), @@ -258,6 +305,8 @@ make_test_expected_biomark_df <- function() { test_expected_biomarkers_df <- data.frame( HMDB_code = c("HMDB002", "HMDB002", "HMDB005", "HMDB005", "HMDB005", "HMDB009", "HMDB012", "HMDB012", "HMDB020", "HMDB020", "HMDB025", "HMDB025", "HMDB025", "HMDB028", "HMDB028"), + HMDB_name = c("metab2", "metab2", "metab5", "metab5", "metab5", "metab9", "metab12", "metab12", "metab20", "metab20", + "metab25", "metab25", "metab25", "metab28", "metab28"), Disease = c("Disease A", "Disease B", "Disease B", "Disease B", "Disease C", "Disease D", "Disease A", "Disease E", "Disease F", "Disease C", "Disease F", "Disease G", "Disease D", "Disease G", "Disease E"), M.z = c("1.2", "1.2", "2.5", "2.5", "2.5", "3.0", "4.5", "4.5", "2.5", "2.5", "5.1", "5.1", "5.1", "5.8", "5.8"), @@ -288,3 +337,62 @@ make_test_probability_score_df <- function() { write.table(test_probability_score_df, sep = "\t", quote = FALSE, row.names = FALSE, file = "tests/testthat/fixtures/test_probability_score_df.txt") } + +make_zscore_dfs <- function() { + test_zscore_patients_df <- data.frame( + HMDB_code = c("HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB011", "HMDB012", "HMDB000TT1", "HMDB000TT2", "HMDB000TT3"), + HMDB_name = c("metab1", "metab2", "metab3", "metab4", "metab5", "metab6", "Test_ratio1", "Test_ratio2", "Test_ratio3"), + P2025M1 = c(0.31, 1.84, 2.34, 0.84, -0.46, 0.14, -0.58, 0.48, -0.45), + P2025M2 = c(2.45, 0.48, 1.45, -0.15, -1.51, 3.56, -0.71, 0.39, -0.54), + P2025M3 = c(2.14, 0.15, -1.44, -0.78, 1.68, 0.51, -0.22, 0.38, -0.48), + P2025M4 = c(12.18, 2.48, -0.18, 0.84, 1.48, -2.45, -0.21, 0.51, -0.29), + P2025M5 = c(3.22, 0.48, -3.18, 0.47, 1.18, 2.14, 1.74, -1.78, 1.78) + ) + + test_zscore_controls_df <- data.frame( + HMDB_code = c("HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB011", "HMDB012", "HMDB000TT1", "HMDB000TT2", "HMDB000TT3"), + HMDB_name = c("metab1", "metab2", "metab3", "metab4", "metab5", "metab6", "Test_ratio1", "Test_ratio2", "Test_ratio3"), + C101.1 = c(0.45, 1.67, -1.86, 0.58, 2.47, -0.56, -0.58, 0.48, -0.45), + C102.1 = c(2.89, 0.79, -1.88, 5.46, -0.68, 1.65, -0.71, 0.39, -0.54), + C103.1 = c(0.54, -0.85, 1.58, 3.84, 0.84, -1.11, -0.22, 0.38, -0.48), + C104.1 = c(0.53, 1.84, 0.35, -0.54, 1.48, 0.43, -0.21, 0.51, -0.29), + C105.1 = c(3.46, -1.31, 0.14, -0.15, 1.48, 0.36, 1.74, -1.78, 1.78) + ) + + write.table(test_zscore_patients_df, sep = "\t", quote = FALSE, row.names = FALSE, + file = "tests/testthat/fixtures/GenerateViolinPlots/test_zscore_patients_df.txt") + write.table(test_zscore_controls_df, sep = "\t", quote = FALSE, row.names = FALSE, + file = "tests/testthat/fixtures/GenerateViolinPlots/test_zscore_controls_df.txt") +} + +make_test_metabolite_class_dfs <- function() { + test_metabolite_class_patients_df <- data.frame( + HMDB_name = c("HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB011", "HMDB012", "HMDB000TT1", "HMDB000TT2", "HMDB000TT3", + "HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB011", "HMDB012", "HMDB000TT1", "HMDB000TT2", "HMDB000TT3", + "HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB011", "HMDB012", "HMDB000TT1", "HMDB000TT2", "HMDB000TT3"), + Sample = c("P2025M1", "P2025M1", "P2025M1", "P2025M1", "P2025M1", "P2025M1", "P2025M1", "P2025M1", "P2025M1", + "P2025M2", "P2025M2", "P2025M2", "P2025M2", "P2025M2", "P2025M2", "P2025M2", "P2025M2", "P2025M2", + "P2025M3", "P2025M3", "P2025M3", "P2025M3", "P2025M3", "P2025M3", "P2025M3", "P2025M3", "P2025M3"), + Z_score = c(0.31, 1.84, 2.34, 0.84, -0.46, 0.14, -0.58, 0.48, -0.45, + 2.45, 0.48, 1.45, -0.15, -1.51, 3.56, -0.71, 0.39, -0.54, + 2.14, 0.15, -1.44, -0.78, 1.68, 0.51, -0.22, 0.38, -0.48) + ) + + test_metabolite_class_controls_df <- data.frame( + HMDB_name = c("HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB011", "HMDB012", "HMDB000TT1", "HMDB000TT2", "HMDB000TT3", + "HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB011", "HMDB012", "HMDB000TT1", "HMDB000TT2", "HMDB000TT3", + "HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB011", "HMDB012", "HMDB000TT1", "HMDB000TT2", "HMDB000TT3"), + Sample = c("C101.1", "C101.1", "C101.1", "C101.1", "C101.1", "C101.1", "C101.1", "C101.1", "C101.1", + "C102.1", "C102.1", "C102.1", "C102.1", "C102.1", "C102.1", "C102.1", "C102.1", "C102.1", + "C103.1", "C103.1", "C103.1", "C103.1", "C103.1", "C103.1", "C103.1", "C103.1", "C103.1"), + Z_score = c(0.45, 1.67, -1.86, 0.58, 2.47, -0.56, -0.58, 0.48, -0.45, + 2.89, 0.79, -1.88, 5.46, -0.68, 1.65, -0.71, 0.39, -0.54, + 0.54, -0.85, 1.58, 3.84, 0.84, -1.11, -0.22, 0.38, -0.48) + ) + + write.table(test_metabolite_class_patients_df, sep = "\t", quote = FALSE, row.names = FALSE, + file = "tests/testthat/fixtures/GenerateViolinPlots/test_metabolite_class_patients_df.txt") + write.table(test_metabolite_class_controls_df, sep = "\t", quote = FALSE, row.names = FALSE, + file = "tests/testthat/fixtures/GenerateViolinPlots/test_metabolite_class_controls_df.txt") + +} diff --git a/DIMS/tests/testthat/fixtures/make_test_outlist_df.R b/DIMS/tests/testthat/fixtures/make_test_outlist_df.R index f5ee8b47..d1b451a0 100644 --- a/DIMS/tests/testthat/fixtures/make_test_outlist_df.R +++ b/DIMS/tests/testthat/fixtures/make_test_outlist_df.R @@ -24,7 +24,7 @@ make_test_outlist_df <- function() { HMDB_ID_all = c("HMDB001;HMDB011", "HMDB002", "HMDB003;HMDB013", "HMDB004"), sec_HMDB_ID = c("HMDB1;HMDB11", "", "HMDB3;HMDB13", "HMDB4"), HMDB_key = c("HMDB001", "HMDB002", "HMDB003", "HMDB004"), - sec_HMDB_ID_rlvc = c(c("HMDB1 | HMDB11", "HMDB2", "HMDB3", "HMDB4")), + sec_HMDB_ID_rlvnc = c(c("HMDB1 | HMDB11", "HMDB2", "HMDB3", "HMDB4")), name = c("metab_1 | metab_11", "metab_2", "metab_3", "metab_4"), relevance = c("Endogenous, relevant", "Endogenous, relevant | Exogenous", "Endogenous, relevant", "Endogenous, relevant"), descr = c("descr1", "descr2", "descr3", "descr4"), From fb40e09a6084238b166d75ed3b1dacd942aad134 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 26 Feb 2026 15:57:41 +0100 Subject: [PATCH 22/30] Changed and added new test files --- .../test_metabolite_class_controls_df.txt | 28 ++++++++++++++++ .../test_metabolite_class_patients_df.txt | 28 ++++++++++++++++ .../GenerateViolinPlots/test_outlist_df.txt | 7 ++++ .../test_zscore_controls_df.txt | 10 ++++++ .../test_zscore_patients_df.txt | 10 ++++++ .../fixtures/test_expected_biomarkers_df.txt | 32 +++++++++---------- .../test_acyl_carnitines.txt | 2 +- .../{ => Diagnostics}/test_crea_gua.txt | 0 DIMS/tests/testthat/fixtures/test_outlist.txt | 2 +- 9 files changed, 101 insertions(+), 18 deletions(-) create mode 100644 DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_metabolite_class_controls_df.txt create mode 100644 DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_metabolite_class_patients_df.txt create mode 100644 DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_outlist_df.txt create mode 100644 DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_zscore_controls_df.txt create mode 100644 DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_zscore_patients_df.txt rename DIMS/tests/testthat/fixtures/test_metabolite_groups/{ => Diagnostics}/test_acyl_carnitines.txt (77%) rename DIMS/tests/testthat/fixtures/test_metabolite_groups/{ => Diagnostics}/test_crea_gua.txt (100%) diff --git a/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_metabolite_class_controls_df.txt b/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_metabolite_class_controls_df.txt new file mode 100644 index 00000000..9b04311f --- /dev/null +++ b/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_metabolite_class_controls_df.txt @@ -0,0 +1,28 @@ +HMDB_name Sample Z_score +HMDB001 C101.1 0.45 +HMDB002 C101.1 1.67 +HMDB003 C101.1 -1.86 +HMDB004 C101.1 0.58 +HMDB011 C101.1 2.47 +HMDB012 C101.1 -0.56 +HMDB000TT1 C101.1 -0.58 +HMDB000TT2 C101.1 0.48 +HMDB000TT3 C101.1 -0.45 +HMDB001 C102.1 2.89 +HMDB002 C102.1 0.79 +HMDB003 C102.1 -1.88 +HMDB004 C102.1 5.46 +HMDB011 C102.1 -0.68 +HMDB012 C102.1 1.65 +HMDB000TT1 C102.1 -0.71 +HMDB000TT2 C102.1 0.39 +HMDB000TT3 C102.1 -0.54 +HMDB001 C103.1 0.54 +HMDB002 C103.1 -0.85 +HMDB003 C103.1 1.58 +HMDB004 C103.1 3.84 +HMDB011 C103.1 0.84 +HMDB012 C103.1 -1.11 +HMDB000TT1 C103.1 -0.22 +HMDB000TT2 C103.1 0.38 +HMDB000TT3 C103.1 -0.48 diff --git a/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_metabolite_class_patients_df.txt b/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_metabolite_class_patients_df.txt new file mode 100644 index 00000000..63e79a69 --- /dev/null +++ b/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_metabolite_class_patients_df.txt @@ -0,0 +1,28 @@ +HMDB_name Sample Z_score +HMDB001 P2025M1 0.31 +HMDB002 P2025M1 1.84 +HMDB003 P2025M1 2.34 +HMDB004 P2025M1 0.84 +HMDB011 P2025M1 -0.46 +HMDB012 P2025M1 0.14 +HMDB000TT1 P2025M1 -0.58 +HMDB000TT2 P2025M1 0.48 +HMDB000TT3 P2025M1 -0.45 +HMDB001 P2025M2 2.45 +HMDB002 P2025M2 0.48 +HMDB003 P2025M2 1.45 +HMDB004 P2025M2 -0.15 +HMDB011 P2025M2 -1.51 +HMDB012 P2025M2 3.56 +HMDB000TT1 P2025M2 -0.71 +HMDB000TT2 P2025M2 0.39 +HMDB000TT3 P2025M2 -0.54 +HMDB001 P2025M3 2.14 +HMDB002 P2025M3 0.15 +HMDB003 P2025M3 -1.44 +HMDB004 P2025M3 -0.78 +HMDB011 P2025M3 1.68 +HMDB012 P2025M3 0.51 +HMDB000TT1 P2025M3 -0.22 +HMDB000TT2 P2025M3 0.38 +HMDB000TT3 P2025M3 -0.48 diff --git a/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_outlist_df.txt b/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_outlist_df.txt new file mode 100644 index 00000000..3441ebaa --- /dev/null +++ b/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_outlist_df.txt @@ -0,0 +1,7 @@ +"plots" "C101.1" "C102.1" "C103.1" "C104.1" "C105.1" "P2025M1" "P2025M2" "P2025M3" "P2025M4" "P2025M5" "HMDB_name" "HMDB_name_all" "HMDB_ID_all" "sec_HMDB_ID" "HMDB_key" "sec_HMDB_ID_rlvnc" "name" "relevance" "descr" "origin" "fluids" "tissue" "disease" "pathway" "HMDB_code" "avg_ctrls" "sd_ctrls" "nr_ctrls" "C101.1_Zscore" "C102.1_Zscore" "C103.1_Zscore" "C104.1_Zscore" "C105.1_Zscore" "P2025M1_Zscore" "P2025M2_Zscore" "P2025M3_Zscore" "P2025M4_Zscore" "P2025M5_Zscore" +"HMDB001" NA 1000 1100 1300 1650 180000 1000 1100 1300 1650 180000 "metab1" NA NA NA NA NA NA NA NA NA NA NA NA NA "HMDB001" 37010 79934.23 25 0.45 2.89 0.54 0.53 3.46 0.31 2.45 2.14 12.18 3.22 +"HMDB002" NA 1200 1700 750 925 1950 1200 1700 750 925 1950 "metab2" NA NA NA NA NA NA NA NA NA NA NA NA NA "HMDB002" 1305 508.8 26 1.67 0.79 -0.85 1.84 -1.31 1.84 0.48 0.15 2.48 0.48 +"HMDB003" NA 1300 925 1000 1600 750 1300 925 1000 1600 750 "metab3" NA NA NA NA NA NA NA NA NA NA NA NA NA "HMDB003" 1115 336.15 27 -1.86 -1.88 1.58 0.35 0.14 2.34 1.45 -1.44 -0.18 -3.18 +"HMDB004" NA 1400 1125 1220 1650 15050 1400 1125 1220 1650 15050 "metab4" NA NA NA NA NA NA NA NA NA NA NA NA NA "HMDB004" 4089 6130.65 28 0.58 5.46 3.84 -0.54 -0.15 0.84 -0.15 -0.78 0.84 0.47 +"HMDB011" NA 1500 1200 1100 1025 1100 1100 1050 975 1700 10000 "metab5" NA NA NA NA NA NA NA NA NA NA NA NA NA "HMDB011" 1185 186.75 29 2.47 -0.68 0.84 1.48 1.48 -0.46 -1.51 1.68 1.48 1.18 +"HMDB012" NA 1600 1050 1200 1150 1300 975 1175 1100 1750 1500 "metab6" NA NA NA NA NA NA NA NA NA NA NA NA NA "HMDB012" 1260 210.36 30 -0.56 1.65 -1.11 0.43 0.36 0.14 3.56 0.51 -2.45 2.14 diff --git a/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_zscore_controls_df.txt b/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_zscore_controls_df.txt new file mode 100644 index 00000000..b6bf5c1c --- /dev/null +++ b/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_zscore_controls_df.txt @@ -0,0 +1,10 @@ +HMDB_code HMDB_name C101.1 C102.1 C103.1 C104.1 C105.1 +HMDB001 metab1 0.45 2.89 0.54 0.53 3.46 +HMDB002 metab2 1.67 0.79 -0.85 1.84 -1.31 +HMDB004 metab4 0.58 5.46 3.84 -0.54 -0.15 +HMDB005 metab5 2.47 -0.68 0.84 1.48 1.48 +HMDB009 metab9 -1.86 -1.88 1.58 0.35 0.14 +HMDB012 metab12 -0.56 1.65 -1.11 0.43 0.36 +HMDB000TT1 Test_ratio1 -0.58 -0.71 -0.22 -0.21 1.74 +HMDB000TT2 Test_ratio2 0.48 0.39 0.38 0.51 -1.78 +HMDB000TT3 Test_ratio3 -0.45 -0.54 -0.48 -0.29 1.78 diff --git a/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_zscore_patients_df.txt b/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_zscore_patients_df.txt new file mode 100644 index 00000000..b457af02 --- /dev/null +++ b/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_zscore_patients_df.txt @@ -0,0 +1,10 @@ +HMDB_code HMDB_name P2025M1 P2025M2 P2025M3 P2025M4 P2025M5 +HMDB001 metab1 0.31 2.45 2.14 12.18 3.22 +HMDB002 metab2 1.84 0.48 0.15 2.48 0.48 +HMDB004 metab4 0.84 -0.15 -0.78 0.84 0.47 +HMDB005 metab5 -0.46 -1.51 1.68 1.48 1.18 +HMDB009 metab9 2.34 1.45 -1.44 -0.18 -3.18 +HMDB012 metab12 0.14 3.56 0.51 -2.45 2.14 +HMDB000TT1 Test_ratio1 -0.58 -0.71 -0.22 -0.21 1.74 +HMDB000TT2 Test_ratio2 0.48 0.39 0.38 0.51 -1.78 +HMDB000TT3 Test_ratio3 -0.45 -0.54 -0.48 -0.29 1.78 diff --git a/DIMS/tests/testthat/fixtures/test_expected_biomarkers_df.txt b/DIMS/tests/testthat/fixtures/test_expected_biomarkers_df.txt index 9fb0bb0b..4038e7f9 100644 --- a/DIMS/tests/testthat/fixtures/test_expected_biomarkers_df.txt +++ b/DIMS/tests/testthat/fixtures/test_expected_biomarkers_df.txt @@ -1,16 +1,16 @@ -HMDB_code Disease M.z Change Total_Weight Absolute_Weight Dispensability -HMDB002 Disease A 1.2 Increase 10 10 Dispensable -HMDB002 Disease B 1.2 Decrease -1.5 1.5 Dispensable -HMDB005 Disease B 2.5 Increase 2 2 Indispensable -HMDB005 Disease B 2.5 Increase 5 5 Dispensable -HMDB005 Disease C 2.5 Decrease -2.5 2.5 Dispensable -HMDB009 Disease D 3.0 Decrease -3 3 Indispensable -HMDB012 Disease A 4.5 Increase 14.5 14.5 Dispensable -HMDB012 Disease E 4.5 Increase 4 4 Dispensable -HMDB020 Disease F 2.5 Decrease -7.5 7.5 Indispensable -HMDB020 Disease C 2.5 Increase 6 6 Indispensable -HMDB025 Disease F 5.1 Increase 20 20 Dispensable -HMDB025 Disease G 5.1 Decrease -5 5 Dispensable -HMDB025 Disease D 5.1 Increase 3 3 Dispensable -HMDB028 Disease G 5.8 Decrease -1.5 1.5 Dispensable -HMDB028 Disease E 5.8 Increase 4 4 Indispensable +HMDB_code HMDB_name Disease M.z Change Total_Weight Absolute_Weight Dispensability +HMDB002 metab2 Disease A 1.2 Increase 10 10 Dispensable +HMDB002 metab2 Disease B 1.2 Decrease -1.5 1.5 Dispensable +HMDB005 metab5 Disease B 2.5 Increase 2 2 Indispensable +HMDB005 metab5 Disease B 2.5 Increase 5 5 Dispensable +HMDB005 metab5 Disease C 2.5 Decrease -2.5 2.5 Dispensable +HMDB009 metab9 Disease D 3.0 Decrease -3 3 Indispensable +HMDB012 metab12 Disease A 4.5 Increase 14.5 14.5 Dispensable +HMDB012 metab12 Disease E 4.5 Increase 4 4 Dispensable +HMDB020 metab20 Disease F 2.5 Decrease -7.5 7.5 Indispensable +HMDB020 metab20 Disease C 2.5 Increase 6 6 Indispensable +HMDB025 metab25 Disease F 5.1 Increase 20 20 Dispensable +HMDB025 metab25 Disease G 5.1 Decrease -5 5 Dispensable +HMDB025 metab25 Disease D 5.1 Increase 3 3 Dispensable +HMDB028 metab28 Disease G 5.8 Decrease -1.5 1.5 Dispensable +HMDB028 metab28 Disease E 5.8 Increase 4 4 Indispensable diff --git a/DIMS/tests/testthat/fixtures/test_metabolite_groups/test_acyl_carnitines.txt b/DIMS/tests/testthat/fixtures/test_metabolite_groups/Diagnostics/test_acyl_carnitines.txt similarity index 77% rename from DIMS/tests/testthat/fixtures/test_metabolite_groups/test_acyl_carnitines.txt rename to DIMS/tests/testthat/fixtures/test_metabolite_groups/Diagnostics/test_acyl_carnitines.txt index 03a6ad6b..51c9852c 100644 --- a/DIMS/tests/testthat/fixtures/test_metabolite_groups/test_acyl_carnitines.txt +++ b/DIMS/tests/testthat/fixtures/test_metabolite_groups/Diagnostics/test_acyl_carnitines.txt @@ -1,4 +1,4 @@ HMDB_code HMDB_name Helix Helix_naam high_zscore low_zscore HMDB001 metab1 ja Metab_1 2 -1.5 HMDB003 metab3 nee Metab_3 2 -1.5 -HMDBAA1 ratio1 ja Ratio_1 2 -1.5 +HMDB000TT1 ratio1 ja Ratio_1 2 -1.5 diff --git a/DIMS/tests/testthat/fixtures/test_metabolite_groups/test_crea_gua.txt b/DIMS/tests/testthat/fixtures/test_metabolite_groups/Diagnostics/test_crea_gua.txt similarity index 100% rename from DIMS/tests/testthat/fixtures/test_metabolite_groups/test_crea_gua.txt rename to DIMS/tests/testthat/fixtures/test_metabolite_groups/Diagnostics/test_crea_gua.txt diff --git a/DIMS/tests/testthat/fixtures/test_outlist.txt b/DIMS/tests/testthat/fixtures/test_outlist.txt index 957adc6a..43a638ac 100644 --- a/DIMS/tests/testthat/fixtures/test_outlist.txt +++ b/DIMS/tests/testthat/fixtures/test_outlist.txt @@ -1,4 +1,4 @@ -"plots" "C101.1" "C102.1" "C103.1" "C104.1" "C105.1" "C106.1" "C107.1" "C108.1" "C109.1" "C110.1" "C111.1" "C112.1" "P2.1" "P3.1" "HMDB_name" "HMDB_name_all" "HMDB_ID_all" "sec_HMDB_ID" "HMDB_key" "sec_HMDB_ID_rlvc" "name" "relevance" "descr" "origin" "fluids" "tissue" "disease" "pathway" "HMDB_code" +"plots" "C101.1" "C102.1" "C103.1" "C104.1" "C105.1" "C106.1" "C107.1" "C108.1" "C109.1" "C110.1" "C111.1" "C112.1" "P2.1" "P3.1" "HMDB_name" "HMDB_name_all" "HMDB_ID_all" "sec_HMDB_ID" "HMDB_key" "sec_HMDB_ID_rlvnc" "name" "relevance" "descr" "origin" "fluids" "tissue" "disease" "pathway" "HMDB_code" "HMDB001" NA 1000 1100 1300 1650 180000 1050 1150 1350 1450 1200 1050 1250 3000 5750 "metab_1" "metab_1;metab_11" "HMDB001;HMDB011" "HMDB1;HMDB11" "HMDB001" "HMDB1 | HMDB11" "metab_1 | metab_11" "Endogenous, relevant" "descr1" "Endogenous" "Blood" "Muscle" "disease 1" "pathway1" "HMDB001" "HMDB002" NA 1200 1700 750 925 1950 1100 1250 850 1025 950 1125 975 12500 2750 "metab_2" "metab_2" "HMDB002" "" "HMDB002" "HMDB2" "metab_2" "Endogenous, relevant | Exogenous" "descr2" "Endogenous | Exogenous" "Blood" "" "disease2" "pathway2" "HMDB002" "HMDB003" NA 1300 925 1000 1600 750 1200 825 1175 1500 1750 1300 1450 5500 6750 "metab_3" "metab_3;metab_13" "HMDB003;HMDB013" "HMDB3;HMDB13" "HMDB003" "HMDB3" "metab_3" "Endogenous, relevant" "descr3" "Endogenous" "Blood" "Prostate" "" "pathway3" "HMDB003" From 56c467d5cd11d46cac3a63c943e60460c14eedf7 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 26 Feb 2026 16:06:45 +0100 Subject: [PATCH 23/30] Fixed issues occuring during testing --- DIMS/GenerateQCOutput.R | 6 +++--- DIMS/GenerateViolinPlots.R | 2 +- DIMS/export/generate_violin_plots_functions.R | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/DIMS/GenerateQCOutput.R b/DIMS/GenerateQCOutput.R index 78d815f3..15865a8c 100644 --- a/DIMS/GenerateQCOutput.R +++ b/DIMS/GenerateQCOutput.R @@ -12,8 +12,8 @@ cmd_args <- commandArgs(trailingOnly = TRUE) init_file <- cmd_args[1] project <- cmd_args[2] dims_matrix <- cmd_args[3] -sst_components_file <- cmd_args[5] -export_scripts_dir <- cmd_args[6] +sst_components_file <- cmd_args[4] +export_scripts_dir <- cmd_args[5] outdir <- "./" @@ -273,7 +273,7 @@ patterns <- c("^(P1002\\.)[[:digit:]]+_", "^(P1003\\.)[[:digit:]]+_", "^(P1005\\ positive_controls_index <- grepl(pattern = paste(patterns, collapse = "|"), column_list) positive_control_list <- column_list[positive_controls_index] -if (z_score == 1) { +if (positive_controls_index > 0) { # find if one or more positive control samples are missing pos_contr_warning <- c() if (all(sapply(c("^P1002", "^P1003", "^P1005"), diff --git a/DIMS/GenerateViolinPlots.R b/DIMS/GenerateViolinPlots.R index cad5d83d..e49b942c 100644 --- a/DIMS/GenerateViolinPlots.R +++ b/DIMS/GenerateViolinPlots.R @@ -81,7 +81,7 @@ make_and_save_violin_plot_pdfs( ) #### Run the IEM algorithm ######### -diem_probability_score <- run_diem_algorithm(expected_biomarkers_df, zscore_patients_df, patient_col_names) +diem_probability_score <- run_diem_algorithm(expected_biomarkers_df, zscore_patients_df, patient_ids) save_prob_scores_to_excel(diem_probability_score, run_name) diff --git a/DIMS/export/generate_violin_plots_functions.R b/DIMS/export/generate_violin_plots_functions.R index 83ad7cd7..33723c70 100644 --- a/DIMS/export/generate_violin_plots_functions.R +++ b/DIMS/export/generate_violin_plots_functions.R @@ -27,7 +27,7 @@ prepare_intensities_zscore_df <- function(intensities_zscore_df) { #' #' @returns sample_colnames: a vector of column names all containing the prefix. get_colnames_samples <- function(dataframe, sample_label) { - sample_colnames <- unique(gsub("_Zscore", "", grepv(paste0("^", sample_label), colnames(dataframe)))) + sample_colnames <- unique(gsub("_Zscore", "", grep(paste0("^", sample_label), colnames(dataframe), value = TRUE))) return(sample_colnames) } From 0d8a5efc60b8080e65395bd3584b9ead5047001b Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 26 Feb 2026 16:15:15 +0100 Subject: [PATCH 24/30] Fixed missing comma --- DIMS/GenerateQCOutput.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DIMS/GenerateQCOutput.R b/DIMS/GenerateQCOutput.R index 15865a8c..59dd697b 100644 --- a/DIMS/GenerateQCOutput.R +++ b/DIMS/GenerateQCOutput.R @@ -213,7 +213,7 @@ if (nrow(is_below_threshold) > 0) { row.names = FALSE, sep = "\t") } else { write.table("no internal standards are below threshold", - file = "internal_standards_below_threshold.txt" + file = "internal_standards_below_threshold.txt", row.names = FALSE, col.names = FALSE ) } From df0edf31d29f5be118f24a06539b5d598872535d Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 26 Feb 2026 16:32:31 +0100 Subject: [PATCH 25/30] Fixed missing variable and testing issues --- DIMS/GenerateViolinPlots.R | 3 ++- DIMS/export/generate_violin_plots_functions.R | 3 ++- .../testthat/test_generate_violin_plots.R | 24 ++++++++++++------- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/DIMS/GenerateViolinPlots.R b/DIMS/GenerateViolinPlots.R index e49b942c..3e20de41 100644 --- a/DIMS/GenerateViolinPlots.R +++ b/DIMS/GenerateViolinPlots.R @@ -95,7 +95,8 @@ patient_no_iem <- make_and_save_diem_plots( nr_plots_perpage, number_of_samples, number_of_metabolites, - iem_variables + iem_variables, + explanation_violin_plot ) if (length(patient_no_iem) > 0) { diff --git a/DIMS/export/generate_violin_plots_functions.R b/DIMS/export/generate_violin_plots_functions.R index 33723c70..154da4b2 100644 --- a/DIMS/export/generate_violin_plots_functions.R +++ b/DIMS/export/generate_violin_plots_functions.R @@ -877,7 +877,8 @@ make_and_save_diem_plots <- function( nr_plots_perpage, number_of_samples, number_of_metabolites, - iem_variables) { + iem_variables, + explanation_violin_plot) { diem_plot_dir <- paste("./dIEM_plots", sep = "/") dir.create(diem_plot_dir) diff --git a/DIMS/tests/testthat/test_generate_violin_plots.R b/DIMS/tests/testthat/test_generate_violin_plots.R index 5d0a818a..61c18672 100644 --- a/DIMS/tests/testthat/test_generate_violin_plots.R +++ b/DIMS/tests/testthat/test_generate_violin_plots.R @@ -11,7 +11,8 @@ source("../../export/generate_violin_plots_functions.R") testthat::test_that("get_intensities_fraction_side: Get intensities for calculating the ratios", { test_intensities_zscore_df <- read.delim(test_path("fixtures", "test_intensities_zscore_df.txt")) - + test_intensity_cols <- c("C101.1", "C102.1", "C103.1", "C104.1", "C105.1", + "P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5") test_ratios_metabs_df <- data.frame( HMDB.code = c("HMDBAA1", "HMDBAA2", "HMDBAB1"), Ratio_name = c("ratio1", "ratio2", "ratio3"), @@ -111,8 +112,8 @@ testthat::test_that("get_list_dataframes_from_dir: Get a list with dataframes fr }) testthat::test_that("merge_metabolite_info_zscores: Combine metabolite info dataframe and Z-score dataframe", { - test_acyl_carnitines_df <- read.delim(test_path("fixtures/test_metabolite_groups/", "test_acyl_carnitines.txt")) - test_crea_gua_df <- read.delim(test_path("fixtures/test_metabolite_groups/", "test_crea_gua.txt")) + test_acyl_carnitines_df <- read.delim(test_path("fixtures/test_metabolite_groups/Diagnostics", "test_acyl_carnitines.txt")) + test_crea_gua_df <- read.delim(test_path("fixtures/test_metabolite_groups/Diagnostics", "test_crea_gua.txt")) test_metab_list_all <- list(test_acyl_carnitines_df, test_crea_gua_df) names(test_metab_list_all) <- c("test_acyl_carnitines", "test_crea_gua") @@ -280,14 +281,14 @@ testthat::test_that("get_data_per_metabolite_class: Combine patient and control }) testthat::test_that("prepare_helix_patient_data: Generate a dataframe with information for Helix", { - test_acyl_carnitines_pat <- read.delim(test_path("fixtures/", "test_acyl_carnitines_patients.txt")) - test_crea_gua_pat <- read.delim(test_path("fixtures/", "test_crea_gua_patients.txt")) + test_acyl_carnitines_pat <- read.delim(test_path("fixtures", "test_acyl_carnitines_patients.txt")) + test_crea_gua_pat <- read.delim(test_path("fixtures", "test_crea_gua_patients.txt")) test_metab_interest_sorted <- list(test_acyl_carnitines_pat, test_crea_gua_pat) names(test_metab_interest_sorted) <- c("test_acyl_carnitines", "test_crea_gua") - test_acyl_carnitines_df <- read.delim(test_path("fixtures/test_metabolite_groups/", "test_acyl_carnitines.txt")) - test_crea_gua_df <- read.delim(test_path("fixtures/test_metabolite_groups/", "test_crea_gua.txt")) + test_acyl_carnitines_df <- read.delim(test_path("fixtures/test_metabolite_groups/Diagnostics", "test_acyl_carnitines.txt")) + test_crea_gua_df <- read.delim(test_path("fixtures/test_metabolite_groups/Diagnostics", "test_crea_gua.txt")) test_metab_list_all <- list(test_acyl_carnitines_df, test_crea_gua_df) names(test_metab_list_all) <- c("test_acyl_carnitines", "test_crea_gua") @@ -852,6 +853,7 @@ testthat::test_that("make_and_save_diem_plots: Make and save dIEM plots", { top_number_iem_diseases = 5, threshold_iem = 5 ) + test_explanation_violin_plot <- "Unit test violin plot pdfs" expect_silent(make_and_save_diem_plots( test_diem_probability_score, @@ -862,8 +864,10 @@ testthat::test_that("make_and_save_diem_plots: Make and save dIEM plots", { test_nr_plots_perpage, test_number_of_samples, test_number_of_metabolites, - test_iem_variables + test_iem_variables, + test_explanation_violin_plot )) + unlink("dIEM_plots/", recursive = TRUE) expect_equal(make_and_save_diem_plots( test_diem_probability_score, @@ -874,7 +878,8 @@ testthat::test_that("make_and_save_diem_plots: Make and save dIEM plots", { test_nr_plots_perpage, test_number_of_samples, test_number_of_metabolites, - test_iem_variables + test_iem_variables, + test_explanation_violin_plot ), "P2025M3") expect_true(file.exists("dIEM_plots/IEM_P2025M1.pdf")) @@ -922,4 +927,5 @@ testthat::test_that("save_patient_no_iem: Save a list of patient IDs to a text f expect_true(file.exists("missing_probability_scores.txt")) expect_snapshot(save_patient_no_iem(test_threshold_iem, test_patient_no_iem)) + file.remove("missing_probability_scores.txt") }) From 0a1eb5ce2f4c3fd0ad1174b4913cf19fcd8e0f44 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 26 Feb 2026 19:38:31 +0100 Subject: [PATCH 26/30] Fixed unit test issues --- DIMS/tests/testthat/test_generate_violin_plots.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DIMS/tests/testthat/test_generate_violin_plots.R b/DIMS/tests/testthat/test_generate_violin_plots.R index 61c18672..b709b656 100644 --- a/DIMS/tests/testthat/test_generate_violin_plots.R +++ b/DIMS/tests/testthat/test_generate_violin_plots.R @@ -926,6 +926,6 @@ testthat::test_that("save_patient_no_iem: Save a list of patient IDs to a text f expect_true(file.exists("missing_probability_scores.txt")) - expect_snapshot(save_patient_no_iem(test_threshold_iem, test_patient_no_iem)) + expect_snapshot_file("missing_probability_scores.txt") file.remove("missing_probability_scores.txt") }) From 34b26f77a62494276c05383ade7579074c18e08a Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 26 Feb 2026 19:38:46 +0100 Subject: [PATCH 27/30] Test files and snapshots --- .../testthat/_snaps/generate_violin_plots.md | 4 +-- .../missing_probability_scores.txt | 3 +++ .../Other/test_other.txt | 4 +++ DIMS/tests/testthat/test_evaluate_tics.R | 26 +++++++++---------- 4 files changed, 22 insertions(+), 15 deletions(-) create mode 100644 DIMS/tests/testthat/_snaps/generate_violin_plots/missing_probability_scores.txt create mode 100644 DIMS/tests/testthat/fixtures/test_metabolite_groups/Other/test_other.txt diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots.md b/DIMS/tests/testthat/_snaps/generate_violin_plots.md index 5df67e7a..4930649a 100644 --- a/DIMS/tests/testthat/_snaps/generate_violin_plots.md +++ b/DIMS/tests/testthat/_snaps/generate_violin_plots.md @@ -1,4 +1,4 @@ -# Create a pdf with a table of top metabolites and violin plots +# create_pdf_violin_plots: Create a pdf with a table of top metabolites and violin plots Code content_pdf_violinplots @@ -8,7 +8,7 @@ [3] " Results for patient P2025M1\n test crea gua\n metab4 Z=−0.46\nMetabolites\n metab11 Z=0.84\n −5 0 5 10 15 20\n Z−scores\n" [4] " Unit test Generate Violin Plots\nUnit test Generate Violin Plots\n" -# Saving the probability score dataframe as an Excel file +# save_prob_scores_to_excel: Saving the probability score dataframe as an Excel file Disease P2025M1 P2025M2 P2025M3 P2025M4 1 Disease A 10.900 -10.9 49.90 -49.9 diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots/missing_probability_scores.txt b/DIMS/tests/testthat/_snaps/generate_violin_plots/missing_probability_scores.txt new file mode 100644 index 00000000..f7e33571 --- /dev/null +++ b/DIMS/tests/testthat/_snaps/generate_violin_plots/missing_probability_scores.txt @@ -0,0 +1,3 @@ +The following patient(s) did not have dIEM probability scores higher than 5 : +Patient1 +Patient2 diff --git a/DIMS/tests/testthat/fixtures/test_metabolite_groups/Other/test_other.txt b/DIMS/tests/testthat/fixtures/test_metabolite_groups/Other/test_other.txt new file mode 100644 index 00000000..76013216 --- /dev/null +++ b/DIMS/tests/testthat/fixtures/test_metabolite_groups/Other/test_other.txt @@ -0,0 +1,4 @@ +HMDB_code HMDB_name Helix Helix_naam high_zscore low_zscore +HMDB004 metab4 ja Metab_4 2 -1.5 +HMDB011 metab11 ja Metab_11 2 -1.5 +HMDB000TT1 ratio1 ja Ratio_1 2 -1.5 diff --git a/DIMS/tests/testthat/test_evaluate_tics.R b/DIMS/tests/testthat/test_evaluate_tics.R index b474a81b..2682c398 100644 --- a/DIMS/tests/testthat/test_evaluate_tics.R +++ b/DIMS/tests/testthat/test_evaluate_tics.R @@ -7,15 +7,15 @@ source("../../preprocessing/evaluate_tics_functions.R") testthat::test_that("TICS are correctly accepted or rejected", { # It's necessary to copy/symlink the files to the current location for the combine_sum_adducts_parts function # local: setwd("~/Development/DIMS_refactor_PeakFinding_codereview/CustomModules/DIMS/tests/testthat") - test_files <- list.files("fixtures/", "test_evaluate_tics", full.names = TRUE) + test_files <- list.files("fixtures", "test_evaluate_tics", full.names = TRUE) file.symlink(file.path(test_files), getwd()) - + # create replication pattern to test on: technical_replicates <- paste0("test_evaluate_tics_file", 1:3) test_repl_pattern <- list(technical_replicates) names(test_repl_pattern) <- "sample1" test_thresh2remove <- 10^9 - + # test that output has two entries expect_equal(length(find_bad_replicates(test_repl_pattern, test_thresh2remove)), 2) # test that first technical replicate is removed in positive scan mode @@ -24,11 +24,12 @@ testthat::test_that("TICS are correctly accepted or rejected", { expect_equal(find_bad_replicates(test_repl_pattern, test_thresh2remove)$neg, "test_evaluate_tics_file3", TRUE) # test that output files are generated expect_equal(sum(grepl("miss_infusions_", list.files("./"))), 2) - + # Remove symlinked files - files_remove <- list.files("./", "SummedAdducts_test.RData", full.names = TRUE) + files_remove_tics <- list.files("./", "test_evaluate_tics_file", full.names = TRUE) + files_remove_miss <- list.files("./", "miss_infusions_", full.names = TRUE) + files_remove <- c(files_remove_tics, files_remove_miss) file.remove(files_remove) - }) # test remove_from_repl_pattern @@ -39,7 +40,7 @@ testthat::test_that("technical replicates are correctly removed from replication names(test_repl_pattern) <- "sample1" test_bad_samples <- "test_evaluate_tics_file2" test_nr_replicates <- 3 - + # test that the output contains 1 sample expect_equal(length(remove_from_repl_pattern(test_bad_samples, test_repl_pattern, test_nr_replicates)), 1) # test that the output for the sample contains 2 technical replicates @@ -57,10 +58,9 @@ testthat::test_that("overview of technical replicates is correctly created", { test_scanmode <- "positive" # test that overview is correctly created - expect_equal(get_overview_tech_reps(test_repl_pattern_filtered, test_scanmode)[ ,1], "sample1") - expect_equal(get_overview_tech_reps(test_repl_pattern_filtered, test_scanmode)[ ,3], "positive") - expect_true(get_overview_tech_reps(test_repl_pattern_filtered, test_scanmode)[ ,2] == - paste0(technical_replicates, collapse = ";"), TRUE) - -}) + expect_equal(get_overview_tech_reps(test_repl_pattern_filtered, test_scanmode)[, 1], "sample1") + expect_equal(get_overview_tech_reps(test_repl_pattern_filtered, test_scanmode)[, 3], "positive") + expect_true(get_overview_tech_reps(test_repl_pattern_filtered, test_scanmode)[, 2] == + paste0(technical_replicates, collapse = ";"), TRUE) +}) From b56fdfcdd9e069755e00bc38821a5f63e2ee931d Mon Sep 17 00:00:00 2001 From: ALuesink Date: Mon, 2 Mar 2026 10:52:15 +0100 Subject: [PATCH 28/30] Styling and linting --- DIMS/GenerateViolinPlots.R | 6 +- DIMS/export/generate_violin_plots_functions.R | 4 +- .../testthat/test_generate_violin_plots.R | 176 ++++++++++-------- 3 files changed, 106 insertions(+), 80 deletions(-) diff --git a/DIMS/GenerateViolinPlots.R b/DIMS/GenerateViolinPlots.R index 3e20de41..0007881b 100644 --- a/DIMS/GenerateViolinPlots.R +++ b/DIMS/GenerateViolinPlots.R @@ -27,8 +27,10 @@ rm(outlist) metabolites_ratios_df <- read.csv(file_ratios_metabolites, sep = ";", stringsAsFactors = FALSE) expected_biomarkers_df <- read.csv(file_expected_biomarkers_iem, sep = ";", stringsAsFactors = FALSE) expected_biomarkers_df <- expected_biomarkers_df %>% - rename(HMDB_code = HMDB.code, - HMDB_name = Metabolite) + rename( + HMDB_code = HMDB.code, + HMDB_name = Metabolite + ) explanation_violin_plot <- readLines(file_explanation) # Set global variables diff --git a/DIMS/export/generate_violin_plots_functions.R b/DIMS/export/generate_violin_plots_functions.R index 154da4b2..378148e4 100644 --- a/DIMS/export/generate_violin_plots_functions.R +++ b/DIMS/export/generate_violin_plots_functions.R @@ -430,7 +430,7 @@ get_list_page_plot_data <- function( #' Create the order of metabolites and add empty strings if the number of metabolites is lower than #' the number of plots per page. #' -#' @param number_of_plots_per_page: integer containing the number of metabolites per plot per page +#' @param number_of_plots_per_page: integer containing the number of metabolites per plot per page #' @param metabolite_names_chunk: list of vectors, each containing metabolites #' #' @returns metabolite_order: a vector containing all metabolites and possibly empty strings @@ -963,7 +963,7 @@ get_probabilities_top_iems <- function(patient_top_iems_probs, expected_biomarke #' Save a list of patient IDs to a text file #' #' @param threshold_iem: integer containing the IEM threshold -#' @param patient_no_iem: vector containing patient IDs +#' @param patient_no_iem: vector containing patient IDs save_patient_no_iem <- function(threshold_iem, patient_no_iem) { patient_no_iem <- c( paste0( diff --git a/DIMS/tests/testthat/test_generate_violin_plots.R b/DIMS/tests/testthat/test_generate_violin_plots.R index b709b656..fd48cbe8 100644 --- a/DIMS/tests/testthat/test_generate_violin_plots.R +++ b/DIMS/tests/testthat/test_generate_violin_plots.R @@ -11,8 +11,10 @@ source("../../export/generate_violin_plots_functions.R") testthat::test_that("get_intensities_fraction_side: Get intensities for calculating the ratios", { test_intensities_zscore_df <- read.delim(test_path("fixtures", "test_intensities_zscore_df.txt")) - test_intensity_cols <- c("C101.1", "C102.1", "C103.1", "C104.1", "C105.1", - "P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5") + test_intensity_cols <- c( + "C101.1", "C102.1", "C103.1", "C104.1", "C105.1", + "P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5" + ) test_ratios_metabs_df <- data.frame( HMDB.code = c("HMDBAA1", "HMDBAA2", "HMDBAB1"), Ratio_name = c("ratio1", "ratio2", "ratio3"), @@ -370,7 +372,7 @@ testthat::test_that("transform_metab_df_to_helix_df: Make the output for Helix", ) }) -testthat::test_that("get_top_metabolites_df: Create a dataframe with all metabolites that exceed the min and max Z-score cutoff", { +testthat::test_that("get_top_metabolites_df: Create a dataframe with the top metabolites", { test_df_metabs_helix <- read.delim(test_path("fixtures/", "test_df_metabs_helix.txt")) test_patient_id <- "P2025M1" @@ -407,15 +409,15 @@ testthat::test_that("get_top_metabolites_df: Create a dataframe with all metabol testthat::test_that("prepare_toplist: Create a dataframe with the top 20 highest and top 10 lowest metabolites", { test_zscore_patient_df <- read.delim(test_path("fixtures/", "test_zscore_patient_df.txt")) test_patient_id <- "P2025M1" - test_num_of_highest_metabolites <- 20 - test_num_of_lowest_metabolites <- 10 + test_num_of_highest_metabs <- 20 + test_num_of_lowest_metabs <- 10 expect_equal( dim(prepare_toplist( test_patient_id, test_zscore_patient_df, - test_num_of_highest_metabolites, - test_num_of_lowest_metabolites + test_num_of_highest_metabs, + test_num_of_lowest_metabs )), c(32, 3) ) @@ -423,8 +425,8 @@ testthat::test_that("prepare_toplist: Create a dataframe with the top 20 highest colnames(prepare_toplist( test_patient_id, test_zscore_patient_df, - test_num_of_highest_metabolites, - test_num_of_lowest_metabolites + test_num_of_highest_metabs, + test_num_of_lowest_metabs )), c("HMDB_ID", "Metabolite", "Z-score") ) @@ -432,8 +434,8 @@ testthat::test_that("prepare_toplist: Create a dataframe with the top 20 highest prepare_toplist( test_patient_id, test_zscore_patient_df, - test_num_of_highest_metabolites, - test_num_of_lowest_metabolites + test_num_of_highest_metabs, + test_num_of_lowest_metabs )$HMDB_ID, c( "Increased", "HMDB030", "HMDB029", "HMDB028", "HMDB027", "HMDB026", "HMDB025", "HMDB024", "HMDB023", @@ -446,8 +448,8 @@ testthat::test_that("prepare_toplist: Create a dataframe with the top 20 highest prepare_toplist( test_patient_id, test_zscore_patient_df, - test_num_of_highest_metabolites, - test_num_of_lowest_metabolites + test_num_of_highest_metabs, + test_num_of_lowest_metabs )$`Z-score`, c( "", "30", "29", "28", "27", "26", "25", "24", "23", "22", "21", "20", "19", "18", "17", "16", "15", @@ -461,8 +463,8 @@ testthat::test_that("prepare_toplist: Create a dataframe with the top 20 highest prepare_toplist( test_patient_id, test_zscore_patient_df, - test_num_of_highest_metabolites, - test_num_of_lowest_metabolites + test_num_of_highest_metabs, + test_num_of_lowest_metabs )$Metabolite, c( "", "metab1", "metab2", "metab3", "metab4", "metab5", "metab6", "metab7", "metab8", "metab9", "metab10", @@ -475,8 +477,8 @@ testthat::test_that("prepare_toplist: Create a dataframe with the top 20 highest prepare_toplist( test_patient_id, test_zscore_patient_df, - test_num_of_highest_metabolites, - test_num_of_lowest_metabolites + test_num_of_highest_metabs, + test_num_of_lowest_metabs )$`Z-score`, c( "", "-1", "-2", "-3", "-4", "-5", "-6", "-7", "-8", "-9", "-10", "-11", "-12", "-13", "-14", "-15", @@ -712,50 +714,55 @@ testthat::test_that("calculate_zscore_ratios: Calculate Zscores for ratios", { }) testthat::test_that("get_list_page_plot_data: Get a list of dataframes for each chunk", { - test_metabolite_class_patients_df <- read.delim(test_path("fixtures/GenerateViolinPlots", - "test_metabolite_class_patients_df.txt")) - test_metabolite_class_controls_df <- read.delim(test_path("fixtures/GenerateViolinPlots", - "test_metabolite_class_controls_df.txt")) + test_metab_class_patients_df <- read.delim(test_path( + "fixtures/GenerateViolinPlots", + "test_metabolite_class_patients_df.txt" + )) + test_metab_class_controls_df <- read.delim(test_path( + "fixtures/GenerateViolinPlots", + "test_metabolite_class_controls_df.txt" + )) test_nr_plots_perpage <- 2 test_metabolite_in_chunks <- list( c("HMDB001", "HMDB002", "HMDB003"), c("HMDB004", "HMDB011", "HMDB012"), c("HMDB000TT1", "HMDB000TT2", "HMDB000TT3") ) - + t <- get_list_page_plot_data( test_metabolite_in_chunks, - test_metabolite_class_patients_df, - test_metabolite_class_controls_df, + test_metab_class_patients_df, + test_metab_class_controls_df, test_nr_plots_perpage ) - + expect_type(get_list_page_plot_data( test_metabolite_in_chunks, - test_metabolite_class_patients_df, - test_metabolite_class_controls_df, + test_metab_class_patients_df, + test_metab_class_controls_df, test_nr_plots_perpage ), "list") - + expect_equal(length(get_list_page_plot_data( test_metabolite_in_chunks, - test_metabolite_class_patients_df, - test_metabolite_class_controls_df, + test_metab_class_patients_df, + test_metab_class_controls_df, test_nr_plots_perpage )), 3) - + test_plot_data_list <- get_list_page_plot_data( test_metabolite_in_chunks, - test_metabolite_class_patients_df, - test_metabolite_class_controls_df, + test_metab_class_patients_df, + test_metab_class_controls_df, test_nr_plots_perpage ) for (num_chunk in seq_along(test_metabolite_in_chunks)) { expect_equal(unique(test_plot_data_list[[num_chunk]]$HMDB_name), test_metabolite_in_chunks[[num_chunk]]) - expect_equal(unique(test_plot_data_list[[num_chunk]]$Sample), - c("P2025M1", "P2025M2", "P2025M3", "C101.1", "C102.1", "C103.1")) + expect_equal( + unique(test_plot_data_list[[num_chunk]]$Sample), + c("P2025M1", "P2025M2", "P2025M3", "C101.1", "C102.1", "C103.1") + ) } - }) testthat::test_that("make_and_save_violin_plot_pdfs: Make and save violin plots for each patient in a PDF", { @@ -786,18 +793,18 @@ testthat::test_that("make_and_save_violin_plot_pdfs: Make and save violin plots test_explanation_violin_plot, test_number_of_metabolites )) - + patient_ids <- c("P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5") for (patient_id in patient_ids) { pdf_file_name_diagnotics <- file.path(paste0("Diagnostics/MB", gsub("^P|M", "", patient_id), "_DIMS_PL_DIAG.pdf")) pdf_file_name_other <- file.path(paste0("Other/R_", patient_id, ".pdf")) - + expect_true(file.exists(pdf_file_name_diagnotics)) expect_true(file.exists(pdf_file_name_other)) } expect_true(file.exists("output_Helix_unit_test.csv")) - - unlink(c("Diagnostics", 'Other'), recursive = TRUE) + + unlink(c("Diagnostics", "Other"), recursive = TRUE) file.remove("output_Helix_unit_test.csv") }) @@ -808,25 +815,36 @@ testthat::test_that("get_probabilities_top_iems: Get the IEM probabilities for a ) test_expected_biomarkers_df <- read.delim(test_path("fixtures", "test_expected_biomarkers_df.txt")) test_patient_id <- "P2025M1" - - - expect_type(get_probabilities_top_iems(test_patient_top_iems_probs, test_expected_biomarkers_df, test_patient_id), - "list") - - expect_equal(length(get_probabilities_top_iems(test_patient_top_iems_probs, test_expected_biomarkers_df, test_patient_id)), - 4) - expect_equal(names(get_probabilities_top_iems(test_patient_top_iems_probs, test_expected_biomarkers_df, test_patient_id)), - c("Disease A, probability score 100", "Disease B, probability score 75", "Disease C, probability score 50", - "Disease D, probability score 25")) - - expect_equal(get_probabilities_top_iems(test_patient_top_iems_probs, - test_expected_biomarkers_df, - test_patient_id)$"Disease A, probability score 100", - data.frame( - HMDB_code = c("HMDB002", "HMDB012"), - HMDB_name = c("metab2", "metab12") - )) - + + + expect_type( + get_probabilities_top_iems(test_patient_top_iems_probs, test_expected_biomarkers_df, test_patient_id), + "list" + ) + + expect_equal( + length(get_probabilities_top_iems(test_patient_top_iems_probs, test_expected_biomarkers_df, test_patient_id)), + 4 + ) + expect_equal( + names(get_probabilities_top_iems(test_patient_top_iems_probs, test_expected_biomarkers_df, test_patient_id)), + c( + "Disease A, probability score 100", "Disease B, probability score 75", "Disease C, probability score 50", + "Disease D, probability score 25" + ) + ) + + expect_equal( + get_probabilities_top_iems( + test_patient_top_iems_probs, + test_expected_biomarkers_df, + test_patient_id + )$"Disease A, probability score 100", + data.frame( + HMDB_code = c("HMDB002", "HMDB012"), + HMDB_name = c("metab2", "metab12") + ) + ) }) testthat::test_that("make_and_save_diem_plots: Make and save dIEM plots", { @@ -868,7 +886,7 @@ testthat::test_that("make_and_save_diem_plots: Make and save dIEM plots", { test_explanation_violin_plot )) unlink("dIEM_plots/", recursive = TRUE) - + expect_equal(make_and_save_diem_plots( test_diem_probability_score, test_patient_ids, @@ -881,22 +899,24 @@ testthat::test_that("make_and_save_diem_plots: Make and save dIEM plots", { test_iem_variables, test_explanation_violin_plot ), "P2025M3") - + expect_true(file.exists("dIEM_plots/IEM_P2025M1.pdf")) expect_true(file.exists("dIEM_plots/IEM_P2025M2.pdf")) - + unlink("dIEM_plots/", recursive = TRUE) }) testthat::test_that("make_metabolite_order: Make the order of metabolites for the violin plots", { test_metabolites_vector <- c("metab1", "metab2", "metab3") test_num_plots_per_page <- 5 - + make_metabolite_order(test_num_plots_per_page, test_metabolites_vector) - + expect_equal(length(make_metabolite_order(test_num_plots_per_page, test_metabolites_vector)), 5) - expect_equal(make_metabolite_order(test_num_plots_per_page, test_metabolites_vector), - c("metab1", "metab2", "metab3", " ", " ")) + expect_equal( + make_metabolite_order(test_num_plots_per_page, test_metabolites_vector), + c("metab1", "metab2", "metab3", " ", " ") + ) }) testthat::test_that("pad_truncate_hmdb_names: Pad or truncate HMDB names to a fixed width", { @@ -905,14 +925,18 @@ testthat::test_that("pad_truncate_hmdb_names: Pad or truncate HMDB names to a fi ) test_width <- 10 test_pad_character <- "+" - - expect_equal(nrow(pad_truncate_hmdb_names(test_dataframe, test_width, test_pad_character)), - 3) - expect_equal(pad_truncate_hmdb_names(test_dataframe, test_width, test_pad_character)$HMDB_name, - c("metab1++++", "metabol...", "metabo3+++")) - + + expect_equal( + nrow(pad_truncate_hmdb_names(test_dataframe, test_width, test_pad_character)), + 3 + ) + expect_equal( + pad_truncate_hmdb_names(test_dataframe, test_width, test_pad_character)$HMDB_name, + c("metab1++++", "metabol...", "metabo3+++") + ) + test_df <- pad_truncate_hmdb_names(test_dataframe, test_width, test_pad_character) - for (row in seq(nrow(test_df))) { + for (row in seq_len(nrow(test_df))) { expect_equal(nchar(test_df[row, "HMDB_name"]), 10) } }) @@ -921,11 +945,11 @@ testthat::test_that("save_patient_no_iem: Save a list of patient IDs to a text f local_edition(3) test_threshold_iem <- 5 test_patient_no_iem <- c("Patient1", "Patient2") - + expect_silent(save_patient_no_iem(test_threshold_iem, test_patient_no_iem)) - + expect_true(file.exists("missing_probability_scores.txt")) - + expect_snapshot_file("missing_probability_scores.txt") file.remove("missing_probability_scores.txt") }) From 78285ff2dd5cb27483ea2d4316aaf662d0ee9493 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Mon, 2 Mar 2026 14:06:12 +0100 Subject: [PATCH 29/30] Add theormz_HMDB to columns to remove --- DIMS/export/generate_violin_plots_functions.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DIMS/export/generate_violin_plots_functions.R b/DIMS/export/generate_violin_plots_functions.R index 378148e4..165c6bb0 100644 --- a/DIMS/export/generate_violin_plots_functions.R +++ b/DIMS/export/generate_violin_plots_functions.R @@ -10,7 +10,7 @@ prepare_intensities_zscore_df <- function(intensities_zscore_df) { intensities_zscore_df <- intensities_zscore_df %>% select(-c( - plots, HMDB_name_all, HMDB_ID_all, sec_HMDB_ID, HMDB_key, sec_HMDB_ID_rlvnc, name, + plots, HMDB_name_all, HMDB_ID_all, sec_HMDB_ID, HMDB_key, theormz_HMDB, sec_HMDB_ID_rlvnc, name, relevance, descr, origin, fluids, tissue, disease, pathway, nr_ctrls )) %>% relocate(c(HMDB_code, HMDB_name)) %>% From a02deba62dd34a03a4275c969aca91a23d46f3fd Mon Sep 17 00:00:00 2001 From: ALuesink Date: Mon, 2 Mar 2026 14:20:44 +0100 Subject: [PATCH 30/30] Revert "Add theormz_HMDB to columns to remove" This reverts commit 78285ff2dd5cb27483ea2d4316aaf662d0ee9493. Changes to the wrong feature branch. --- DIMS/export/generate_violin_plots_functions.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DIMS/export/generate_violin_plots_functions.R b/DIMS/export/generate_violin_plots_functions.R index 165c6bb0..378148e4 100644 --- a/DIMS/export/generate_violin_plots_functions.R +++ b/DIMS/export/generate_violin_plots_functions.R @@ -10,7 +10,7 @@ prepare_intensities_zscore_df <- function(intensities_zscore_df) { intensities_zscore_df <- intensities_zscore_df %>% select(-c( - plots, HMDB_name_all, HMDB_ID_all, sec_HMDB_ID, HMDB_key, theormz_HMDB, sec_HMDB_ID_rlvnc, name, + plots, HMDB_name_all, HMDB_ID_all, sec_HMDB_ID, HMDB_key, sec_HMDB_ID_rlvnc, name, relevance, descr, origin, fluids, tissue, disease, pathway, nr_ctrls )) %>% relocate(c(HMDB_code, HMDB_name)) %>%