Skip to content

Commit 01dd914

Browse files
Feature diann anomaly (#119)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent e347aea commit 01dd914

10 files changed

Lines changed: 491 additions & 92 deletions

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ inst/doc
88
*.log
99
*.o
1010
*.so
11-
*.dll
11+
*.dll
12+
.lintr
13+
.vscode

R/MSstatsConvert_core_functions.R

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,12 @@ MSstatsMakeAnnotation = function(input, annotation, ...) {
518518
}
519519

520520
#' Run Anomaly Model
521+
#'
522+
#' Detects anomalous measurements in mass spectrometry data using an isolation forest algorithm.
523+
#' This function identifies unusual precursor measurements based on quality metrics and their
524+
#' temporal patterns. For features with insufficient quality metric data, it assigns anomaly
525+
#' scores based on the median score of similar features (same peptide and charge combination).
526+
#' The model supports parallel processing for improved performance on large datasets.
521527
#'
522528
#' @param input data.table preprocessed by the MSstatsBalancedDesign function
523529
#' @param quality_metrics character vector of quality metrics to use in the model
@@ -535,7 +541,7 @@ MSstatsMakeAnnotation = function(input, annotation, ...) {
535541
MSstatsAnomalyScores = function(input, quality_metrics, temporal_direction,
536542
missing_run_count, n_feat, run_order, n_trees,
537543
max_depth, cores){
538-
544+
539545
input = .prepareSpectronautAnomalyInput(input, quality_metrics,
540546
run_order, n_feat,
541547
missing_run_count)
@@ -548,14 +554,14 @@ MSstatsAnomalyScores = function(input, quality_metrics, temporal_direction,
548554
temporal_direction[i]))
549555
}
550556
}
551-
557+
552558
input = .runAnomalyModel(input,
553559
n_trees=n_trees,
554560
max_depth=max_depth,
555561
cores=cores,
556562
split_column="PSM",
557563
quality_metrics=quality_metrics)
558-
564+
559565
subset_cols = c("Run", "ProteinName", "PeptideSequence",
560566
"PrecursorCharge", "FragmentIon",
561567
"ProductCharge", "IsotopeLabelType",

R/clean_DIANN.R

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
quantificationColumn = "FragmentQuantCorrected",
99
global_qvalue_cutoff = 0.01,
1010
qvalue_cutoff = 0.01,
11-
pg_qvalue_cutoff = 0.01) {
11+
pg_qvalue_cutoff = 0.01,
12+
calculateAnomalyScores = FALSE,
13+
anomalyModelFeatures = c()) {
1214
dn_input <- getInputFile(msstats_object, "input")
1315
dn_input <- data.table::as.data.table(dn_input)
1416

@@ -19,7 +21,9 @@
1921
dn_input <- .cleanDIANNAddMissingColumns(dn_input)
2022

2123
# Select required columns
22-
dn_input <- .cleanDIANNSelectRequiredColumns(dn_input, quantificationColumn, MBR)
24+
dn_input <- .cleanDIANNSelectRequiredColumns(dn_input, quantificationColumn, MBR,
25+
calculateAnomalyScores,
26+
anomalyModelFeatures)
2327

2428
# Split concatenated values
2529
dn_input <- .cleanDIANNSplitConcatenatedValues(dn_input, quantificationColumn)
@@ -78,7 +82,9 @@
7882
#' @param MBR logical indicating if match between runs was used
7983
#' @return data.table with selected columns
8084
#' @noRd
81-
.cleanDIANNSelectRequiredColumns <- function(dn_input, quantificationColumn, MBR) {
85+
.cleanDIANNSelectRequiredColumns <- function(dn_input, quantificationColumn, MBR,
86+
calculateAnomalyScores,
87+
anomalyModelFeatures) {
8288
base_cols <- c('ProteinNames', 'StrippedSequence', 'ModifiedSequence',
8389
'PrecursorCharge', quantificationColumn, 'QValue',
8490
'PrecursorMz', 'FragmentInfo', 'Run')
@@ -89,7 +95,13 @@
8995
c('GlobalQValue', 'GlobalPGQValue')
9096
}
9197

92-
req_cols <- c(base_cols, mbr_cols)
98+
qual_cols <- if (calculateAnomalyScores) {
99+
anomalyModelFeatures
100+
} else {
101+
c()
102+
}
103+
104+
req_cols <- c(base_cols, mbr_cols, qual_cols)
93105
return(dn_input[, req_cols, with = FALSE])
94106
}
95107

@@ -163,13 +175,13 @@
163175
getOption("MSstatsLog")("INFO", msg)
164176
getOption("MSstatsMsg")("INFO", msg)
165177

166-
dn_input = dn_input[QValue < global_qvalue_cutoff, ]
178+
dn_input = dn_input[QValue >= global_qvalue_cutoff, quantificationColumn := 0]
167179
if (MBR) {
168180
msg = '** MBR was used to analyze the data. Now setting names and filtering'
169181
msg_1_mbr = paste0('-- LibPGQValue < ', pg_qvalue_cutoff)
170182
msg_2_mbr = paste0('-- LibQValue < ', qvalue_cutoff)
171-
dn_input = dn_input[LibPGQValue < pg_qvalue_cutoff, ]
172-
dn_input = dn_input[LibQValue < qvalue_cutoff, ]
183+
dn_input = dn_input[LibPGQValue >= pg_qvalue_cutoff, , quantificationColumn := 0]
184+
dn_input = dn_input[LibQValue >= qvalue_cutoff, , quantificationColumn := 0]
173185
getOption("MSstatsLog")("INFO", msg)
174186
getOption("MSstatsMsg")("INFO", msg)
175187
getOption("MSstatsLog")("INFO", msg_1_mbr)
@@ -181,8 +193,8 @@
181193
msg = '** MBR was not used to analyze the data. Now setting names and filtering'
182194
msg_1 = paste0('-- Filtering on GlobalPGQValue < ', pg_qvalue_cutoff)
183195
msg_2 = paste0('-- Filtering on GlobalQValue < ', qvalue_cutoff)
184-
dn_input = dn_input[GlobalPGQValue < pg_qvalue_cutoff, ]
185-
dn_input = dn_input[GlobalQValue < qvalue_cutoff, ]
196+
dn_input = dn_input[GlobalPGQValue >= pg_qvalue_cutoff, quantificationColumn := 0]
197+
dn_input = dn_input[GlobalQValue >= qvalue_cutoff, quantificationColumn := 0]
186198
getOption("MSstatsLog")("INFO", msg)
187199
getOption("MSstatsMsg")("INFO", msg)
188200
getOption("MSstatsLog")("INFO", msg_1)

R/converters_DIANNtoMSstatsFormat.R

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@
2222
#' @param quantificationColumn Use 'FragmentQuantCorrected'(default) column for quantified intensities for DIANN 1.8.x.
2323
#' Use 'FragmentQuantRaw' for quantified intensities for DIANN 1.9.x.
2424
#' Use 'auto' for quantified intensities for DIANN 2.x where each fragment intensity is a separate column, e.g. Fr0Quantity.
25+
#' @param calculateAnomalyScores Default is FALSE. If TRUE, will run anomaly detection model and calculate anomaly scores for each feature. Used downstream to weigh measurements in differential analysis.
26+
#' @param anomalyModelFeatures character vector of quality metric column names to be used as features in the anomaly detection model. List must not be empty if calculateAnomalyScores=TRUE.
27+
#' @param anomalyModelFeatureTemporal character vector of temporal direction corresponding to columns passed to anomalyModelFeatures. Values must be one of: `mean_decrease`, `mean_increase`, `dispersion_increase`, or NULL (to perform no temporal feature engineering). Default is empty vector. If calculateAnomalyScores=TRUE, vector must have as many values as anomalyModelFeatures (even if all NULL).
28+
#' @param removeMissingFeatures Remove features with missing values in more than this fraction of runs. Default is 0.5. Only used if calculateAnomalyScores=TRUE.
29+
#' @param anomalyModelFeatureCount Feature selection for anomaly model. Anomaly detection works on the precursor-level and can be much slower if all features used. We will by default filter to the top-100 highest intensity features. This can be adjusted as necessary. To turn feature-selection off, set this value to a high number (e.g. 10000). Only used if calculateAnomalyScores=TRUE.
30+
#' @param runOrder Temporal order of MS runs. Should be a two column data.table with columns `Run` and `Order`, where `Run` matches the run name output by DIA-NN and `Order` is an integer. Used to engineer the temporal features defined in anomalyModelFeatureTemporal.
31+
#' @param n_trees Number of trees to use in isolation forest when calculateAnomalyScores=TRUE. Default is 100.
32+
#' @param max_depth Max tree depth to use in isolation forest when calculateAnomalyScores=TRUE. Default is "auto" which calculates depth as log2(N) where N is the number of runs. Otherwise must be an integer.
33+
#' @param numberOfCores Number of cores for parallel processing anomaly detection model. When > 1, a logfile named 'MSstats_anomaly_model_progress.log' is created to track progress. Only works for Linux & Mac OS. Default is 1.
2534
#' @param ... additional parameters to `data.table::fread`.
2635
#'
2736
#' @return data.frame in the MSstats required format.
@@ -51,29 +60,39 @@
5160
#' output_2_0 = DIANNtoMSstatsFormat(input_2_0, annotation = annot_2_0, MBR = FALSE,
5261
#' use_log_file = FALSE, quantificationColumn = 'auto')
5362
#' head(output_2_0)
54-
DIANNtoMSstatsFormat = function(input, annotation = NULL,
55-
global_qvalue_cutoff = 0.01,
56-
qvalue_cutoff = 0.01,
57-
pg_qvalue_cutoff = 0.01,
58-
useUniquePeptide = TRUE,
59-
removeFewMeasurements = TRUE,
60-
removeOxidationMpeptides = TRUE,
61-
removeProtein_with1Feature = TRUE,
62-
use_log_file = TRUE, append = FALSE,
63-
verbose = TRUE, log_file_path = NULL,
64-
MBR = TRUE,
65-
quantificationColumn = "FragmentQuantCorrected",
66-
...) {
63+
DIANNtoMSstatsFormat = function(
64+
input, annotation = NULL,
65+
global_qvalue_cutoff = 0.01,
66+
qvalue_cutoff = 0.01,
67+
pg_qvalue_cutoff = 0.01,
68+
useUniquePeptide = TRUE,
69+
removeFewMeasurements = TRUE,
70+
removeOxidationMpeptides = TRUE,
71+
removeProtein_with1Feature = TRUE,
72+
MBR = TRUE,
73+
quantificationColumn = "FragmentQuantCorrected",
74+
calculateAnomalyScores=FALSE, anomalyModelFeatures=c(),
75+
anomalyModelFeatureTemporal=c(), removeMissingFeatures=.5,
76+
anomalyModelFeatureCount=100,
77+
runOrder=NULL, n_trees=100, max_depth="auto", numberOfCores=1,
78+
use_log_file = TRUE, append = FALSE,
79+
verbose = TRUE, log_file_path = NULL,
80+
...) {
6781
MSstatsConvert::MSstatsLogsSettings(use_log_file, append, verbose,
6882
log_file_path)
6983

84+
anomalyModelFeatures = .standardizeColnames(anomalyModelFeatures)
85+
7086
input = MSstatsConvert::MSstatsImport(list(input = input),
7187
"MSstats", "DIANN")
88+
7289
input = MSstatsConvert::MSstatsClean(input, MBR = MBR,
7390
quantificationColumn = quantificationColumn,
7491
global_qvalue_cutoff = global_qvalue_cutoff,
7592
qvalue_cutoff = qvalue_cutoff,
76-
pg_qvalue_cutoff = pg_qvalue_cutoff)
93+
pg_qvalue_cutoff = pg_qvalue_cutoff,
94+
calculateAnomalyScores = calculateAnomalyScores,
95+
anomalyModelFeatures = anomalyModelFeatures)
7796
annotation = MSstatsConvert::MSstatsMakeAnnotation(input, annotation)
7897

7998
decoy_filter = list(col_name = "ProteinName",
@@ -87,6 +106,7 @@ DIANNtoMSstatsFormat = function(input, annotation = NULL,
87106

88107
feature_columns = c("PeptideSequence", "PrecursorCharge",
89108
"FragmentIon", "ProductCharge")
109+
# browser()
90110
input = MSstatsConvert::MSstatsPreprocess(
91111
input,
92112
annotation,
@@ -101,19 +121,28 @@ DIANNtoMSstatsFormat = function(input, annotation = NULL,
101121
remove_features_with_few_measurements = removeFewMeasurements,
102122
summarize_multiple_psms = max),
103123
columns_to_fill = list(Fraction = 1,
104-
IsotopeLabelType = "Light"))
124+
IsotopeLabelType = "Light"),
125+
anomaly_metrics = anomalyModelFeatures)
105126
input[, Intensity := ifelse(Intensity == 0, NA, Intensity)]
106-
127+
# browser()
107128
input = MSstatsConvert::MSstatsBalancedDesign(input, feature_columns,
108-
fill_incomplete = FALSE,
129+
fill_incomplete = TRUE,
109130
handle_fractions = FALSE,
110-
remove_few = removeFewMeasurements
131+
remove_few = removeFewMeasurements,
132+
anomaly_metrics = anomalyModelFeatures
111133
)
112-
134+
# browser()
135+
if (calculateAnomalyScores){
136+
input = MSstatsConvert::MSstatsAnomalyScores(
137+
input, anomalyModelFeatures, anomalyModelFeatureTemporal,
138+
removeMissingFeatures, anomalyModelFeatureCount, runOrder, n_trees,
139+
max_depth, numberOfCores)
140+
}
141+
# browser()
113142
msg_final = paste("** Finished preprocessing. The dataset is ready",
114143
"to be processed by the dataProcess function.")
115144
getOption("MSstatsLog")("INFO", msg_final)
116145
getOption("MSstatsMsg")("INFO", msg_final)
117146
getOption("MSstatsLog")("INFO", "\n")
118147
input
119-
}
148+
}

0 commit comments

Comments
 (0)