This is an R Markdown document. Here the developed R-script is described in detail.
Code is represented like this:
# Set working directory
setwd("~\\Adviesprojecten\\2021\\HRMS PoC\\rmarkdown\\")
When user input is necessary, it is represented like this: user input required.
First, required packages must be loaded.
# Load required packages
library("rawrr") # required for reading spectra
library("matrixStats") # required for binning
library("spectacles") # required for SNV normalization
library("pls") # required for MSC normalization
library("signal") # required for Savitzky-Golay smoothing
library("baseline") # required for modified polynomial fitting (smoothing) and Gaussian weighting (smoothing)
library("factoextra") # For PCA
library("ggplot2") # For plotting
library("ggpubr") # For plotting
library("pheatmap") # For hierarchical clustering
library("OrgMassSpecR") # For calculating spectrum similarity
Load sample information from the CSV file generated in the previous Rmarkdown document. The user can select the files of interest here.
# Load data
samples <- read.csv(file = "\\\\nwg\\dfs\\projectdata\\P403817_001\\iQxTT2016_samplefiles.csv")
# Select all samples of September 2018, Lobith
samples <- samples[grep("1809", samples$Chromatogramm.Dateiname), ] # Select all measurements of September 2018
samples <- samples[grep("LOB", samples$Chromatogramm.Dateiname), ] # Select all Lobith measurements
Load TIC’s for all selected measurements. Make sure that the correct path is entered here.
# Create empty list for all TIC's
TIC <- vector(mode = "list", length = nrow(samples))
# Load TIC's for all pilot files
for (i in 1:nrow(samples)) {
file <- paste0("\\\\nwg\\dfs\\projectdata\\P403817_001\\RWSdata\\", samples$filepath[i])
temp <- rawrr::readChromatogram(rawfile = file, type = "tic")
df <- data.frame(as.numeric(temp$times), temp$intensities)
colnames(df) <- c("time", "intensity")
TIC[[i]] <- df
names(TIC)[i] <- samples$Chromatogramm.Dateiname[i]
print(i)
rm(file, temp, df)
}
# Store list in an RDS object
saveRDS(object = TIC, file = "TIC_validation_1month_phenol.RDS")
Load MS spectra for all selected measurements. Make sure that the correct path is entered here.
# Create empty list for MS spectra
spectra <- vector(mode = "list", length = nrow(samples))
names(spectra) <- samples$Chromatogramm.Dateiname
# Read MS spectra for samples
for (k in 1:nrow(samples)) {
file <- paste0("\\\\nwg\\dfs\\projectdata\\P403817_001\\RWSdata\\", samples$filepath[k])
filename <- samples$Chromatogramm.Dateiname[k]
n <- rawrr::readFileHeader(rawfile = file)$`Number of scans`
temp <- rawrr::readSpectrum(rawfile = file, scan = c(1:n))
spectra[[filename]] <- temp
rm(temp, file, filename, n)
print(k)
}
# Store list in an RDS object
saveRDS(object = spectra, file = "MSspectra_validation_1month_phenol.RDS")
Load reference data. User input required in the section below, he/she must provide the reference chromatogram and the file with reference MS spectra of the 8 internal standards.
# Read file with reference MS spectra of the 8 internal standards
IS <- readRDS(file = "IS_MSspectra_pilot.RDS")
# To be filled in by the operator, reference chromatogram used for KRetI alignment and other preprocessing steps (such as MSC)
ref.chromatogram <- "180901_LOB_06"
Define function to retrieve the retention times from internal standards quickly. This function is applied in Chapter 8 - PCA.
getRt <- function(all.ref.IS, chromatogram) {
chrom.rt.IS <- data.frame(matrix(ncol = 8, nrow = 1))
colnames(chrom.rt.IS) <- c(
"toluene-d8", "chlorobenzene-d5", "dichlorobenzene-d4", "naphthalene-d8",
"dibromobenzene-d4", "terbuthylazine-d5", "phenanthrene-d10", "chrysene-d12"
)
chrom.rt.IS$`toluene-d8` <- all.ref.IS[["toluene-d8"]][["Rt.IS"]][which(all.ref.IS[["toluene-d8"]][["Rt.IS"]]$FileName == chromatogram), "rtinmin"]
chrom.rt.IS$`chlorobenzene-d5` <- all.ref.IS[["chlorobenzene-d5"]][["Rt.IS"]][which(all.ref.IS[["chlorobenzene-d5"]][["Rt.IS"]]$FileName == chromatogram), "rtinmin"]
chrom.rt.IS$`dichlorobenzene-d4` <- all.ref.IS[["dichlorobenzene-d4"]][["Rt.IS"]][which(all.ref.IS[["dichlorobenzene-d4"]][["Rt.IS"]]$FileName == chromatogram), "rtinmin"]
chrom.rt.IS$`naphthalene-d8` <- all.ref.IS[["naphthalene-d8"]][["Rt.IS"]][which(all.ref.IS[["naphthalene-d8"]][["Rt.IS"]]$FileName == chromatogram), "rtinmin"]
chrom.rt.IS$`dibromobenzene-d4` <- all.ref.IS[["dibromobenzene-d4"]][["Rt.IS"]][which(all.ref.IS[["dibromobenzene-d4"]][["Rt.IS"]]$FileName == chromatogram), "rtinmin"]
chrom.rt.IS$`terbuthylazine-d5` <- all.ref.IS[["terbuthylazine-d5"]][["Rt.IS"]][which(all.ref.IS[["terbuthylazine-d5"]][["Rt.IS"]]$FileName == chromatogram), "rtinmin"]
chrom.rt.IS$`phenanthrene-d10` <- all.ref.IS[["phenanthrene-d10"]][["Rt.IS"]][which(all.ref.IS[["phenanthrene-d10"]][["Rt.IS"]]$FileName == chromatogram), "rtinmin"]
chrom.rt.IS$`chrysene-d12` <- all.ref.IS[["chrysene-d12"]][["Rt.IS"]][which(all.ref.IS[["chrysene-d12"]][["Rt.IS"]]$FileName == chromatogram), "rtinmin"]
return(chrom.rt.IS)
}
Define function to add classification (in this case of location). This function is applied in Chapter 2.
getClassification <- function(name) {
if (grepl("LOB", name)) {
a <- "LOB"
} else if (grepl("BIM", name)) {
a <- "BIM"
} else if (grepl("REE", name)) {
a <- "REE"
} else if (grepl("WSL", name)) {
a <- "WSL"
} else if (grepl("GWH", name)) {
a <- "GWH"
} else if (grepl("EIJ", name)) {
a <- "EIJ"
} else {
a <- "unknown"
}
return(a)
}
Define function to add classification (in this case of year). This function is applied in Chapter 8 - PCA.
getYear <- function(name) {
if (substr(name, start = 1, stop = 2) == "19") {
a <- "2019"
} else if (substr(name, start = 1, stop = 2) == "20") {
a <- "2020"
} else if (substr(name, start = 1, stop = 2) == "21") {
a <- "2021"
} else if (substr(name, start = 1, stop = 2) == "18") {
a <- "2018"
} else if (substr(name, start = 1, stop = 2) == "17") {
a <- "2017"
} else if (substr(name, start = 1, stop = 2) == "16") {
a <- "2016"
} else {
a <- "unknown"
}
return(a)
}
Retrieve per internal standard (IS) the reference fragments (quantifier and qualifiers) used to find the retention time. In a later stage, this part should be done by the analyst, who provides the m/z of the Q1-Q4 and the script will calculate their relative intensities. The algorithm relies on the specific MS spectrum of the internal standard. In particular, a reference MS-spectrum and expected Rt of the internal standard are defined by the operator. Subsequently, the algorithm defines an Rt window, based on the Rt provided by the user, and uses a spectral similarity function to find which of the recorded spectra in the window matches the one defined by the user. User input required in the sections below, he/she must provide the retention time window where to look for the internal standard.
# Manually define Rt window (in min) to look for the IS. Tested also 2min but there seems to be no difference in performances.
Rt.window <- 3
# Initialise a list which will contain all info about the reference IS
all.ref.IS <- list()
#' Define a filtering function to remove intensities below 5% of the max
low_int <- function(MSspec) {
MSspec <- MSspec[which(MSspec$intensity > max(MSspec$intensity, na.rm = T) * 0.05), ]
}
#' Define a function to normalize the intensities
norm_int <- function(MSintensity) {
MSintensity <- (MSintensity / max(MSintensity, na.rm = T)) * 100
}
Prepare reference m/z of each internal standard.
Toluene-d8
# Retrieve mZ & intensity for the IS
ref.IS.spectrum <- data.frame(mZ = IS[["toluene-d8"]]$mZ, intensity = IS[["toluene-d8"]]$intensity)
ref.IS.spectrum <- low_int(ref.IS.spectrum) # remove m/z which intensity is less than 5% of the max
ref.IS.spectrum$intensity <- norm_int(ref.IS.spectrum$intensity) # normalize intensities
ref.IS.spectrum.top4 <- ref.IS.spectrum[order(ref.IS.spectrum$intensity, decreasing = T), ] # order by decreasing intensity
# Manually select most abundant and diagnostic fragments for IS 1
ref.IS.frag <- ref.IS.spectrum
ref.IS.frag$round.mz <- round(ref.IS.frag$mZ / 0.5) * 0.5
ref.IS.frag$rel.int <- (ref.IS.frag$intensity / max(ref.IS.frag$intensity)) * 100
# Manually define the Rt (in min)
expected.Rt <- 4.7
# Calculate the range
expected.Rt.range <- c(expected.Rt - Rt.window, expected.Rt + Rt.window) # define the Rt range to look for the IS
expected.Rt.range <- expected.Rt.range * 60 # transform in seconds
all.ref.IS[["toluene-d8"]]$ref.IS.frag <- ref.IS.frag
all.ref.IS[["toluene-d8"]]$RtRange <- expected.Rt.range
all.ref.IS[["toluene-d8"]]$expected.Rt <- expected.Rt
For Toluene-d8, this results in the following list:
## mZ intensity round.mz rel.int
## 10 42.06339 12.929432 42 12.929432
## 11 43.06023 7.532936 43 7.532936
## 20 52.04442 5.324995 52 5.324995
## 22 54.06854 7.325357 54 7.325357
## 34 66.06665 6.231184 66 6.231184
## 38 70.07024 13.921788 70 13.921788
## 59 91.02415 5.520571 91 5.520571
## 65 97.09407 5.484367 97 5.484367
## 66 98.09047 100.000000 98 100.000000
## 67 99.12589 11.537655 99 11.537655
## 68 100.10323 60.227808 100 60.227808
## [1] 102 462
## [1] 4.7
Now, the same is done for all other internal standards.
Chlorobenzene-d5
# Retrieve mZ & intensity for the IS
ref.IS.spectrum <- data.frame(mZ = IS[["chlorobenzene-d5"]]$mZ, intensity = IS[["chlorobenzene-d5"]]$intensity)
ref.IS.spectrum <- low_int(ref.IS.spectrum) # remove mz which intensity is less than 5% of the max
ref.IS.spectrum$intensity <- norm_int(ref.IS.spectrum$intensity) # normalize intensities
ref.IS.spectrum.top4 <- ref.IS.spectrum[order(ref.IS.spectrum$intensity, decreasing = T), ] # order by decreasing intensity
# Manually select most abundant and diagnostic fragments for IS 2
ref.IS.frag <- ref.IS.spectrum
ref.IS.frag$round.mz <- round(ref.IS.frag$mZ / 0.5) * 0.5
ref.IS.frag$rel.int <- (ref.IS.frag$intensity / max(ref.IS.frag$intensity)) * 100
# Manually define the Rt (in min)
expected.Rt <- 4.97
# Calculate the range
expected.Rt.range <- c(expected.Rt - Rt.window, expected.Rt + Rt.window) # define the Rt range to look for the IS
expected.Rt.range <- expected.Rt.range * 60 # transform in seconds
all.ref.IS[["chlorobenzene-d5"]]$ref.IS.frag <- ref.IS.frag
all.ref.IS[["chlorobenzene-d5"]]$RtRange <- expected.Rt.range
all.ref.IS[["chlorobenzene-d5"]]$expected.Rt <- expected.Rt
1,4-Dichlorobenzene-d4
# Retrieve mZ & intensity for the IS
ref.IS.spectrum <- data.frame(mZ = IS[["dichlorobenzene-d4"]]$mZ, intensity = IS[["dichlorobenzene-d4"]]$intensity)
ref.IS.spectrum <- low_int(ref.IS.spectrum) # remove mz which intensity is less than 5% of the max
ref.IS.spectrum$intensity <- norm_int(ref.IS.spectrum$intensity) # normalize intensities
ref.IS.spectrum.top4 <- ref.IS.spectrum[order(ref.IS.spectrum$intensity, decreasing = T), ] # order by decreasing intensity
# Manually select most abundant and diagnostic fragments for IS 3
ref.IS.frag <- ref.IS.spectrum
ref.IS.frag$round.mz <- round(ref.IS.frag$mZ / 0.5) * 0.5
ref.IS.frag$rel.int <- (ref.IS.frag$intensity / max(ref.IS.frag$intensity)) * 100
# Manually define the Rt (in min)
expected.Rt <- 5.99
# Calculate the range
expected.Rt.range <- c(expected.Rt - Rt.window, expected.Rt + Rt.window) # define the Rt range to look for the IS
expected.Rt.range <- expected.Rt.range * 60 # transform in seconds
all.ref.IS[["dichlorobenzene-d4"]]$ref.IS.frag <- ref.IS.frag
all.ref.IS[["dichlorobenzene-d4"]]$RtRange <- expected.Rt.range
all.ref.IS[["dichlorobenzene-d4"]]$expected.Rt <- expected.Rt
Naphthalene-d8
# Retrieve mZ & intensity for the IS
ref.IS.spectrum <- data.frame(mZ = IS[["naphthalene-d8"]]$mZ, intensity = IS[["naphthalene-d8"]]$intensity)
ref.IS.spectrum <- low_int(ref.IS.spectrum) # remove mz which intensity is less than 5% of the max
ref.IS.spectrum$intensity <- norm_int(ref.IS.spectrum$intensity) # normalize intensities
ref.IS.spectrum.top4 <- ref.IS.spectrum[order(ref.IS.spectrum$intensity, decreasing = T), ] # order by decreasing intensity
# Manually select most abundant and diagnostic fragments for IS 4
ref.IS.frag <- ref.IS.spectrum
ref.IS.frag$round.mz <- round(ref.IS.frag$mZ / 0.5) * 0.5
ref.IS.frag$rel.int <- (ref.IS.frag$intensity / max(ref.IS.frag$intensity)) * 100
# Manually define the Rt (in min)
expected.Rt <- 6.8
# Calculate the range
expected.Rt.range <- c(expected.Rt - Rt.window, expected.Rt + Rt.window) # define the Rt range to look for the IS
expected.Rt.range <- expected.Rt.range * 60 # transform in seconds
all.ref.IS[["naphthalene-d8"]]$ref.IS.frag <- ref.IS.frag
all.ref.IS[["naphthalene-d8"]]$RtRange <- expected.Rt.range
all.ref.IS[["naphthalene-d8"]]$expected.Rt <- expected.Rt
1,4-Dibromobenzene-d4
# Retrieve mZ & intensity for the IS
ref.IS.spectrum <- data.frame(mZ = IS[["dibromobenzene-d4"]]$mZ, intensity = IS[["dibromobenzene-d4"]]$intensity)
ref.IS.spectrum <- low_int(ref.IS.spectrum) # remove mz which intensity is less than 5% of the max
ref.IS.spectrum$intensity <- norm_int(ref.IS.spectrum$intensity) # normalize intensities
ref.IS.spectrum.top4 <- ref.IS.spectrum[order(ref.IS.spectrum$intensity, decreasing = T), ] # order by decreasing intensity
# Manually select most abundant and diagnostic fragments for IS 5
ref.IS.frag <- ref.IS.spectrum
ref.IS.frag$round.mz <- round(ref.IS.frag$mZ / 0.5) * 0.5
ref.IS.frag$rel.int <- (ref.IS.frag$intensity / max(ref.IS.frag$intensity)) * 100
# Manually define the Rt (in min)
expected.Rt <- 6.83
# Calculate the range
expected.Rt.range <- c(expected.Rt - Rt.window, expected.Rt + Rt.window) # define the Rt range to look for the IS
expected.Rt.range <- expected.Rt.range * 60 # transform in seconds
all.ref.IS[["dibromobenzene-d4"]]$ref.IS.frag <- ref.IS.frag
all.ref.IS[["dibromobenzene-d4"]]$RtRange <- expected.Rt.range
all.ref.IS[["dibromobenzene-d4"]]$expected.Rt <- expected.Rt
Terbuthylazine-d5
# Retrieve mZ & intensity for the IS
ref.IS.spectrum <- data.frame(mZ = IS[["terbuthylazine-d5"]]$mZ, intensity = IS[["terbuthylazine-d5"]]$intensity)
ref.IS.spectrum <- low_int(ref.IS.spectrum) # remove mz which intensity is less than 5% of the max
ref.IS.spectrum$intensity <- norm_int(ref.IS.spectrum$intensity) # normalize intensities
ref.IS.spectrum.top4 <- ref.IS.spectrum[order(ref.IS.spectrum$intensity, decreasing = T), ] # order by decreasing intensity
# Manually select most abundant and diagnostic fragments for IS 6
ref.IS.frag <- ref.IS.spectrum
ref.IS.frag$round.mz <- round(ref.IS.frag$mZ / 0.5) * 0.5
ref.IS.frag$rel.int <- (ref.IS.frag$intensity / max(ref.IS.frag$intensity)) * 100
# Manually define the Rt (in min)
expected.Rt <- 8.7
# Calculate the range
expected.Rt.range <- c(expected.Rt - Rt.window, expected.Rt + Rt.window) # define the Rt range to look for the IS
expected.Rt.range <- expected.Rt.range * 60 # transform in seconds
all.ref.IS[["terbuthylazine-d5"]]$ref.IS.frag <- ref.IS.frag
all.ref.IS[["terbuthylazine-d5"]]$RtRange <- expected.Rt.range
all.ref.IS[["terbuthylazine-d5"]]$expected.Rt <- expected.Rt
Phenanthrene-d10
# Retrieve mZ & intensity for the IS
ref.IS.spectrum <- data.frame(mZ = IS[["phenanthrene-d10"]]$mZ, intensity = IS[["phenanthrene-d10"]]$intensity)
ref.IS.spectrum <- low_int(ref.IS.spectrum) # remove mz which intensity is less than 5% of the max
ref.IS.spectrum$intensity <- norm_int(ref.IS.spectrum$intensity) # normalize intensities
ref.IS.spectrum.top4 <- ref.IS.spectrum[order(ref.IS.spectrum$intensity, decreasing = T), ] # order by decreasing intensity
# Manually select most abundant and diagnostic fragments for IS 7
ref.IS.frag <- ref.IS.spectrum
ref.IS.frag$round.mz <- round(ref.IS.frag$mZ / 0.5) * 0.5
ref.IS.frag$rel.int <- (ref.IS.frag$intensity / max(ref.IS.frag$intensity)) * 100
# Manually define the Rt (in min)
expected.Rt <- 8.87
# Calculate the range
expected.Rt.range <- c(expected.Rt - Rt.window, expected.Rt + Rt.window) # define the Rt range to look for the IS
expected.Rt.range <- expected.Rt.range * 60 # transform in seconds
all.ref.IS[["phenanthrene-d10"]]$ref.IS.frag <- ref.IS.frag
all.ref.IS[["phenanthrene-d10"]]$RtRange <- expected.Rt.range
all.ref.IS[["phenanthrene-d10"]]$expected.Rt <- expected.Rt
Chrysene-d12
# Retrieve mZ & intensity for the IS
ref.IS.spectrum <- data.frame(mZ = IS[["chrysene-d12"]]$mZ, intensity = IS[["chrysene-d12"]]$intensity)
ref.IS.spectrum <- low_int(ref.IS.spectrum) # remove mz which intensity is less than 5% of the max
ref.IS.spectrum$intensity <- norm_int(ref.IS.spectrum$intensity) # normalize intensities
ref.IS.spectrum.top4 <- ref.IS.spectrum[order(ref.IS.spectrum$intensity, decreasing = T), ] # order by decreasing intensity
# Manually select most abundant and diagnostic fragments for IS 8
ref.IS.frag <- ref.IS.spectrum
ref.IS.frag$round.mz <- round(ref.IS.frag$mZ / 0.5) * 0.5
ref.IS.frag$rel.int <- (ref.IS.frag$intensity / max(ref.IS.frag$intensity)) * 100
# Manually define the Rt (in min)
expected.Rt <- 10.78
# Calculate the range
expected.Rt.range <- c(expected.Rt - Rt.window, expected.Rt + Rt.window) # define the Rt range to look for the IS
expected.Rt.range <- expected.Rt.range * 60 # transform in seconds
all.ref.IS[["chrysene-d12"]]$ref.IS.frag <- ref.IS.frag
all.ref.IS[["chrysene-d12"]]$RtRange <- expected.Rt.range
all.ref.IS[["chrysene-d12"]]$expected.Rt <- expected.Rt
Next, search in scans for reference fragments to determine the retention time of the IS in the samples. User input required in the section below, he/she can adjust the tolerance parameters.
Set some parameters
# Set the m/z tolerance to search for the quantifier in the spectra
mz.tolerance <- 0.5
# Set the tolerance for deviation from relative intensity
rel.int.tolerance <- 20
# Set the tolerance for the spectrum similarity score
score.tolerance <- 0.7
Apply loop to all chromatograms
for (k in 1:length(all.ref.IS)) {
# Initialize an empty vector which will contain the name of each chromatogram, IS and corresponding Rt
all.Rt.IS <- data.frame()
# Loop over spectra, a large list containing all the chromatograms that need to be searched
for (n in 1:length(spectra)) {
# Initialize a vector which will contain scans which have the relevant fragment ion (quantifier)
saved.scan.num <- NULL
# Initialise a vector which will contain the SpectrumSimilarity score of the relevant fragment ion
saved.scan.score <- NULL
# Retrieve all scans and their corresponding retention times in seconds.
# This will be used for the Rt window to look for the IS
all.Rt.scans <- NULL
for (y in 1:length(spectra[[n]])) {
Rt.scans <- data.frame(rtinseconds = spectra[[n]][[y]]$rtinseconds, scan = spectra[[n]][[y]]$scan)
all.Rt.scans <- rbind(all.Rt.scans, Rt.scans) # append the scans and Rt to a vector
}
# Retrieve the Rt range as given by the operator
Range <- all.ref.IS[[k]]$RtRange
# We use a +- 2min window, hence it can happen that the range will be below 0.
# Below we put it to 0 should the min of the range be negative
if (min(Range) < 0) {
Range <- c(0, max(Range))
}
# Similarly we check if the max range is out of bounds
if (max(Range) > max(all.Rt.scans$rtinseconds)) {
Range <- c(Range[1], max(all.Rt.scans$rtinseconds))
}
# Retrieve the scans which correspond to the relevant Rt
selected.Rt.scans <- all.Rt.scans[which(all.Rt.scans$rtinseconds >= Range[1] &
all.Rt.scans$rtinseconds <= Range[2]), ]
for (i in min(selected.Rt.scans):max(selected.Rt.scans)) {
# retrieve the first scan of the first chromatogram to be searched for
scan.i <- spectra[[n]][[i]]
# retrieve the m/z of the first scan and round them
scan.i.mz <- data.frame(mZ = scan.i$mZ, intensity = scan.i$intensity)
scan.i.mz <- low_int(scan.i.mz) # remove intensities below 5% of the max
scan.i.mz$intensity <- norm_int(scan.i.mz$intensity) # normalize intensities
# Calculate spectral similarity
require(OrgMassSpecR)
score <- SpectrumSimilarity(cbind(scan.i.mz$mZ, scan.i.mz$intensity),
cbind(all.ref.IS[[k]]$ref.IS.frag$mZ, all.ref.IS[[k]]$ref.IS.frag$intensity),
t = 0.1, print.graphic = F
)
# Save all scores for all scans
# if the similarity between the found spectrum and the reference IS spectra is above the threshold
if (is.na(score)) {}
else if (score >= score.tolerance) {
saved.scan.num <- rbind(saved.scan.num, c(scan.i$scan, "Score equal to or above Tolerance"))
}
else if (score < score.tolerance & score >= 0.6) {
saved.scan.num <- rbind(saved.scan.num, c(scan.i$scan, "Score equal to or above 0.6"))
}
else {}
}
#### Below is to handle instances where the IS cannot be found. Here we introduce two options.####
# 1) We expand the research of the IS to all scans to see if we can find it.
# 2) If we cannot find, then we replace everything with NA
# Option 1)
if (is.null(saved.scan.num)) {
# Search through all scans
for (i in 1:length(spectra[[n]])) {
# retrieve the first scan of the first chromatogram to be searched for
scan.i <- spectra[[n]][[i]]
# retrieve the m/z of the first scan and round them
scan.i.mz <- data.frame(mZ = scan.i$mZ, intensity = scan.i$intensity)
scan.i.mz <- low_int(scan.i.mz) # remove intensities below 5% of the max
scan.i.mz$intensity <- norm_int(scan.i.mz$intensity) # normalize intensities
# Calculate spectral similarity
require(OrgMassSpecR)
score <- SpectrumSimilarity(cbind(scan.i.mz$mZ, scan.i.mz$intensity),
cbind(all.ref.IS[[k]]$ref.IS.frag$mZ, all.ref.IS[[k]]$ref.IS.frag$intensity),
t = 0.1, print.graphic = F
)
# Save all scores for all scans
# if the similarity between the found spectrum and the reference IS spectra is above the threshold
if (is.na(score)) {}
else if (score >= score.tolerance) {
saved.scan.num <- rbind(saved.scan.num, c(
scan.i$scan,
"Score >= Tolerance (All scans have been searched!)"
))
}
else if (score < score.tolerance & score >= 0.6) {
saved.scan.num <- rbind(saved.scan.num, c(
scan.i$scan,
"Score >= 0.6 (All scans have been searched!)"
))
}
else {}
}
}
# 2) if the saved.scan.num is still empty, then we replace it with NAs
if (is.null(saved.scan.num)) {
rt.is <- data.frame(FileName = names(TIC[n]), rtinseconds = NA, rtinmin = NA, scan = NA, alert = NA)
}
# End of options 1 and 2#
else {
# Transform to data.frame and make scan a number
saved.scan.num <- data.frame(scan = saved.scan.num[, 1], alert = saved.scan.num[, 2])
saved.scan.num$scan <- as.numeric(as.character(saved.scan.num$scan))
# Select the scans which match the criteria
sel.scan <- spectra[[n]][saved.scan.num$scan]
# Initialise a vector which will contain the intensities of the mz of the quantifier (main fragment)
# in the selected spectra
sel.scan.int <- NULL
for (m in 1:length(sel.scan)) {
# create a data frame only with the relevant information
sel.scan.m.mz <- data.frame(mZ = sel.scan[[m]]$mZ, intensity = sel.scan[[m]]$intensity)
# find the intensity of the mz which matches the mz of the quantifier used as reference.
# The mz needs to be +- within the defined tolerance. Once its been found, append it to
# the previously initialised vector with all the intensities
sel.scan.int <- rbind(
sel.scan.int,
max(sel.scan.m.mz[which(round(sel.scan.m.mz$mZ, 1) > round(all.ref.IS[[k]]$ref.IS.frag$mZ[1], 1) -
mz.tolerance & round(sel.scan.m.mz$mZ, 1) <
round(all.ref.IS[[k]]$ref.IS.frag$mZ[1], 1) + mz.tolerance), ]$intensity)
)
}
# find the scan number of the spectra with the highest intensity.
sel.scan.int.max <- sel.scan[[which.max(sel.scan.int)]]$scan
# Use the retrieved scan number to find the Rt of the IS in question
rt.is <- spectra[[n]][sel.scan.int.max]
# create a data.frame with all relevant information
rt.is <- data.frame(
FileName = names(TIC[n]), rtinseconds = rt.is[[1]]$rtinseconds, rtinmin = rt.is[[1]]$rtinseconds / 60,
scan = sel.scan.int.max, alert = saved.scan.num$alert[which.max(sel.scan.int)]
)
}
# append data.frame to data.frame initialised at the beginning of the for loop
all.Rt.IS <- rbind(all.Rt.IS, rt.is)
}
# All Rt of the IS are saved in this list, including corresponding scan number and sample name.
all.ref.IS[[k]]$Rt.IS <- all.Rt.IS
# Print number of iteration so that the user knows how far the calculation is
print(k)
}
# Save list to an RDS file
saveRDS(all.ref.IS, file = "Rt_IS_validation.RDS")
For Toluene-d8, this results in the following data frame with filenames, RT’s and scannumbers:
## FileName rtinseconds rtinmin scan alert
## 1 180901_LOB_06 252.700 4.211667 64 Score â<U+0089>¥ Tolerance
## 2 180901_LOB_18 252.098 4.201633 61 Score â<U+0089>¥ Tolerance
## 3 180902_LOB_06 253.100 4.218333 66 Score â<U+0089>¥ Tolerance
## 4 180902_LOB_18 253.504 4.225067 68 Score â<U+0089>¥ Tolerance
## 5 180903_LOB_06 253.718 4.228633 69 Score â<U+0089>¥ Tolerance
## 6 180903_LOB_18 252.904 4.215067 65 Score â<U+0089>¥ Tolerance
To correct for shifts in retention time (Rt), an approach based on the calculation of retention time indexes was implemented. The retention times of the internal standards are used to align the TIC’s. First, the Rt’s of the reference chromatogram are retrieved and an empty list to store the data in is prepared.
# Get Rt's of reference TIC
ref <- getRt(all.ref.IS = all.ref.IS, chromatogram = ref.chromatogram)
# Create empty list for aligned TIC's
TIC_align <- vector(mode = "list", length = length(TIC))
names(TIC_align) <- names(TIC)
Next, the retention indexes are calculated and stored in the list.
for (z in 1:length(TIC)) {
clipb <- TIC[[z]] # select TIC
is <- getRt(all.ref.IS = all.ref.IS, chromatogram = names(TIC[z])) # get the retention times of the IS
is$`toluene-d8` <- NULL # This IS is not used for KRetI alignment
is$`terbuthylazine-d5` <- NULL # This IS is not used for KRetI alignment
is$`chrysene-d12` <- NULL # This IS is not used for KRetI alignment
if (!any(is.na(unlist(is)))) { # Check if all IS have been found by checking if no NA is present
clipb$time_corrected <- NA # define new column where corrected retention time can be stored
for (j in 1:nrow(clipb)) {
rtX <- clipb$time[j]
if (rtX <= ref$`dichlorobenzene-d4`) {
clipb$time_corrected[j] <- ref$`chlorobenzene-d5` +
((ref$`dichlorobenzene-d4` - ref$`chlorobenzene-d5`) /
(is$`dichlorobenzene-d4` - is$`chlorobenzene-d5`)) * (rtX - is$`chlorobenzene-d5`)
} else if (rtX > ref$`chlorobenzene-d5` && rtX <= ref$`naphthalene-d8`) {
clipb$time_corrected[j] <- ref$`naphthalene-d8` +
((ref$`chlorobenzene-d5` - ref$`naphthalene-d8`) /
(is$`chlorobenzene-d5` - is$`naphthalene-d8`)) * (rtX - is$`naphthalene-d8`)
} else if (rtX > ref$`naphthalene-d8` && rtX <= ref$`dibromobenzene-d4`) {
clipb$time_corrected[j] <- ref$`dibromobenzene-d4` +
((ref$`naphthalene-d8` - ref$`dibromobenzene-d4`) /
(is$`naphthalene-d8` - is$`dibromobenzene-d4`)) * (rtX - is$`dibromobenzene-d4`)
} else if (rtX > ref$`dibromobenzene-d4`) {
clipb$time_corrected[j] <- ref$`phenanthrene-d10` +
((ref$`dibromobenzene-d4` - ref$`phenanthrene-d10`) /
(is$`dibromobenzene-d4` - is$`phenanthrene-d10`)) * (rtX - is$`phenanthrene-d10`)
}
}
TIC_align[[names(TIC[z])]] <- clipb[, c("time", "time_corrected", "intensity")]
rm(clipb, is, j, rtX)
} else { # if NA or Inf is present perform this
TIC_align[[names(TIC[z])]] <- NULL
} # close if else statement
print(z) # print number of iteration to track progress
} # close for loop
# Save object
saveRDS(object = TIC_align, file = "TIC_validation_aligned_KRetI.RDS")
rm(TIC, z) # remove variables that we don't need anymore
The TIC’s are binned using a bin size of 0.01 minutes.
delta <- 0.01 # Define bin size here
TIC_binned <- vector(mode = "list", length = length(TIC_align)) # create empty list for binned data
names(TIC_binned) <- names(TIC_align) # copy names
# Perform binning for all TICs
for (q in 1:length(TIC_align)) {
df <- TIC_align[[q]]
RT_min <- round(min(TIC_align[[q]]$time_corrected), digits = 2) # determine minimum RT
RT_max <- round(max(TIC_align[[q]]$time_corrected), digits = 2) # determine maximum RT
breaks <- seq(from = RT_min, to = RT_max, by = delta) # define breaks between min and max
time_bin <- seq(from = (RT_min + delta / 2), to = (RT_max), by = delta) # define bins
intensity_bin <- matrixStats::binMeans(df$intensity, x = df$time_corrected, bx = breaks) # compute sample means in non-overlapping bins
intensity_bin[is.nan(intensity_bin)] <- 0 # set missing values to 0
binned <- data.frame(time_bin, intensity_bin) # create data frame with results
TIC_binned[[q]] <- binned # add data frame to list
rm(df, RT_min, RT_max, breaks, time_bin, intensity_bin, binned) # remove variables that we don't need anymore
} # close for loop
rm(delta) # remove variables that we don't need anymore
# Save object
saveRDS(object = TIC_binned, file = "TIC_validation_KRetI_binned.RDS")
Normalization can be performed in different ways, two options are presented here: standard normal variate (SNV) and multiplicative scatter correction (MSC). Select the one that fits your data best.
SNV normalization performs normalization per individual TIC
TIC_normalized <- vector(mode = "list", length = length(TIC_binned)) # create empty list for normalized data
names(TIC_normalized) <- names(TIC_binned) # copy names
# Perform normalization using SNV
for (q in 1:length(TIC_binned)) {
df <- TIC_binned[[q]]
df$intensity_norm <- spectacles::snv(df$intensity_bin) # perform SNV normalization
normalized <- data.frame(df$time_bin, df$intensity_norm)
colnames(normalized) <- c("time", "intensity")
TIC_normalized[[q]] <- normalized
rm(df, normalized)
} # close for loop
# Change list into data matrix (required for further processing and PCA)
# Find minimum and maximum for each TIC
rt_min <- c()
rt_max <- c()
for (i in 1:length(TIC_normalized)) {
temp_min <- min(TIC_normalized[[i]][["time"]])
temp_max <- max(TIC_normalized[[i]][["time"]])
rt_min <- c(rt_min, temp_min)
rt_max <- c(rt_max, temp_max)
rm(temp_min, temp_max)
} # close for loop
rm(i, q) # clean up environment
# Determine lowest maximum and highest minimum for the cutoff
rt_min <- max(rt_min)
rt_max <- min(rt_max)
# Define empty data frame
data.snv <- data.frame()
# Organise data into matrix (incl. RT cut-off at rt_min and rt_max)
for (j in 1:length(TIC_normalized)) {
clipb <- TIC_binned[[j]]
selection <- which(round(clipb$time, digits = 3) >= rt_min & round(clipb$time, digits = 3) <= rt_max)
clipb <- clipb[selection, ]
clipb <- as.data.frame(t(clipb))
colnames(clipb) <- as.character(clipb["time", ])
clipb <- clipb[-1, ]
rownames(clipb) <- names(TIC_normalized[j])
data.snv <- rbind(data.snv, clipb)
rm(clipb)
} # close for loop
rm(j, rt_max, rt_min, TIC_normalized) # clean up environment
# Save object
saveRDS(object = data.snv, file = "TIC_validation_KRetI_binned_SNV.RDS")
MSC normalization performs normalization alongside a reference chromatogram
# Create data matrix which is required for MSC
# Find highest minimum and lowest maximum for the cutoff (this is done to be able to make a filled matrix for the PCA)
rt_min <- c()
rt_max <- c()
# Find minimum and maximum for each TIC
for (i in 1:length(TIC_binned)) {
temp_min <- min(TIC_binned[[i]][["time_bin"]])
temp_max <- max(TIC_binned[[i]][["time_bin"]])
rt_min <- c(rt_min, temp_min)
rt_max <- c(rt_max, temp_max)
rm(temp_min, temp_max)
}
rm(i)
# Determine lowest maximum and highest minimum for the cutoff
rt_min <- max(rt_min)
rt_max <- min(rt_max)
# Define empty data frame
data.msc <- data.frame()
# Organise data for MSC (incl. RT cut-off at rt_min and rt_max)
for (j in 1:length(TIC_binned)) {
clipb <- TIC_binned[[j]]
selection <- which(round(clipb$time, digits = 3) >= rt_min & round(clipb$time, digits = 3) <= rt_max)
clipb <- clipb[selection, ]
clipb <- as.data.frame(t(clipb))
colnames(clipb) <- as.character(clipb["time", ])
clipb <- clipb[-1, ]
rownames(clipb) <- names(TIC_binned[j])
data.msc <- rbind(data.msc, clipb)
rm(clipb)
} # close for loop
rm(j, rt_max, rt_min)
ref <- as.numeric(data.msc[ref.chromatogram, ]) # get values of reference chromatogram
# Perform normalization using MSC
TIC_normalized <- as.data.frame(pls::msc(X = as.matrix(data.msc), reference = ref))
# Save object
saveRDS(object = TIC_normalized, file = "TIC_validation_KRetI_binned_MSC.RDS")
Smoothing can be done in different ways as well. Three options are presented here: Savitzky-Golay smoothing, modified polynomial fitting and local windows & Gaussian weighting. Select the one that fits your data best.
Savitzky-Golay smoothing
# apply SGolay smoothing
TIC_smoothed <- as.data.frame(t(apply(TIC_normalized, 1, signal::sgolayfilt)))
colnames(TIC_smoothed) <- colnames(TIC_normalized) # add chromatogram names
# Save object
saveRDS(object = TIC_smoothed, file = "TIC_validation_KRetI_binned_SGolay.RDS")
Modified polynomial fitting
# Apply modified polynomial fitting
polyfit <- as.data.frame(baseline::baseline.modpolyfit(spectra = as.matrix(TIC_normalized))[[2]])
colnames(polyfit) <- colnames(TIC_normalized)
rownames(polyfit) <- rownames(TIC_normalized)
# Save object
saveRDS(file = "TIC_validation_KRetI_binned_polynomial.RDS", object = polyfit)
Local windows & Gaussian weighting
# Apply local window & Gaussian weighting
locsmooth <- as.data.frame(baseline::baseline.medianWindow(spectra = as.matrix(TIC_normalized), hwm = 0.05)[[2]])
# Save object
saveRDS(file = "TIC_validation_KRetI_binned_localwindow.RDS", object = locsmooth)
Another option is to calculate 1st and 2nd derivatives of the data. The code is presented here, you can test whether it is beneficial for your data.
First derivative
# Calculate first derivative
d1 <- as.data.frame(t(diff(t(TIC_smoothed), differences = 1)))
# Save object
saveRDS(object = as.data.frame(d1), file = "TIC_validation_KRetI_binned_1stderiv.RDS")
Second derivative
# Calculate second derivative
d2 <- as.data.frame(t(diff(t(TIC_smoothed), differences = 2)))
# Save object
saveRDS(object = as.data.frame(d2), file = "TIC_validation_KRetI_binned_2ndderiv.RDS")
A PCA can be used to reduce the dimensions of the data. The code is presented here. First, the data is prepared by removing everything before the solvent peak (t0) and removing the internal standard peaks. If information from the blank samples is available, impurities must be removed as well.
Data preparation
# Load data
data.pca <- readRDS("TIC_validation_KRetI_binned_MSC_1month_phenol.RDS")
# Remove everything before t0.cutoff (to be defined by the operator) min to exclude t0
t0.cutoff <- 4.465
data.pca <- data.pca[, -which(as.numeric(colnames(data.pca)) < t0.cutoff)]
# Remove internal standard peaks (range of 0.2 min around APEX)
## Define reference chromatogram (same as for KRetI alignment)
ref <- getRt(all.ref.IS = all.ref.IS, chromatogram = ref.chromatogram)
range <- 0.2
# Remove IS peaks
data.pca <- data.pca[, -which(as.numeric(colnames(data.pca)) > (ref$`chlorobenzene-d5`[1] - range) &
as.numeric(colnames(data.pca)) < (ref$`chlorobenzene-d5`[1] + range))] # Remove chlorobenzene-d5
data.pca <- data.pca[, -which(as.numeric(colnames(data.pca)) > (ref$`dichlorobenzene-d4`[1] - range) &
as.numeric(colnames(data.pca)) < (ref$`dichlorobenzene-d4`[1] + range))] # Remove 1,4-dichlorobenzene-d4
data.pca <- data.pca[, -which(as.numeric(colnames(data.pca)) > (ref$`naphthalene-d8`[1] - range) &
as.numeric(colnames(data.pca)) < (ref$`naphthalene-d8`[1] + range))] # Remove naphthalene-d8
data.pca <- data.pca[, -which(as.numeric(colnames(data.pca)) > (ref$`dibromobenzene-d4`[1] - range) &
as.numeric(colnames(data.pca)) < (ref$`dibromobenzene-d4`[1] + range))] # Remove 1,4-dibromobenzene-d4
data.pca <- data.pca[, -which(as.numeric(colnames(data.pca)) > (ref$`phenanthrene-d10`[1] - range) &
as.numeric(colnames(data.pca)) < (ref$`phenanthrene-d10`[1] + range))] # Remove phenanthrene-d10
data.pca <- data.pca[, -which(as.numeric(colnames(data.pca)) > (ref$`terbuthylazine-d5`[1] - range) &
as.numeric(colnames(data.pca)) < (ref$`terbuthylazine-d5`[1] + range))] # Remove terbuthylazine-d5
Perform PCA
pc <- prcomp(data.pca, scale. = T) # perform PCA
data.pca$class <- sapply(rownames(data.pca), getClassification) # add class to data
data.pca$year <- sapply(rownames(data.pca), getYear) # add year to data
Create scree plot
factoextra::fviz_eig(pc, addlabels = TRUE)
Create biplot
factoextra::fviz_pca_biplot(pc,
select.var = list(contrib = 15),
col.var = "black", alpha.var = 0.2,
repel = FALSE,
habillage = data.pca$year,
ggtheme = theme_minimal(),
title = "PCA biplot",
labelsize = 2
)
HCA can be used to generate clusters in the data. The code is presented here. First, the data is prepared by removing everything before the solvent peak (t0) and removing the internal standard peaks. If information from the blank samples is available, impurities must be removed as well. User input required in the section below, he/she can define the t0.cutoff and remove other irrelevant peaks if desired.
Data preparation
# Load data
data.hca <- readRDS("TIC_validation_KRetI_binned_MSC_1month_phenol.RDS")
# Remove everything before t0.cutoff (to be defined by the operator) min to exclude t0
t0.cutoff <- 4.465
data.hca <- data.hca[, -which(as.numeric(colnames(data.hca)) < t0.cutoff)]
# Remove internal standard peaks (range of 0.2 min around APEX)
## Define reference chromatogram (same as for KRetI alignment)
ref <- getRt(all.ref.IS = all.ref.IS, chromatogram = ref.chromatogram)
range <- 0.2
# Remove IS peaks
data.hca <- data.hca[, -which(as.numeric(colnames(data.hca)) > (ref$`chlorobenzene-d5`[1] - range) &
as.numeric(colnames(data.hca)) < (ref$`chlorobenzene-d5`[1] + range))] # Remove chlorobenzene-d5
data.hca <- data.hca[, -which(as.numeric(colnames(data.hca)) > (ref$`dichlorobenzene-d4`[1] - range) &
as.numeric(colnames(data.hca)) < (ref$`dichlorobenzene-d4`[1] + range))] # Remove 1,4-dichlorobenzene-d4
data.hca <- data.hca[, -which(as.numeric(colnames(data.hca)) > (ref$`naphthalene-d8`[1] - range) &
as.numeric(colnames(data.hca)) < (ref$`naphthalene-d8`[1] + range))] # Remove naphthalene-d8
data.hca <- data.hca[, -which(as.numeric(colnames(data.hca)) > (ref$`dibromobenzene-d4`[1] - range) &
as.numeric(colnames(data.hca)) < (ref$`dibromobenzene-d4`[1] + range))] # Remove 1,4-dibromobenzene-d4
data.hca <- data.hca[, -which(as.numeric(colnames(data.hca)) > (ref$`phenanthrene-d10`[1] - range) &
as.numeric(colnames(data.hca)) < (ref$`phenanthrene-d10`[1] + range))] # Remove phenanthrene-d10
data.hca <- data.hca[, -which(as.numeric(colnames(data.hca)) > (ref$`terbuthylazine-d5`[1] - range) &
as.numeric(colnames(data.hca)) < (ref$`terbuthylazine-d5`[1] + range))] # Remove terbuthylazine-d5
# According to Schollee et al. 2018: divide by maximum values
max_row_pos <- apply(data.hca, 1, max)
normmax.pos <- data.hca[, 1:ncol(data.hca)] / max_row_pos
# If desired, values with low intensities (below threshold) can be turned into 0, this can also be done the other way around (exclude high intensities)
# treshold <- 0.3
# normmax.pos[normmax.pos < threshold] <- 0
Perform HCA
pheatmap::pheatmap(normmax.pos,
scale = "none",
clustering_distance_rows = "euclidean",
show_rownames = T,
border_color = NA,
cluster_cols = FALSE,
main = "Hierarchical clustering of GC-MS analyses (Euclidean distance, max normalized)",
fontsize_col = 4,
width = 35,
height = 20
)
The plot generated here is too small. It can be zoomed in when saved to a .png file, which can be done by simply adding a filename to the pheatmap() function.
When a peak of interest has been found, the corresponding MS scan can be retrieved using the following code. Here, a .txt file will be generated which can be uploaded on MassBank Europe and MassBank of North America. User input required in the section below, he/she must select the chromatogram and scan of interest.
Get scan of interest
# Define chromatogram of interest
chrom.of.interest <- "180911_LOB_18_SS" # To be filled in by the operator
# Define (corrected) retention time of interest, retrieved from HCA plot
rt.of.interest <- 5.494 # To be filled in by the operator
# Find original retention time which belongs to the rt.of.interest
rt.original <- TIC_align[[chrom.of.interest]]$time[which.min(abs(TIC_align[[chrom.of.interest]]$time_corrected - rt.of.interest))]
# Convert retention time from minutes to seconds
rt.original.sec <- rt.original * 60
# Find MS spectrum which belongs to this retention time
# Retrieve all scans and their corresponding retention times in seconds. This will be used for the Rt window to look for the IS
all.Rt.scans <- NULL
for (y in 1:length(spectra[[chrom.of.interest]])) {
Rt.scans <- data.frame(rtinseconds = spectra[[chrom.of.interest]][[y]]$rtinseconds, scan = spectra[[chrom.of.interest]][[y]]$scan)
all.Rt.scans <- rbind(all.Rt.scans, Rt.scans) # append the scans and Rt to a vector
}
# Retrieve scan number of interest
scan.of.interest <- all.Rt.scans$scan[which.min(abs(all.Rt.scans$rtinseconds - rt.original.sec))]
# Retrieve mass spectrum
MS <- data.frame(spectra[[chrom.of.interest]][[scan.of.interest]][["mZ"]], spectra[[chrom.of.interest]][[scan.of.interest]][["intensity"]])
# Make intensities relative and clean up data frame
MS$relativeIntensity <- MS$spectra..chrom.of.interest....scan.of.interest.....intensity... / max(MS$spectra..chrom.of.interest....scan.of.interest.....intensity...) * 999
MS$spectra..chrom.of.interest....scan.of.interest.....intensity... <- NULL
colnames(MS) <- c("mZ", "relativeIntensity")
Export scan to .txt ready for uploading to MassBank Europe and MoNA
write.table(MS, file = paste0(chrom.of.interest, "_scan", scan.of.interest, ".txt"), row.names = FALSE)
# Export only top 20 peaks
MS.top20 <- data.table::setorder(MS, -relativeIntensity)[1:20, ]
write.table(MS.top20, file = paste0(chrom.of.interest, "_scan", scan.of.interest, "_top20.txt"), row.names = FALSE)
## mZ relativeIntensity
## 1 94.03635 999.00000
## 2 66.08657 501.47166
## 3 65.08504 324.92512
## 4 39.07484 218.90578
## 5 40.08612 112.41123
## 6 55.06335 92.58730
## 7 63.05878 82.65805
## 8 95.07397 70.97227
## 9 38.06773 69.10941
## 10 43.06993 61.67833
## 11 50.06155 58.62468
## 12 51.06593 52.08628
## 13 62.06422 47.40763
## 14 41.09200 40.79415
## 15 67.08057 38.65261
## 16 53.05008 34.30169
## 17 42.08316 33.90247
## 18 64.08242 33.35000
## 19 47.07994 33.05748
## 20 61.06101 32.81697