Skip to content

M sstats big/work/20260513 arrow chunking followups#17

Merged
tonywu1999 merged 19 commits into
develfrom
MSstatsBig/work/20260513_arrow_chunking_followups
Jul 10, 2026
Merged

M sstats big/work/20260513 arrow chunking followups#17
tonywu1999 merged 19 commits into
develfrom
MSstatsBig/work/20260513_arrow_chunking_followups

Conversation

@Rudhik1904

@Rudhik1904 Rudhik1904 commented May 18, 2026

Copy link
Copy Markdown
Contributor

Motivation and Context

reduceBigSpectronaut() previously streamed Spectronaut CSV exports through readr::read_delim_chunked, which holds a string-interning pool that grows across chunks and pushed peak memory well above one batch's working set. This branch ports the reader to Arrow (which releases per-batch state) and follows up with the three remaining items from TODO-arrow_chunking_followups.md:

  1. The Arrow block_size was Arrow's 256 KiB default — too small for wide Spectronaut rows, causing Invalid: straddling object straddles two block boundaries errors and excessive parser overhead.
  2. End users had no documented escape hatch when the straddling error did fire on extra-wide exports.
  3. cleanSpectronautChunk() ran every batch through ~13 sequential dplyr verbs on a data.frame, producing repeated transient column allocations and fragmenting R's allocator. The rest of the MSstatsConvert family is data.table-native.

Solution: replace readr with Arrow's CsvReadOptions / Scanner / ToRecordBatchReader, raise the default block_size to 16 MiB and expose it as a user parameter, document the straddling-object workaround at the parameter, and rewrite cleanSpectronautChunk() in pure data.table.

Changes

  • Arrow reader replaces readr::read_delim_chunked in reduceBigSpectronaut(). Uses arrow::open_dataset() + Scanner + ToRecordBatchReader to stream record batches; preserves the existing comma/tab/semicolon delimiter switch via CsvParseOptions$delimiter. Per-batch progress logging every 1,000 batches.
  • New block_size parameter on both reduceBigSpectronaut() and the user-facing bigSpectronauttoMSstatsFormat(). Default 16L * 1024L * 1024L (16 MiB) replaces Arrow's 256 KiB default. Coerced to integer and validated (length 1, non-NA, positive).
  • Roxygen @param block_size documents the exact error string (Invalid: straddling object straddles two block boundaries) and the recommended override (64L * 1024L * 1024L) so users hitting the straddling error on pathological rows have a self-service fix.
  • cleanSpectronautChunk() rewritten in data.table. setDT(input) at entry; column selection via dt[, cols, with = FALSE]; two-step rename via setnames(..., skip_absent = TRUE) matching the MSstatsConvert family convention; in-place column updates via :=; conditional NA assignment via mask form dt[cond, Intensity := NA_real_]. Function shrank from ~88 lines to ~64.
  • NA-q-value semantics preserved. Q-value filters use is.na(EGQvalue) | EGQvalue >= cutoff so rows with missing q-values still get Intensity = NA, matching the previous dplyr::if_else behavior (a naive data.table translation would silently change this).
  • Dead code removed. The dplyr::collect(head(dplyr::select(...))) pattern at the old lines 140/144 was a no-op residue from an earlier Arrow-Table refactor and is gone.
  • data.table added to Imports in DESCRIPTION and imported via @importFrom data.table := .SD setDT setnames so the package is cedta()-aware. Regenerated NAMESPACE and man/bigSpectronauttoMSstatsFormat.Rd via devtools::document().

Testing

All tests run via devtools::test(): 51 PASS, 0 FAIL, 0 WARN, 0 SKIP.

New tests added in tests/testthat/test-converters.R:

  • reduceBigSpectronaut validates block_size: rejects negative, zero, NA, length-2 vector, and unparseable string inputs.
  • bigSpectronauttoMSstatsFormat plumbs block_size through: spies on reduceBigSpectronaut via mockery::stub, asserts the default forwards 16L * 1024L * 1024L and an explicit override forwards the user's value.
  • cleanSpectronautChunk schema smoke test: synthetic minimal Spectronaut-shaped input, asserts output column set and basic values.
  • cleanSpectronautChunk filter_by_excluded: rows with Excluded == "True" get Intensity = NA.
  • cleanSpectronautChunk filter_by_identified: rows with Identified == "False" get Intensity = NA.
  • cleanSpectronautChunk filter_by_qvalue (incl. NA case): rows below cutoff are kept, above cutoff become NA, and NA q-values become NA — the explicit semantic guarantee from the rewrite.
  • cleanSpectronautChunk drops rows where F.FrgLossType != "noloss".

Before this branch cleanSpectronautChunk had no direct test coverage (existing tests stubbed reduceBigSpectronaut out entirely).

Checklist Before Requesting a Review

  • I have read the MSstats contributing guidelines
  • My changes generate no new warnings
  • Any dependent changes have been merged and published in downstream modules

Motivation & Context

Large Spectronaut exports previously used readr::read_delim_chunked() for chunked CSV ingestion, which could lead to unbounded memory growth. Additionally, some wide exports hit Apache Arrow CSV “straddling object” parsing errors across block boundaries. Finally, the chunk cleaning logic relied on dplyr-style transformations rather than efficient in-place updates for high-volume streaming.

This PR switches Spectronaut ingestion in reduceBigSpectronaut() to an Arrow streaming pipeline (open_dataset() + Scanner + ToRecordBatchReader) with bounded peak memory, introduces a tunable block_size to mitigate straddling-object failures, and rewrites cleanSpectronautChunk() using data.table to preserve existing filtering/NA semantics while improving performance.

Detailed changes

  • Package metadata / dependencies

    • Added data.table to DESCRIPTION imports.
    • Updated NAMESPACE to import required data.table symbols (:=, .SD, setDT, setnames) and stats::setNames.
  • Arrow-based streaming ingestion with configurable chunk sizing

    • Updated reduceBigSpectronaut(..., anomalyModelFeatures=c(), block_size = 16L * 1024L * 1024L):
      • Validates block_size is a single, non-NA, positive value (stopifnot(length(block_size) == 1L, !is.na(block_size), block_size > 0L) after coercion to integer).
      • Determines CSV delimiter from the input filename.
      • Opens an Arrow CSV dataset and uses CsvReadOptions$create(block_size = block_size) to control parsing block size.
      • Uses a Scanner with a projection limited to only the columns needed by downstream cleaning (intersected with columns actually present).
      • Iterates record-batch-by-record-batch via ToRecordBatchReader() / read_next_batch().
      • Converts each batch to a data frame, calls cleanSpectronautChunk(..., pos=pos, ...), advances pos by the number of rows, and logs progress every 1,000 batches plus a final completion log.
  • Chunk cleaning rewritten for efficient in-place processing

    • Reimplemented cleanSpectronautChunk() using data.table:
      • Coerces input to data.table (setDT(input)) and filters to columns actually present in the batch.
      • Performs two-stage column renaming (standardization, then mapping to final MSstats names).
      • Converts Intensity to numeric.
      • Converts "True" string fields to logicals for Excluded and (when present and character-typed) Identified.
      • Applies filtering rules while preserving NA-aware q-value semantics:
        • filter_by_excluded: excluded rows get Intensity := NA_real_.
        • filter_by_identified: unidentified rows get Intensity := NA_real_.
        • filter_by_qvalue: requires EGQvalue/PGQvalue, and preserves dplyr-like if_else semantics by setting Intensity := NA_real_ when is.na(q) | q >= qvalue_cutoff.
      • Enforces FFrgLossType == "noloss".
      • Derives IsotopeLabelType from LabeledSequence (H for Lys8/Arg10-containing sequences; otherwise L).
      • Writes only the finalized output columns (including anomaly-score columns when enabled).
  • Forwarding block_size from the public converter

    • Updated bigSpectronauttoMSstatsFormat(..., block_size = 16L * 1024L * 1024L):
      • Documents Arrow CSV block_size and the “straddling object straddles two block boundaries” mitigation/tuning guidance.
      • Forwards block_size to reduceBigSpectronaut().
  • Documentation

    • Updated man/bigSpectronauttoMSstatsFormat.Rd usage, parameter docs for block_size, troubleshooting guidance for straddling-object errors, and details of required/optional Spectronaut export columns.
    • Added man/dot-prefixedPath.Rd for internal .prefixedPath(prefix, path).

Unit tests added or modified

  • tests/testthat/test-converters.R
    • Added tests covering reduceBigSpectronaut():
      • block_size validation rejects negative values, zero, NA, vectors, and non-numeric strings.
      • Ensures projection drops unused “junk” columns before cleanSpectronautChunk() runs.
    • Added tests covering bigSpectronauttoMSstatsFormat():
      • Confirms block_size is forwarded to reduceBigSpectronaut() (default and override).
    • Added/expanded tests covering cleanSpectronautChunk():
      • Output schema/column expectations.
      • filter_by_excluded and filter_by_identified set Intensity to NA appropriately.
      • filter_by_qvalue NA-aware behavior matches dplyr-style semantics (NA q-values result in NA intensity).
      • Rows not matching FFrgLossType == "noloss" are dropped.
      • Clear error when required filter source columns (e.g., EG.Qvalue) are missing.

Coding guidelines / violations

No coding guideline violations identified.

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@tonywu1999, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 38 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7acb0394-1536-495a-85bf-e2354e72b82f

📥 Commits

Reviewing files that changed from the base of the PR and between 9fb9e7f and 98327d6.

📒 Files selected for processing (6)
  • DESCRIPTION
  • NAMESPACE
  • R/clean_spectronaut.R
  • R/converters.R
  • man/bigSpectronauttoMSstatsFormat.Rd
  • tests/testthat/test-converters.R
📝 Walkthrough

Walkthrough

This PR switches Spectronaut conversion to Arrow record-batch streaming, rewrites chunk cleaning with data.table, adds configurable block_size handling, and updates package metadata, documentation, and tests.

Changes

Spectronaut Arrow Streaming & Data.Table Refactor

Layer / File(s) Summary
Dependency setup
DESCRIPTION, NAMESPACE
Adds data.table metadata and imports the symbols used by the cleaner.
Arrow streaming and chunk cleaning
R/clean_spectronaut.R
Streams projected CSV batches through Arrow and applies data.table-based validation, renaming, filtering, coercion, and output selection.
block_size API and documentation
R/converters.R, man/bigSpectronauttoMSstatsFormat.Rd, man/dot-prefixedPath.Rd
Adds and forwards block_size, documents tuning and required columns, and documents .prefixedPath().
Spectronaut conversion validation
tests/testthat/test-converters.R
Tests cleaning behavior, missing columns, projection, block size validation, and argument forwarding.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested reviewers: tonywu1999

Poem

🐰 Hop, hop — CSV batches flow,
Arrow guides them row by row.
data.table cleans with care,
Block sizes tune the air,
Neat MSstats results grow.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is noisy, but it still identifies the main change: Arrow-based chunking follow-ups for MSstats Big.
Description check ✅ Passed The description matches the repository template and includes motivation, changes, testing, and checklist items.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch MSstatsBig/work/20260513_arrow_chunking_followups

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Rudhik1904 Rudhik1904 requested a review from tonywu1999 May 18, 2026 21:53

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@R/clean_spectronaut.R`:
- Around line 155-167: The current filter block assumes columns Excluded,
Identified, EGQvalue, PGQvalue, and FFrgLossType always exist and will error if
any are missing; update the logic in clean_spectronaut.R to guard each filter by
checking column presence (e.g., use "if ('Excluded' %in% names(input))" before
the Excluded filter, similarly for Identified, EGQvalue and PGQvalue before
applying qvalue-based NA assignment, and check for FFrgLossType before
subsetting), so each conditional only runs when its target column exists and the
behavior remains unchanged otherwise.
- Around line 25-58: The computed needed_cols is never passed to Arrow, so the
CSV reader still parses all columns; update the CsvConvertOptions usage to
include the projection by calling
arrow::CsvConvertOptions$create(include_columns = needed_cols) (or set the
include_columns field on convert_opts after creation) so that convert_opts
includes needed_cols before calling arrow::open_dataset/Scanner$create;
reference the symbols needed_cols and convert_opts (and the call
arrow::CsvConvertOptions$create) and ensure this happens prior to creating
ds/reader.

In `@tests/testthat/test-converters.R`:
- Around line 106-113: The test's expect_error calls are too broad—change them
to assert the error message mentions "block_size" so they only pass when
block_size validation fails; update each expect_error(reduceBigSpectronaut(...),
...) in tests/testthat/test-converters.R to include a second argument (string or
regex) that matches "block_size" (e.g., "block_size" or "block_size.*invalid")
for the negative values and vector cases, and similarly for the
suppressed-warning call so all invalid-block_size cases are checked by message
content rather than any error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9ebfd981-78cf-43a3-b93a-fd55ccfaeccc

📥 Commits

Reviewing files that changed from the base of the PR and between a43b90b and c8c835e.

📒 Files selected for processing (7)
  • DESCRIPTION
  • NAMESPACE
  • R/clean_spectronaut.R
  • R/converters.R
  • man/bigSpectronauttoMSstatsFormat.Rd
  • man/dot-prefixedPath.Rd
  • tests/testthat/test-converters.R

Comment thread R/clean_spectronaut.R Outdated
Comment thread R/clean_spectronaut.R
Comment thread tests/testthat/test-converters.R Outdated
Comment thread R/clean_spectronaut.R
Rudhik1904 added a commit that referenced this pull request Jul 2, 2026
…hten tests, document

R/clean_spectronaut.R: needed_cols was computed but never applied, so every
batch still materialized all ~35 columns. Wire it in via Scanner projection
(intersect(needed_cols, names(ds)) -> Scanner$create(projection=)). Note:
CsvConvertOptions$include_columns (the obvious fix) collides with the
open_dataset schema layer and errors ("Multiple matches for FieldRef.Name"),
so Scanner projection is the mechanism that works with the dataset API.
intersect() keeps only present columns so partial exports still read.

R/clean_spectronaut.R: cleanSpectronautChunk carefully tolerated missing
columns at subset/rename time, then assumed Excluded/Identified/EGQvalue/
PGQvalue/FFrgLossType existed in the filters -> cryptic data.table
"object not found". Added require_filter_cols() that fails fast with a clear
message naming the original Spectronaut column, scoped to when each filter
actually runs (chose fail-fast over silent skip: skipping a filter changes
which rows/intensities survive downstream).

tests/testthat/test-converters.R: added a test that stubs cleanSpectronautChunk
and asserts junk columns are projected out before the cleaner (guards the
projection; verified it fails if the projection is removed); added tests for
the fail-fast filter guards; tightened block_size expect_error() calls with
regexp = "block_size" so they only pass on the actual validation error.

R/converters.R, man/: expanded @param block_size with tuning guidance (memory
scales ~linearly, diminishing speed returns, double-and-watch strategy) and
added an @details section listing required Spectronaut columns split into
always-required / optional / required-per-filter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/testthat/test-converters.R (1)

237-241: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider expect_no_error() over expect_error(x, NA).

Functionally fine, but testthat's docs now recommend expect_no_error()/expect_no_condition() in place of expect_error(object, NA) for asserting absence of an error: "If NA, asserts that there should be no errors, but we now recommend using expect_no_error() and friends instead."

♻️ Suggested modernization
-  expect_error(
+  expect_no_error(
     MSstatsBig:::cleanSpectronautChunk(make_spectronaut_input(n = 1L),
-                                       output_file, pos = 1L,
-                                       filter_by_qvalue = FALSE),
-    NA)
+                                       output_file, pos = 1L,
+                                       filter_by_qvalue = FALSE))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/testthat/test-converters.R` around lines 237 - 241, Replace the
`expect_error(..., NA)` assertion in the `cleanSpectronautChunk` test with the
modern `expect_no_error()` helper. Update the test in the `test-converters`
suite to keep the same inputs (`make_spectronaut_input`, `output_file`,
`filter_by_qvalue`) while asserting that `MSstatsBig:::cleanSpectronautChunk`
completes without error.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/testthat/test-converters.R`:
- Around line 237-241: Replace the `expect_error(..., NA)` assertion in the
`cleanSpectronautChunk` test with the modern `expect_no_error()` helper. Update
the test in the `test-converters` suite to keep the same inputs
(`make_spectronaut_input`, `output_file`, `filter_by_qvalue`) while asserting
that `MSstatsBig:::cleanSpectronautChunk` completes without error.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 25f345c6-28cc-4482-9681-8556664282c4

📥 Commits

Reviewing files that changed from the base of the PR and between 8188f4e and 160024b.

📒 Files selected for processing (4)
  • R/clean_spectronaut.R
  • R/converters.R
  • man/bigSpectronauttoMSstatsFormat.Rd
  • tests/testthat/test-converters.R
✅ Files skipped from review due to trivial changes (1)
  • man/bigSpectronauttoMSstatsFormat.Rd
🚧 Files skipped from review as they are similar to previous changes (2)
  • R/converters.R
  • R/clean_spectronaut.R

tonywu1999 and others added 15 commits July 10, 2026 12:21
R/clean_spectronaut.R:9-12: added block_size parameter (default 16L * 1024L * 1024L) with coerce + validation.
R/clean_spectronaut.R:44: CsvReadOptions$create now uses the parameter.
R/converters.R:120-125: new @param block_size roxygen with the straddling-object workaround note.
R/converters.R:148-156: bigSpectronauttoMSstatsFormat gains block_size, plumbed to reduceBigSpectronaut.
tests/testthat/test-converters.R:97-163: validation tests (rejects negative/zero/NA/vector/string) + plumbing tests (default forwards 16 MiB, override forwards user's value).
man/bigSpectronauttoMSstatsFormat.Rd: regenerated from roxygen.
…setnames so the package is data.table-aware (cedta()).

R/clean_spectronaut.R:103-187: rewrote cleanSpectronautChunk in data.table:
setDT(input) at entry; subsequent operations modify in place via :=.
Two-step rename (setnames for standardize, then setnames with skip_absent = TRUE to map standardized→MSstats) matches the MSstatsConvert family pattern.
Conditional NA assignment uses mask form dt[cond, Intensity := NA_real_].
Q-value filters preserve dplyr::if_else NA semantics via explicit is.na(EGQvalue) | EGQvalue >= cutoff.
Dropped the leftover dplyr::collect(head(dplyr::select(...))) pattern — was a no-op residue from a prior refactor.
Function shrank from ~88 lines to ~64.
DESCRIPTION:20: added data.table to Imports.
NAMESPACE: regenerated, now imports :=, .SD, setDT, setnames from data.table.
tests/testthat/test-converters.R:97-211: 5 new tests — schema smoke test, filter_by_excluded, filter_by_identified, filter_by_qvalue (covering the NA-q-value case), and FFrgLossType row drop.
…hten tests, document

R/clean_spectronaut.R: needed_cols was computed but never applied, so every
batch still materialized all ~35 columns. Wire it in via Scanner projection
(intersect(needed_cols, names(ds)) -> Scanner$create(projection=)). Note:
CsvConvertOptions$include_columns (the obvious fix) collides with the
open_dataset schema layer and errors ("Multiple matches for FieldRef.Name"),
so Scanner projection is the mechanism that works with the dataset API.
intersect() keeps only present columns so partial exports still read.

R/clean_spectronaut.R: cleanSpectronautChunk carefully tolerated missing
columns at subset/rename time, then assumed Excluded/Identified/EGQvalue/
PGQvalue/FFrgLossType existed in the filters -> cryptic data.table
"object not found". Added require_filter_cols() that fails fast with a clear
message naming the original Spectronaut column, scoped to when each filter
actually runs (chose fail-fast over silent skip: skipping a filter changes
which rows/intensities survive downstream).

tests/testthat/test-converters.R: added a test that stubs cleanSpectronautChunk
and asserts junk columns are projected out before the cleaner (guards the
projection; verified it fails if the projection is removed); added tests for
the fail-fast filter guards; tightened block_size expect_error() calls with
regexp = "block_size" so they only pass on the actual validation error.

R/converters.R, man/: expanded @param block_size with tuning guidance (memory
scales ~linearly, diminishing speed returns, double-and-watch strategy) and
added an @details section listing required Spectronaut columns split into
always-required / optional / required-per-filter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tonywu1999 tonywu1999 force-pushed the MSstatsBig/work/20260513_arrow_chunking_followups branch from 55132da to 74863fc Compare July 10, 2026 16:27
@tonywu1999 tonywu1999 merged commit f4ddf8d into devel Jul 10, 2026
1 check passed
@tonywu1999 tonywu1999 deleted the MSstatsBig/work/20260513_arrow_chunking_followups branch July 10, 2026 16:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants