Skip to content

Commit e4d8966

Browse files
committed
Require mzmine_annotations; inner-join filter
- Error on NULL/missing mzmine_annotations - Drop quant-only unmatched features (no mz_rt fallback) - Log retained feature IDs after join - Update tests, vignette, and roxygen for filtering + MSI Level 2 scope - Remove unused .cleanRawMZMine metadata requirements removeProtein_with1Feature default unchanged (FALSE).
1 parent 53fed81 commit e4d8966

8 files changed

Lines changed: 136 additions & 124 deletions

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,4 @@ Collate:
8383
'utils_fractions.R'
8484
'utils_logging.R'
8585
'utils_shared_peptides.R'
86-
VignetteBuilder: knitr
86+
VignetteBuilder: knitr

R/clean_MZMine.R

Lines changed: 44 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,21 @@
22
#'
33
#' Operates on the column names produced by MZMine after MSstatsConvert's
44
#' internal column-name standardization (spaces collapsed and dots removed):
5-
#' "row ID" becomes `rowID`, "row m/z" becomes `rowmz`, "row retention time"
6-
#' becomes `rowretentiontime`, and each "<sample> Peak area" becomes
5+
#' "row ID" becomes `rowID`, and each "<sample> Peak area" becomes
76
#' `<standardized-sample>Peakarea`.
87
#'
98
#' @param msstats_object an object of class `MSstatsMZMineFiles`.
10-
#' @param mzmine_annotations optional `data.frame` of MZMine spectral-library
11-
#' annotations with columns `id`, `compound_name`, `score`. When supplied,
12-
#' the highest-scoring `compound_name` per feature is used as `ProteinName`.
13-
#' Features without a matching annotation row fall back to an mz_rt string
14-
#' `paste0(round(mz, 4), "_", round(rt, 2))`. When `NULL`, every feature
15-
#' uses the mz_rt fallback.
9+
#' @param mzmine_annotations `data.frame` of MZMine spectral-library
10+
#' annotations with columns `id`, `compound_name`, `score`. Required;
11+
#' passing `NULL` raises an error. The highest-scoring `compound_name`
12+
#' per feature is used as `ProteinName`, and features in the quant
13+
#' table with no matching annotation row are dropped from the output.
14+
#' These are MSI Level 2 annotations (putative identification via
15+
#' MS/MS spectral matching). See the public `MZMinetoMSstatsFormat`
16+
#' docstring for the full scope discussion.
1617
#' @return data.table
1718
#' @keywords internal
18-
.cleanRawMZMine <- function(msstats_object, mzmine_annotations = NULL) {
19+
.cleanRawMZMine <- function(msstats_object, mzmine_annotations) {
1920
ProteinName = PeptideSequence = Intensity = Run = NULL
2021
PrecursorCharge = FragmentIon = ProductCharge = NULL
2122
id = score = compound_name = i.compound_name = NULL
@@ -31,47 +32,45 @@
3132
"columns named '<run> Peak area' (e.g. 'sampleA.mzML Peak area').")
3233
}
3334
id_col <- "rowID"
34-
mz_col <- "rowmz"
35-
rt_col <- "rowretentiontime"
36-
required_meta <- c(id_col, mz_col, rt_col)
35+
required_meta <- id_col
3736
missing_meta <- setdiff(required_meta, colnames(mz_input))
3837
if (length(missing_meta) > 0) {
39-
stop("Missing required MZMine metadata column(s) (expected 'row ID', ",
40-
"'row m/z', 'row retention time'). After standardization, ",
41-
"looked for: ", paste(missing_meta, collapse = ", "), ".")
38+
stop("Missing required MZMine metadata column (expected 'row ID'). ",
39+
"After standardization, looked for: ",
40+
paste(missing_meta, collapse = ", "), ".")
4241
}
4342

44-
mz_rt_fallback <- paste0(round(mz_input[[mz_col]], 4), "_",
45-
round(mz_input[[rt_col]], 2))
46-
mz_input[, ProteinName := mz_rt_fallback]
47-
48-
if (!is.null(mzmine_annotations)) {
49-
feature_to_compound <- data.table::as.data.table(mzmine_annotations)
50-
required_ann <- c("id", "compound_name", "score")
51-
missing_ann <- setdiff(required_ann, colnames(feature_to_compound))
52-
if (length(missing_ann) > 0) {
53-
stop("mzmine_annotations is missing required column(s): ",
54-
paste(missing_ann, collapse = ", "), ".")
55-
}
56-
feature_to_compound[, score := suppressWarnings(as.numeric(score))]
57-
if (anyNA(feature_to_compound$score)) {
58-
stop("The 'score' column in the mzmine annotations file must contain numeric values.")
59-
}
60-
# Sort by id ascending and score descending so the highest-scoring
61-
# annotation per id is the first row in each group.
62-
data.table::setorder(feature_to_compound, id, -score)
63-
# Collapse to one row per id (the highest-scoring). data.table's
64-
# unique() with a 'by' arg keeps the first row per group, which after
65-
# the sort above is the highest-scoring annotation.
66-
feature_to_compound <- unique(feature_to_compound, by = "id")
67-
# Join: unmatched mz_input rows keep the mz_rt_fallback ProteinName
68-
# set above.
69-
mz_input[
70-
feature_to_compound,
71-
ProteinName := i.compound_name,
72-
on = setNames("id", id_col)
73-
]
43+
if (is.null(mzmine_annotations)) {
44+
stop("mzmine_annotations is required. Pass a data.frame with ",
45+
"columns 'id', 'compound_name', 'score'.")
46+
}
47+
feature_to_compound <- data.table::as.data.table(mzmine_annotations)
48+
required_ann <- c("id", "compound_name", "score")
49+
missing_ann <- setdiff(required_ann, colnames(feature_to_compound))
50+
if (length(missing_ann) > 0) {
51+
stop("mzmine_annotations is missing required column(s): ",
52+
paste(missing_ann, collapse = ", "), ".")
7453
}
54+
feature_to_compound[, score := suppressWarnings(as.numeric(score))]
55+
if (anyNA(feature_to_compound$score)) {
56+
stop("The 'score' column in the mzmine annotations file must contain numeric values.")
57+
}
58+
data.table::setorder(feature_to_compound, id, -score)
59+
feature_to_compound <- unique(feature_to_compound, by = "id")
60+
# Inner-join filter: drop quant rows with no matching annotation.
61+
mz_input[
62+
feature_to_compound,
63+
ProteinName := i.compound_name,
64+
on = setNames("id", id_col)
65+
]
66+
mz_input <- mz_input[!is.na(ProteinName)]
67+
68+
retained_ids <- feature_to_compound$id
69+
retained_msg <- paste0("** MZMine: retained ", length(retained_ids),
70+
" feature(s) after annotation join: ",
71+
paste(retained_ids, collapse = ", "))
72+
getOption("MSstatsLog")("INFO", retained_msg)
73+
getOption("MSstatsMsg")("INFO", retained_msg)
7574

7675
mz_input[, PeptideSequence := as.character(get(id_col))]
7776

R/converters_MZMinetoMSstatsFormat.R

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,19 @@
1111
#' trailing `"Peakarea"` suffix removed. For example, a quant-file column
1212
#' `"sampleA.mzML Peak area"` becomes `"sampleAmzML"` after standardization,
1313
#' so the corresponding `Run` value must be `sampleAmzML`.
14-
#' @param mzmine_annotations optional `data.frame` of MZMine spectral-library
15-
#' annotations with columns `id`, `compound_name`, `score`. When supplied,
16-
#' the highest-scoring `compound_name` per feature is used as `ProteinName`;
17-
#' features without a matching annotation row fall back to an mz_rt string
18-
#' `paste0(round(mz, 4), "_", round(rt, 2))`. When `NULL`, every feature
19-
#' uses the mz_rt fallback.
14+
#' @param mzmine_annotations `data.frame` of MZMine spectral-library
15+
#' annotations with columns `id`, `compound_name`, `score`. Required:
16+
#' the highest-scoring `compound_name` per feature is used as
17+
#' `ProteinName`, and features in the quant table with no matching
18+
#' annotation row are dropped from the output.
19+
#'
20+
#' These are MSI Level 2 annotations (putative identification via
21+
#' MS/MS spectral matching against a reference library). Higher-
22+
#' confidence Level 1 identifications require pure reference standards
23+
#' and are out of scope here. Lower-confidence annotations such as
24+
#' Level 3 (SIRIUS, MS2Query) or Level 4 (molecular formula via
25+
#' CANOPUS) are not currently supported -- features without a Level 2
26+
#' annotation row are filtered out.
2027
#'
2128
#' @return data.table in the MSstats required format.
2229
#'
@@ -39,7 +46,7 @@
3946
MZMinetoMSstatsFormat = function(
4047
input,
4148
annotation = NULL,
42-
mzmine_annotations = NULL,
49+
mzmine_annotations,
4350
removeProtein_with1Feature = FALSE,
4451
summaryforMultipleRows = max,
4552
use_log_file = TRUE,
@@ -50,6 +57,11 @@ MZMinetoMSstatsFormat = function(
5057
MSstatsConvert::MSstatsLogsSettings(use_log_file, append, verbose,
5158
log_file_path)
5259

60+
if (missing(mzmine_annotations) || is.null(mzmine_annotations)) {
61+
stop("mzmine_annotations is required. Pass a data.frame with ",
62+
"columns 'id', 'compound_name', 'score'.")
63+
}
64+
5365
input = MSstatsConvert::MSstatsImport(list(input = input),
5466
"MSstats", "MZMine", ...)
5567
input = MSstatsConvert::MSstatsClean(

inst/tinytest/test_converters_MZMinetoMSstatsFormat.R

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ output = MZMinetoMSstatsFormat(input, annotation = annot,
1515
use_log_file = FALSE)
1616
output_dt = data.table::as.data.table(output)
1717

18-
# Basic structure: 6 features x 4 runs = 24 rows, 11 standard columns
18+
# Basic structure: 4 annotated features x 4 runs = 16 rows, 11 standard columns
19+
# Features 4 and 5 have no annotation row and are dropped by the inner join.
1920
expect_equal(ncol(output), 11)
20-
expect_equal(nrow(output), 24)
21+
expect_equal(nrow(output), 16)
2122
expect_true("Run" %in% colnames(output))
2223
expect_true("ProteinName" %in% colnames(output))
2324
expect_true("PeptideSequence" %in% colnames(output))
@@ -53,22 +54,17 @@ expect_equal(as.character(feature3_proteins), "Lactate")
5354
feature6_proteins = unique(output_dt[PeptideSequence == "6", ProteinName])
5455
expect_equal(as.character(feature6_proteins), "Caffeine")
5556

56-
# Features without annotation rows fall back to the mz_rt string
57-
feature4_proteins = unique(output_dt[PeptideSequence == "4", ProteinName])
58-
expect_equal(as.character(feature4_proteins), "489.334_7.89")
59-
feature5_proteins = unique(output_dt[PeptideSequence == "5", ProteinName])
60-
expect_equal(as.character(feature5_proteins), "555.447_9.1")
57+
# Features absent from the annotations file are filtered out (no mz_rt fallback)
58+
expect_false("4" %in% as.character(output_dt$PeptideSequence))
59+
expect_false("5" %in% as.character(output_dt$PeptideSequence))
60+
expect_false(any(as.character(output_dt$ProteinName) %in%
61+
c("489.334_7.89", "555.447_9.1")))
6162

6263
# Zero-intensity input cells are converted to NA in output
63-
# Feature 3 sampleB = 0 -> NA
64+
# Feature 3 sampleB = 0 -> NA (feature 3 is annotated as Lactate)
6465
feature3_sampleB_int = output_dt[PeptideSequence == "3" & Run == "sampleBmzML",
6566
Intensity]
6667
expect_true(is.na(feature3_sampleB_int))
67-
# Feature 5 sampleB/C/D all = 0 -> NA
68-
feature5_zero_ints = output_dt[PeptideSequence == "5" &
69-
Run %in% c("sampleBmzML", "sampleCmzML", "sampleDmzML"),
70-
Intensity]
71-
expect_true(all(is.na(feature5_zero_ints)))
7268

7369
# Annotation merges correctly: sampleA is Control rep 1
7470
sampleA_cond = unique(output_dt[Run == "sampleAmzML", Condition])
@@ -86,27 +82,24 @@ feature2_sampleC_int = output_dt[PeptideSequence == "2" & Run == "sampleCmzML",
8682
Intensity]
8783
expect_equal(as.numeric(feature2_sampleC_int), 5200)
8884

89-
# Without mzmine_annotations -------------------------------------------------
90-
output_nolib = MZMinetoMSstatsFormat(input, annotation = annot,
91-
mzmine_annotations = NULL,
92-
use_log_file = FALSE)
93-
output_nolib_dt = data.table::as.data.table(output_nolib)
94-
95-
# Every ProteinName is the mz_rt fallback string
96-
expect_equal(ncol(output_nolib), 11)
97-
expect_equal(nrow(output_nolib), 24)
98-
expected_mz_rt = c("123.056_1.23", "245.129_3.45", "367.201_5.67",
99-
"489.334_7.89", "555.447_9.1", "123.056_1.45")
100-
expect_equal(
101-
sort(unique(as.character(output_nolib_dt$ProteinName))),
102-
sort(expected_mz_rt)
85+
# mzmine_annotations is mandatory --------------------------------------------
86+
# Passing NULL must raise an error (no silent mz_rt fallback)
87+
expect_error(
88+
MZMinetoMSstatsFormat(input, annotation = annot,
89+
mzmine_annotations = NULL,
90+
use_log_file = FALSE),
91+
"mzmine_annotations is required"
92+
)
93+
# Omitting the argument entirely must also raise an error
94+
expect_error(
95+
MZMinetoMSstatsFormat(input, annotation = annot,
96+
use_log_file = FALSE),
97+
"mzmine_annotations is required"
10398
)
104-
# Compound names from the library must not leak in
105-
expect_false(any(as.character(output_nolib_dt$ProteinName) %in%
106-
c("Caffeine", "GlucoseHigh", "GlucoseLow", "Lactate")))
10799

108100
# removeProtein_with1Feature filters non-Caffeine proteins -------------------
109-
# Caffeine has 2 features (PeptideSequence "1" and "6"); all others have 1.
101+
# Of the annotated features (1, 2, 3, 6), Caffeine has 2 (IDs 1 and 6);
102+
# Lactate and Glucose each have 1.
110103
output_filtered = MZMinetoMSstatsFormat(input, annotation = annot,
111104
mzmine_annotations = mzmine_ann,
112105
removeProtein_with1Feature = TRUE,

man/MSstatsClean.Rd

Lines changed: 9 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/MZMinetoMSstatsFormat.Rd

Lines changed: 14 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/dot-cleanRawMZMine.Rd

Lines changed: 10 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)