False discovery rate control
Over a whole run, the per-spectrum rankings of the scoring pages (SEQUEST-like and Andromeda-like) accumulate into a long list of peptide spectrum matches (PSMs), each carrying a score. The score states how well a spectrum and a candidate agree. It says nothing about how often agreement this good arises by chance, so a score alone cannot separate true identifications from false ones. The search database never covers every peptide actually present in the sample, so for some spectra the true answer was not among the candidates and the best available match is wrong. The instrument also sometimes selects precursor ions that were never peptides. And in a large search space, some wrong candidate will score well on some spectrum simply because so many candidates were tried.
The standard way to put numbers on this problem is the decoy database. Reversed or shuffled sequences are searched alongside the real ones, and any PSM that lands on a decoy is wrong by construction. Counting decoy hits above a score threshold therefore estimates how many target hits above the same threshold are wrong, because a wrong target assignment behaves like a decoy assignment.
Three quantities are built on that count. The global false discovery rate
(FDR) of an accepted PSM list is the fraction of the list that is wrong. The
q-value attached to a score is the FDR of the list you get by accepting
everything at that score or better, made monotone so that relaxing the
threshold never claims a lower error. It is the number used for cutoffs, and
"filter at 1 percent FDR" means keeping PSMs with q at or below 0.01. The
posterior error probability (PEP) asks the question for a single PSM: how
likely is this one match wrong. Tools in this space include Qvality, which
estimates PEPs and q-values by regression on decoy counts, and Percolator,
which rescores PSMs with an iteratively trained SVM and appears again at the
end of this page. The FDRControl module implements the counting and
fitting machinery for q-values.
Simulating a run's worth of PSM scores
The scoring pages worked on one spectrum, so for this page we simulate the score list of a whole run. A seeded generator and a Box-Muller helper draw normally distributed scores on an Andromeda-like scale from 0 to about 120.
open BioFSharp.Mz
let rng = System.Random 42
// Box-Muller transform: two uniform draws become one normal draw.
let gaussian mean sd =
let u1 = 1.0 - rng.NextDouble()
let u2 = rng.NextDouble()
mean + sd * (sqrt (-2.0 * log u1) * cos (2.0 * System.Math.PI * u2))
// Decoy PSMs are wrong by construction and score low.
let decoyScores = Array.init 5000 (fun _ -> max 0.0 (gaussian 25.0 8.0))
// Target PSMs are a mixture. 60 percent are correct identifications and
// score high, 40 percent are wrong assignments. The wrong targets draw from
// the same low distribution as the decoys, because that equivalence is the
// premise of the method: the decoy score distribution estimates how wrong
// target assignments score.
let correctScores = Array.init 3000 (fun _ -> max 0.0 (gaussian 70.0 15.0))
let wrongScores = Array.init 2000 (fun _ -> max 0.0 (gaussian 25.0 8.0))
let targetScores = Array.append correctScores wrongScores
let psms =
Array.append
(targetScores |> Array.map (fun s -> FDRControl.createQValueInput s false))
(decoyScores |> Array.map (fun s -> FDRControl.createQValueInput s true))
let median xs =
let sorted = Array.sort xs
sorted.[Array.length sorted / 2]
printfn "PSMs: %i targets, %i decoys" targetScores.Length decoyScores.Length
printfn "median target score: %.1f median decoy score: %.1f"
(median targetScores) (median decoyScores)
|
createQValueInput builds the QValueInput record the module works with, a
score plus an IsDecoy flag. The functions below take the data together
with three accessors, one for the decoy flag and one score projection each
for decoys and targets. Here both projections read the same field.
Computing q-values by counting decoys
calculateQValueStorey implements the direct counting estimate and returns
a function from score to q-value. Internally it walks the scores from best
to worst and accumulates decoy and target counts. At each score the raw
estimate is accumulated decoys divided by accumulated targets. A second pass
makes the sequence monotone, so q never decreases as the score drops, and a
linear interpolation over the resulting support turns the table into a
function usable at any score.
let storeyQ =
FDRControl.calculateQValueStorey
psms
(fun x -> x.IsDecoy)
(fun x -> x.Score)
(fun x -> x.Score)
let probes = [90.; 70.; 55.; 40.]
for s in probes do
printfn "score %5.0f q = %.5f" s (storeyQ s)
|
No decoy in this simulated run scores above 55, so down to there the accumulated decoy count is zero and so is q. By 40 the upper tail of the decoy distribution is contributing steadily and q has passed 5 percent. Accepting at a q threshold is a score cutoff in disguise, so counting the targets that pass is one filter away.
let acceptedTargets threshold =
targetScores |> Array.filter (fun s -> storeyQ s <= threshold) |> Array.length
printfn "targets accepted at q <= 0.01: %i" (acceptedTargets 0.01)
printfn "targets accepted at q <= 0.05: %i" (acceptedTargets 0.05)
printfn "wrong targets among the q <= 0.01 list: %i"
(wrongScores |> Array.filter (fun s -> storeyQ s <= 0.01) |> Array.length)
|
Against ground truth, the 1 percent list holds 2897 PSMs of which 6 are actually wrong, a true error rate of 0.2 percent. The estimate is conservative here because all 5000 decoys stand in for the 2000 wrong targets, so the decoy count overstates the false hits. The assumed fraction of wrong targets is exactly what the pi0 parameter of the next method makes explicit.
Smoothing the estimate with a logistic fit
The counting estimate is a step function shaped by every random fluctuation
in the decoy tail. calculateQValueLogReg trades the steps for a smooth
curve. First binningFunction slices the score axis into bandwidth-wide
bins and computes raw estimates per bin. The decoy count is normalized by
the overall decoy share, then doubled and scaled by pi0. Dividing that by the
total count in the bin gives a PEP-like value, and dividing it by the total
count at this bin and above gives a q-like value. These raw bin values serve
purely as input points for the fit. A descending logistic curve is then fitted through the
(score, q) points with constrained Levenberg-Marquardt over a range of
steepness guesses, and the fitted curve is the deliverable, a smooth
function from score to q.
The first argument is pi0, the assumed fraction of wrong assignments among the targets. We pass 0.4 because the generated mixture is 40 percent wrong, which is what a real analysis would estimate from the score overlap. The bandwidth of 3 gives bins a few score units wide, enough entries per bin on a 0 to 120 scale to keep the raw points stable.
let logRegQ =
FDRControl.calculateQValueLogReg
0.4
3.0
psms
(fun x -> x.IsDecoy)
(fun x -> x.Score)
(fun x -> x.Score)
printfn "%5s %10s %10s" "score" "Storey q" "logReg q"
for s in probes do
printfn "%5.0f %10.5f %10.5f" s (storeyQ s) (logRegQ s)
|
The methods agree over most of the range. Everywhere above 55 both report zero, and both place the onset of error below 45. They part ways in the transition zone: at a score of 40 the counting estimate already sees the 159 decoys above that score, while the fitted sigmoid centers its drop on the bulk of the decoy distribution near 30 and has fallen close to zero by 40. The pi0 scaling also lowers the fitted curve as a whole, since the fit treats only 40 percent of the targets as potentially wrong where Storey's count treats every decoy as evidence against a target. Which output feeds the pipeline is a choice between the assumption-free step function and the smooth curve that needs a pi0.
From list-level q to single-PSM PEP
The q-value describes a whole accepted list. The PEP asks how likely one particular
PSM is wrong. A PSM
sitting exactly at a 1 percent q cutoff typically carries a PEP well above 1
percent, because the list average is pulled down by all the confident hits
above it. The library derives PEP-like values inside the binning and fitting
machinery you just saw, where each bin's decoy share is a local error
estimate, and it also exposes dedicated PEP functions: calculatePEPValues
computes the per-bin decoy shares directly and initCalculateLin returns a
fitted score-to-PEP mapping. The pipeline-facing outputs on this page are the
two q-value functions.
Estimating protein-level false positives with MAYU
Once PSMs are aggregated to proteins, the error question returns one level
up: how many of the target proteins with at least one accepted hit are
false? FDRControl.MAYU.estimatePi0HG answers with the MAYU model,
translated from Percolator's ProteinFDREstimator. It treats the decoy
protein hits as draws from a hypergeometric distribution over the database
and returns the expected number of false positives among the target hits.
The arguments are the total number of candidate proteins in the database,
the number of target protein hits and the number of decoy protein hits.
let expectedFP = FDRControl.MAYU.estimatePi0HG 2000. 800. 30.
printfn "expected false positive protein hits: %.2f" expectedFP
printfn "implied protein-level FDR: %.4f" (expectedFP / 800.)
|
With 800 of 2000 database proteins hit and 30 decoy hits observed, the model expects about 18 of the 800 target hits to be false, a protein-level FDR just over 2 percent. The expectation is a weighted average over all feasible false positive counts from zero up to the decoy count, so it can land below the raw decoy count, as it does here. The protein inference page applies this estimator when assembling peptide evidence into protein lists.
Rescoring with Percolator
The methods above interpret one score column. Percolator improves the separation before the interpretation: it trains a support vector machine on the PSMs themselves, using the decoy PSMs as the negative training set and high-confidence target PSMs as positives, on features such as the raw score, the delta to the next-best candidate, the precursor mass error and the precursor charge. The learned score replaces the raw one and the positive training set is re-derived from it, and this classification round repeats until the score stabilizes. The output is again q-values and PEPs, computed on a score axis where targets and decoys separate better.
Percolator ships as an external program, and the PercolatorWrapper module
wraps its command line. The option groups of the CLI are mirrored by
discriminated unions under PercolatorWrapper.Parameters, so a parameter
set is ordinary typed F# data, and the PercolatorParams cases collect the
groups. Building a set looks like this.
open BioFSharp.Mz.PercolatorWrapper
let percolatorParams : Parameters.PercolatorParams list =
[ Parameters.GeneralOptions
[ Parameters.VerbosityOfOutput 2
Parameters.PostProcessing_TargetDecoyCompetition ]
Parameters.FileInputOptions
[ Parameters.PINTAB (System.IO.FileInfo "run1.pin") ]
Parameters.FileOutputOptions
[ Parameters.POUTTAB_PSMs (System.IO.FileInfo "run1_psms.tsv")
Parameters.POUTTAB_DecoyPSMs (System.IO.FileInfo "run1_decoy_psms.tsv") ]
Parameters.SVMTrainingOptions
[ Parameters.FDR_CrossValidation 0.01
Parameters.MaxIterations 10 ] ]
let groupSummary (p: Parameters.PercolatorParams) =
match p with
| Parameters.GeneralOptions s -> "general options", Seq.length s
| Parameters.FileInputOptions s -> "file input", Seq.length s
| Parameters.FileOutputOptions s -> "file output", Seq.length s
| Parameters.SVMFeatureOptions s -> "SVM features", Seq.length s
| Parameters.SVMTrainingOptions s -> "SVM training", Seq.length s
| Parameters.ProteinInferenceOptions_FIDO s -> "Fido inference", Seq.length s
| Parameters.ProteinInferenceOptions_Percolator s -> "protein inference", Seq.length s
for p in percolatorParams do
let name, count = groupSummary p
printfn "%-16s %i option(s)" name count
|
The set reads a pin-tab input file, applies target-decoy competition,
evaluates cross validation at 1 percent FDR with at most ten SVM iterations,
and writes separate tab-delimited result files for target and decoy PSMs.
Running the set goes through PercolatorWrapper(os).Percolate, which
renders the options to a command line for the chosen operating system and
launches the percolator process. Executing it requires an external
percolator installation, so this page stops at the constructed parameters.
Where the identifications go next
Whichever q-value function the pipeline uses, the PSMs that pass the chosen threshold form the statistically controlled identification list. Those identifications carry their peptide evidence into protein inference, and the ion areas determined by quantification are aggregated over the same accepted set.
type Random = new: unit -> unit + 1 overload member GetHexString: stringLength: int * ?lowercase: bool -> string + 1 overload member GetItems<'T> : choices: ReadOnlySpan<'T> * length: int -> 'T array + 2 overloads member GetString: choices: ReadOnlySpan<char> * length: int -> string member Next: unit -> int + 2 overloads member NextBytes: buffer: byte array -> unit + 1 overload member NextDouble: unit -> float member NextInt64: unit -> int64 + 2 overloads member NextSingle: unit -> float32 member Shuffle<'T> : values: Span<'T> -> unit + 1 overload ...
<summary>Represents a pseudo-random number generator, which is an algorithm that produces a sequence of numbers that meet certain statistical requirements for randomness.</summary>
--------------------
System.Random() : System.Random
System.Random(Seed: int) : System.Random
<summary>Provides constants and static methods for trigonometric, logarithmic, and other common mathematical functions.</summary>
<summary>Gets the total number of elements in all the dimensions of the <see cref="T:System.Array" />.</summary>
<exception cref="T:System.OverflowException">The array is multidimensional and contains more than <see cref="F:System.Int32.MaxValue">Int32.MaxValue</see> elements.</exception>
<returns>The total number of elements in all the dimensions of the <see cref="T:System.Array" />; zero if there are no elements in the array.</returns>
<summary> Gives a function to calculate the q value for a score in a dataset using Storeys method </summary>
<summary> Gives a function to calculate the q value for a score in a dataset using Lukas method and Levenberg Marguardt fitting </summary>
<summary> Estimates the false positives given the total number of entries, the number of target hits and the number of decoy hits </summary>
union case Parameters.PercolatorParams.GeneralOptions: Parameters.GeneralOptions seq -> Parameters.PercolatorParams
--------------------
type GeneralOptions = | Help | VerbosityOfOutput of int | OnlyPSMs | PostProcessing_MIXMAX | PostProcessing_TargetDecoyCompetition member Equals: GeneralOptions * IEqualityComparer -> bool member IsHelp: bool member IsOnlyPSMs: bool member IsPostProcessing_MIXMAX: bool member IsPostProcessing_TargetDecoyCompetition: bool member IsVerbosityOfOutput: bool
<summary> Set verbosity of output: 0=no processing info, 5=all. Default = 2 </summary>
<summary> Replace the mix-max method by target-decoy competition for assigning q-values and PEPs. If the input PSMs are from separate target and decoy searches, Percolator's SVM scores will be used to eliminate the lower scoring target or decoy PSM(s) of each scan+expMass combination. If the input PSMs are detected to be coming from a concatenated search, this option will be turned on automatically, as this is incompatible with the mix-max method. In case this detection fails, turn this option on explicitly. </summary>
union case Parameters.PercolatorParams.FileInputOptions: Parameters.FileInputOptions seq -> Parameters.PercolatorParams
--------------------
type FileInputOptions = | PINTAB of FileInfo | PINXML of FileInfo | DeprecatedPINXML of FileInfo | SkipSchemeValidation member Equals: FileInputOptions * IEqualityComparer -> bool member IsDeprecatedPINXML: bool member IsPINTAB: bool member IsPINXML: bool member IsSkipSchemeValidation: bool
<summary> Read percolator tab-input format (pin-tab) from standard input. </summary>
type FileInfo = inherit FileSystemInfo new: fileName: string -> unit member AppendText: unit -> StreamWriter member CopyTo: destFileName: string -> FileInfo + 1 overload member Create: unit -> FileStream member CreateText: unit -> StreamWriter member Decrypt: unit -> unit member Delete: unit -> unit member Encrypt: unit -> unit member MoveTo: destFileName: string -> unit + 1 overload ...
<summary>Provides properties and instance methods for the creation, copying, deletion, moving, and opening of files, and aids in the creation of <see cref="T:System.IO.FileStream" /> objects. This class cannot be inherited.</summary>
--------------------
System.IO.FileInfo(fileName: string) : System.IO.FileInfo
union case Parameters.PercolatorParams.FileOutputOptions: Parameters.FileOutputOptions seq -> Parameters.PercolatorParams
--------------------
type FileOutputOptions = | POUTTAB_Peptides of FileInfo | POUTTAB_DecoyPeptides of FileInfo | POUTTAB_PSMs of FileInfo | POUTTAB_DecoyPSMs of FileInfo | POUTTAB_Proteins of FileInfo | POUTTAB_DecoyProteins of FileInfo | POUTTAB_Features of FileInfo | POUTXML of FileInfo | IncludeDecoysInXML member Equals: FileOutputOptions * IEqualityComparer -> bool member IsIncludeDecoysInXML: bool member IsPOUTTAB_DecoyPSMs: bool member IsPOUTTAB_DecoyPeptides: bool member IsPOUTTAB_DecoyProteins: bool member IsPOUTTAB_Features: bool member IsPOUTTAB_PSMs: bool member IsPOUTTAB_Peptides: bool member IsPOUTTAB_Proteins: bool member IsPOUTXML: bool
<summary> Output tab delimited results of PSMs to a file instead of stdout. </summary>
<summary> Output tab delimited results for decoy PSMs into a file </summary>
union case Parameters.PercolatorParams.SVMTrainingOptions: Parameters.SVMTrainingOptions seq -> Parameters.PercolatorParams
--------------------
type SVMTrainingOptions = | SubsetTraining of float | Cpos of float | Cneg of float | FDR_CrossValidation of float | FDR_PositiveExamples of float | MaxIterations of int | QuickValidation | ReportPerfomanceAfterIteration | SeedRndNumberGenerator of float member Equals: SVMTrainingOptions * IEqualityComparer -> bool member IsCneg: bool member IsCpos: bool member IsFDR_CrossValidation: bool member IsFDR_PositiveExamples: bool member IsMaxIterations: bool member IsQuickValidation: bool member IsReportPerfomanceAfterIteration: bool member IsSeedRndNumberGenerator: bool member IsSubsetTraining: bool
<summary> False discovery rate threshold for evaluating best cross validation result and reported end result. Default = 0.01. </summary>
<summary> Maximal number of iterations </summary>
union case Parameters.PercolatorParams.SVMFeatureOptions: Parameters.SVMFeatureOptions seq -> Parameters.PercolatorParams
--------------------
type SVMFeatureOptions = | OUT_SVMWeights of FileInfo | IN_SVMWeights of FileInfo | UnitNorm | Override | DOC | Klammer member Equals: SVMFeatureOptions * IEqualityComparer -> bool member IsDOC: bool member IsIN_SVMWeights: bool member IsKlammer: bool member IsOUT_SVMWeights: bool member IsOverride: bool member IsUnitNorm: bool
union case Parameters.PercolatorParams.ProteinInferenceOptions_FIDO: Parameters.ProteinInferenceOptions_FIDO seq -> Parameters.PercolatorParams
--------------------
type ProteinInferenceOptions_FIDO = | UseFido | Alpha of float | Beta of float | Gamma of float | EmpricialQValue | QValueThreshold of float | GridSearchDepth of float | GridSearchSpeed of float | NoSubgraphSplitting | ProteinTruncationThreshold member Equals: ProteinInferenceOptions_FIDO * IEqualityComparer -> bool member IsAlpha: bool member IsBeta: bool member IsEmpricialQValue: bool member IsGamma: bool member IsGridSearchDepth: bool member IsGridSearchSpeed: bool member IsNoSubgraphSplitting: bool member IsProteinTruncationThreshold: bool member IsQValueThreshold: bool ...
union case Parameters.PercolatorParams.ProteinInferenceOptions_Percolator: Parameters.ProteinInferenceOptions_Percolator seq -> Parameters.PercolatorParams
--------------------
type ProteinInferenceOptions_Percolator = | Fasta of FileInfo | ProteinDecoyPattern of string | Protease of string | ReportProteinFragments | ReportProteinDuplicates member Equals: ProteinInferenceOptions_Percolator * IEqualityComparer -> bool member IsFasta: bool member IsProtease: bool member IsProteinDecoyPattern: bool member IsReportProteinDuplicates: bool member IsReportProteinFragments: bool
BioFSharp.Mz