NADIA 0.99.1
Submitted-version fix, in response to the first Bioconductor build report.
-
A parameter was named
cat.BiocCheckreports “Avoid ‘cat’ and ‘print’ outside of ‘show’ methods” for two lines ofR/Benchmarking_Single.R, but neither callscat(): the name belonged to the argument of an anonymous function, an abbreviation ofCategory, used only to index a colour vector.BiocCheckreads parsed code and cannot tell the local binding from the base function. The parameter is nowcategory, which removes the NOTE rather than leaving the reviewer to verify that it is spurious. There are nocat()calls anywhere inR/; the package writes throughmessage().
NADIA 0.99.0
First version prepared for submission to Bioconductor. The project moves from a collection of modules consumed with source() to an installable R package, without any change to the results the pipeline produces.
New
Package structure:
DESCRIPTION,NAMESPACEand help pages generated with roxygen2. The way to load the code is nowlibrary(NADIA).117 exported functions covering the whole pipeline: preprocessing of Spectronaut, DIA-NN and Proteome Discoverer reports (both TMT and label-free), normalization (13 methods), batch correction, imputation (20 methods), differential expression with limma or limpa, metrics and benchmarking, and interactive and static visualization.
Example dataset
nadia_diaand trimmed reports ininst/extdata/, with their provenance documented ininst/scripts/.Agent skill in
inst/skill/, located withnadia_skill_path(): the pipeline written for a coding assistant, with the decision points and the guardrails the defaults invite.pattern_profiler_analysis()no longer defaultsassay_nameto"LoessCyc". No pipeline built since the package conversion produces an assay with that name, so every call that omitted the argument failed. The default is nowNULLand the assay is taken fromDEPs_results$Assay, which records the one the differential abundance was computed on – and whichassay_namealso filters, so the two have to agree anyway. When those results cover several assays, the function aborts and names them rather than guessing. Calls that already passedassay_nameare unaffected.pattern_profiler_analysis(verbose = FALSE)is now silent. Its six helpers wrote their progress with baremessage()calls, so a run asked to be quiet still reported the assay filter, the significance filter and the condition order. They takeverbosenow.Mfuzz::filter.NA()reports throughcat(), which no argument of ours can reach, so its output is captured instead; and the Biobase startup banner, printed because NADIA attaches it for Mfuzz, is suppressed. The results are unchanged.Batch correction is quiet too. BERT reports through the
loggingpackage, which writes to stdout and honours no argument of NADIA’s, so a corrected run printed fifteenINFO::lines whateververbosesaid. They are captured now.Agent skill corrections, found by driving it through the TMT example from scratch: the
normalization_metrics()call was documented withoutmethods, which silently ranks the assays already in the object –raw,log2,BERT, the imputed one – instead of the twelve normalisation methods, and so would have justified a method choice with a comparison of pipeline stages; the join key ofcovariate_df(aColumncolumn matchingmetadata$Coding, notR.FileName) was never stated; the import check rounded the missing percentage and reported 0 % for a dataset that had one missing cell; and nothing told the reader what to do when there is essentially nothing to impute.Two imputation methods printed regardless of
verbose.impute::impute.knn()reports its recursive cluster split withcat()andimputeLCMD::impute.MinProb()prints its estimated sigma; neither is gated on anything, so both landed in quiet runs and in rendered vignettes. Captured in the wrappers, with a test that sweeps every method rather than the two known ones.A further skill correction:
imputation_metrics()masks observed values, so its ranking measures MAR recovery and places every MNAR method last. Read as a straight ranking it says to drop the MNAR stage that the same dataset’s missingness structure calls for. The skill now says to take only the MAR stage from that table, and to choose the MNAR stage from the intensity dependence or a spike-in.species_dfmust map each protein exactly once. The mapping is joined to the DE results onProtein.IDswith no uniqueness check, so a repeated identifier multiplied that protein’s row in every comparison and inflated the counts every metric is built from – one repeated protein tookclassified_dffrom 3994 to 3996 rows and TP from 469 to 470 on the example dataset, with no error and no warning. Exact repeats are now collapsed with a message, a protein claimed by two different species is an error (there is no way to know which side of the truth table it belongs to), and the join is asserted not to change the number of rows.expected_valuesis validated as the ground truth it is. A malformed benchmark specification did not produce an error further down, it produced a plausible-looking benchmark.benchmarking_proteomics()and the four other entry points now reject a non-numeric,NA, infinite or zeroexpected_logFC(a species expected not to change is background, and background is declared by being left out), blank orNAidentifiers, duplicateComparison + Speciesrows, and – the quietest of them – a declaredComparison+Speciespair with no proteins in the data. A typo in an organism name used to leave those proteins in the background, so every correct detection of that species was counted as a false positive and the specificity collapsed with nothing in the output to say so. The check is per pair rather than global: a species present elsewhere in the results but absent from one of the comparisons that declares it leaves that comparison scored against a truth it does not contain, and if it was the only species declared there the comparison has no positives at all – reportingSensitivity = NAandAUC = NA, butMCC = 0andnMCC = 0.5, numbers rather than NAs, which then average silently into a multi-method ranking. A declared comparison that is absent altogether only warns: scoring a subset is legitimate.Two diagnostic columns in
classified_df, also written tobenchmark_classified.tsv.is_significantis the significance rule the classification actually applied, stored rather than recomputed so that no table or plot can drift from it.direction_errorseparates the two kinds of FN: a change missed altogether, and a change called significant with the sign inverted. Neither takes any part in a metric, and TP/FP/TN/FN are unchanged.-
preprocess_diann(), the fourth input format. DIA-NN protein-group matrices (report.pg_matrix.tsv) are wide — one row per protein group, one column per run — so they need their own reader rather than the Spectronaut one, which expects a long report. The result is the sameproteomics_dataobject as the other three, and an example matrix ships ininst/extdata/nadia_diann_report.tsv.gz: the same twelve injections asnadia_dia, searched with DIA-NN instead of Spectronaut, which puts the missingness at 12.5 % against 8.1 % for the same runs.DIA-NN names its intensity columns after the raw file, which does not identify the experimental design, so the design is declared rather than guessed: either rename the columns to the
Abundance: <Condition>_<Replicate>convention thatpreprocess_tmt()already reads, or pass a sample sheet throughannot_pathand leave the report untouched.R.FileNamekeeps the original header either way. A protein-group matrix carries no per-sample metrics at all; what DIA-NN does report is mapped onto the wide contract, and what it does not is left out rather than filled withNA, except for the two families the interactive protein and quantification tables need in order to detect their sample columns. imp_method = "halfmin", the half-minimum imputation that DIA-NN uses for its own differential analysis: “replaces each missing value with half the observed minimum across all runs”. It can be used on its own or as the MNAR stage ofcomboandsoftHybrid(mnar_method = "halfmin"), and it takes part in theimputation_metrics()benchmark. Since the assay is on the log2 scale, halving the intensity means one unit below the global observed minimum, not half of the log2 value —log2(m / 2)islog2(m) - 1.combostill defaults tomnar_method = "min"; nothing existing changes.-
The
.nadiaformat: one file for a whole analysis.write_nadia()writes the preprocessing tables, the processing results and, optionally, the Pattern Profiler output to a single DuckDB database, together with the parameters and the provenance of the run.read_nadia()reads it back, andnadia_result()andnadia_preprocessing()return objects the rest of the package plots directly — the round trip isidentical(), column order, integer columns and ordered factors included.Nothing is stored twice. The
log2assay is exactlylog2of the raw one and is not stored;PCA_Inputis the imputed assay joined to the results and is not stored either; and of the imputed assay only the cells actually filled are kept, each tagged with the branch that filled it — soimputed_valuesis both the imputed data and the MAR/MNAR mask. On the example dataset the file is 4.1 MB against 6.8 MB of TSV and 2.7 MB of Parquet, in one file instead of eleven.The reconstruction lives in the file as SQL views rather than in R code, so a single definition serves every client; with backward-compatible storage, that includes DuckDB-WASM in a browser, with no R installed.
nadia_connect()opens the file for SQL,nadia_tables()lists it without loading it, andnadia_export_parquet()is the way out to an open format. NeedsduckdbandDBI, both inSuggests. -
deps_with_clusters()andnadia_deps_with_clusters(): the input a functional analysis needs. Differential abundance says which proteins moved and Pattern Profiler says which pattern they follow; an enrichment needs both in one table, and joining them by hand is easy to get wrong —DEPs_resultsis long by comparison,long_outputis long by cluster, and merging them without saying what to do about that produces a silent cartesian product. The two functions return the columns ofDEPs_resultsfollowed byCluster,MembershipandClusterRank, either from the two results in memory or from a.nadiafile, where the join is the viewv_deps_pattern_profiler. They return identical tables, which the tests assert rather than assume.The join is a left join from the results and stays one: every protein that was tested survives it, with or without a cluster. That set is the background an enrichment is measured against, and quietly dropping part of it changes every result computed from it.
assignment = "primary"gives one row per protein and comparison, carrying the dominant cluster;assignment = "all"keeps every membership above the threshold, which is what a soft clustering actually says.Feeding that needed one addition to the file.
min_membershipis applied whenlong_outputis built, so a protein whose highest membership falls below it has no row at all — and the highest membership of a protein is only guaranteed to be at least1 / optimal_c, so with enough clusters this happens. Stored that way, a protein that was clustered is indistinguishable from one that never entered the clustering. The newpp_assignmenttable holds the unfiltered hard assignment, one row per clustered protein, and"primary"now returns a cluster for every one of them. On the example dataset it costs 794 rows.The addition is backward compatible in both directions: a file written earlier has neither the table nor the view and reads without them, and a file written now is read by an earlier NADIA that ignores them. The schema version is unchanged, deliberately — adding an object is not changing the shape of one.
Nine vignettes covering the pipeline end to end:
NADIA(start here), plusinput-formats,missing-values,choosing-methods,benchmarking,batch-correction,pattern-profiler,visualizationandresults-and-export. They replaceexample_workflow.R, which has been removed.A testthat suite of over 450 assertions, including one regression test per bug fixed in the 2026-07 code reviews.
Changes
The label-free example is now the same experiment as the DIA ones. It used to be an unrelated label-free run of six samples in two conditions (WT and MUT).
inst/extdata/nadia_lfq_report.tsv.gzand its annotation now hold the same twelve injections asnadia_diaandnadia_diann— conditions A, B and D with four replicates each — acquired label-free and searched with Proteome Discoverer 3.3. Comparing the three example formats is now comparing acquisition and search methods rather than comparing experiments: 8.0 % missing values against 8.1 % for Spectronaut and 12.5 % for DIA-NN on the same runs. Anything reading the example gets three contrasts (B-A,D-A,D-B) where it used to get one (MUT-WT).-
The three wide readers declare their design the same way. Proteome Discoverer LFQ used to be the odd one out: it required a separate annotation file, while TMT and DIA-NN read the
Abundance: <Condition>_<Replicate>column suffixes. Those suffixes were there all along – the sheet the package ships is a table restating them – sopreprocess_lfq()now reads them too, andannot_pathis optional in all three.sample_namesis retired frompreprocess_diann()in favour of the sameannot_path. A vector of labels has to be positional, and nothing guarantees the column order of an export: Proteome Discoverer does not even write the four LFQ column families in the same order as each other, so a re-export that reordered runs would have relabelled them all without any way to notice. A sheet is matched by name, so it either matches or it aborts.The sheet needs
ColumnandCondition, whereColumnis the sample identifier as it appears in the report. Replicates come from a trailing_<digits>when the identifier has one, so a sheet restating the suffixes gives a resultidentical()to supplying no sheet at all. Anything else in the sheet is ignored, including theExperimentcolumn the LFQ annotation used to carry: batch and experiment structure belongs incovariate_dfofprocess_proteomics(). TheR.Experimentcolumn is gone frommetadata, where nothing ever read it. The Spectronaut export schema ships with the package. Reading a Spectronaut report requires it to have been exported with the right columns, which until now meant assembling that list by hand from the documentation.
system.file("extdata", "NADIA_Report.rs", package = "NADIA")is the schema itself: import it into Spectronaut and export with it, and the report has the shapepreprocess_spectronaut()expects.-
The TMT example drops condition C, so all four example reports share one design. The TMTpro report is a separate experiment from the other three, but it reproduces the same three-proteome spike-in — E. coli rising from A to D and yeast falling, with human and bovine flat — so its condition labels already meant what they mean elsewhere. It now ships conditions A, B and D of eight replicates (2,000 × 43), and
condition_order = c("A", "B", "D")replacesc("A", "B", "C", "D")in the examples and the vignette.The two TMT mixes are deliberately kept. They are the only real batch structure among the examples and a balanced one — replicates 1–4 are the first mix and 5–8 the second, bridged by the four
ISchannels — with the mix accounting for 67 % of the variance on PC1 and the condition for 29 % on PC2, orthogonally. A Proteome Discoverer report carries no mix column, so that mapping is now stated in the vignette and in the help page rather than left implicit. preprocess_lfq()andpreprocess_tmt()read the Proteome Discoverer 3.3 column names. Version 3.3 renamedExp. q-value: CombinedtoExp. Protein q-value: Combined. Both readers resolve a column by trying a list of known spellings and fall back toNAwhen none matches, so a report from the new version used to produce anExp.q.valuecolumn that was entirelyNA, silently. The new spelling is now among the candidates in both readers; older exports keep working unchanged.-
The missingness columns of
DEPs_resultsare renamed, and there is a fourth one.MissingGlobalwas not global: it was the percentage of missing values over the replicates of the two conditions being compared, which coincides with the whole experiment only when there are exactly two conditions. Readers took the name at face value. The four columns are now, from the widest scope to the narrowest:New Old Scope MissGlobal(new) every sample in the experiment MissCompMissingGlobalthe replicates of the two conditions compared MissCND1MissingPCT1the numerator condition MissCND2MissingPCT2the denominator condition The old names are gone, with no aliases: code that reads
MissingGlobalnow fails loudly rather than returning the wrong figure quietly. The three renamed columns are identical value for value to the ones they replace — this is a rename, not a recomputation — andMissGlobalis new information that was not available anywhere before. The columns of the interactive tables are labelled% Global,% Comp,% Cond 1and% Cond 2, and each has its own numeric filter. Progress output goes through
message()instead ofcat(), so it can be silenced withsuppressMessages()and redirected like any other condition. The fourprint()methods still usecat(), which is where it belongs. The text is unchanged except for thirteen strings that were still in Spanish.Every random source is now under the caller’s control.
seedarguments were added tonm_compute_metrics(),normalization_metrics()andpattern_profiler_analysis(). All three already had a seed internally, but no caller ever passed one, so the Hopkins statistic and — more importantly — the Mfuzz clustering could not be made reproducible on demand.The PERMANOVA p-value is reproducible.
vegan::adonis2()derives it by permuting the group labels, soPERMANOVA_pval, a column thatnormalization_metrics()exports, changed between runs on the same data. It is now seeded from the sameseedargument.PERMANOVA_R2was never affected.quant_list_widget()andsummary_list_widget()no longer defaultdataandmatrix_datato files underdata-raw/, which no user has. Both arguments are now required and the functions say so.Depends: R (>= 4.6.0), andbiocViewsgainsBatchEffect.Documentation notes that
missForesttakes no seed, and that the seed ofMLEgoes to the internal RNG of thenormpackage, which cannot be restored.max_na_propnow defaults toNULL, which disables the pre-filter. It used to default to 0.8, but the filter was only applied bysoftHybridand by the single imputation methods;combo, the default, never applied it. The same argument therefore meant different things depending onimp_method. Passing a value still filters, now for every method alike. Runs that used the defaultimp_method = "combo"are unaffected; a run with a single method may now keep proteins that were previously discarded for having more than 80 % missing values — which in a package about missing values is the better default, since those are often the on/off cases.The “Export to Excel” button in the
*_widget()tables no longer loads ExcelJS and PapaParse from a CDN. Both libraries (MIT) ship inside the package, so the button works offline andsaveWidget(selfcontained = TRUE)embeds them. The exported.xlsxis byte-for-byte identical in content and formatting.Dependencies are declared in
Imports:andSuggests:; the code no longer callslibrary(). Packages needed only for one specific method are checked at the point of use.Comments, documentation and user-facing messages are now in English.
process_proteomics()andpattern_profiler_analysis()no longer write to disk unless given an output path:export_dirandoutput_filenow default toNULL.pattern_profiler_analysis()returnslong_outputin its result list.Functions that call
set.seed()restore the random number generator on exit, so they no longer affect the reproducibility of code run afterwards..nm_hopkins()takes the seed as an argument.
Bug fixes
A Pattern Profiler added to a preprocessing-only
.nadiafile was written but never readable..db_create_views()returned early when the file held no processing step, and the block that buildsv_pattern_profilersat after that return. Sonadia_add_pattern_profiler()on a file written withwrite_nadia(f, preprocessing)alone wrotepp_membershipandpp_profile, sethas_pattern_profilertoTRUE, and created no view: the metadata said the clustering was there andnadia_pattern_profiler()returnedNULL, with nothing to say why. The view is now built before the gate.The “Significant Proteins” plot left out the proteins it was best placed to expose.
benchmark_signif_bars_gg()filtered onpredicted == 1, which is significance and the expected direction. A spike-in found significant with the sign inverted is an FN and so carriespredicted = 0, so it disappeared from a chart whose title promises every significant protein. On the example dataset nine yeast proteins were missing, and they were missing from the UP facet – the plot showed no yeast protein rising in any comparison when nine did. The filter now reads the newis_significantcolumn.The significant-protein summary ignored
lfc_thr..summarize_significant_proteins()counted onp <= alphaalone, while the classification behind every metric also requiredabs(logFC) >= lfc_thr, so the table and the metrics in the same output disagreed as soon as a fold-change threshold was in force: 1922 significant proteins reported against 1857 classified on the example dataset atlfc_thr = 0.3. It now readsis_significantinstead of recomputing the rule, which makes the two structurally unable to diverge. Withlfc_thr = 0, the default, the table is unchanged.batch_covariatesnow works with categorical metadata. Protecting a biological variable during batch correction aborted for every non-numeric column, which is the usual case: a condition is stored as text. BERT performs no encoding of its own – it collects theCov_*columns and passes them straight tosva::ComBat(mod = )andlimma::removeBatchEffect(design = )– and NADIA handed it the column unchanged. ComBat coerced the text to double, producedNAs and failed withNA/NaN/Inf in foreign function call, while limma rejected it withdesign must be a numeric matrix. Categorical covariates are now encoded as treatment-contrast indicators before the call. Numeric covariates are still passed through untouched, so a continuous covariate keeps its meaning and results that already worked are unchanged.process_proteomics()no longer emits amax()warning for every protein quantified in no sample by the search engine. The unique-peptide count is the row maximum of the per-sample peptide columns, and a row of allNAmademax(na.rm = TRUE)warn and return-Inf. The value was already replaced by zero on the next line, so the warning was noise; a label-free report produces one per protein detected by the feature detector but never scored.Eight duplicated definitions of colour and filtering helpers (
hex_to_rgba,darken_hex,normalize_hex,get_feature_idsand others) spread across six modules are unified into one each. Because they all lived in the global environment, which copy was active depended on the order in which the modules had been loaded.get_feature_ids()now acceptsmode = "target"andmode = "specific"as synonyms. Previously, loadingHeatmap_tidyHeatmap.RafterPCA_Highcharts_Final.Rmadepca_highchart_list(modes = "specific")abort.Filtering with
mode = "any"can no longer returnNAFeatureIDs when thesig_anycolumn contains missing values.summary_list_widget()works with LFQ and TMT metadata, which lack the identification-count columns specific to Spectronaut.