Skip to content

feat: switch k-fold splitting to StratifiedGroupKFold for slide-disjoint folds#12

Merged
vojtech-cifka merged 3 commits into
masterfrom
feat/stratified-group-kfold
May 13, 2026
Merged

feat: switch k-fold splitting to StratifiedGroupKFold for slide-disjoint folds#12
vojtech-cifka merged 3 commits into
masterfrom
feat/stratified-group-kfold

Conversation

@vojtech-cifka

@vojtech-cifka vojtech-cifka commented May 13, 2026

Copy link
Copy Markdown
Collaborator

Problem

The existing StratifiedKFold split operates at tile level. Tiles from the same
slide can therefore land in both train and val within a single fold, which can
cause leakage when validation patches come from slides seen during training.

At the same time, the original tile-level split is still useful as a baseline and
should remain available for comparison.

Solution

Keep the original StratifiedKFold implementation and add a configurable
StratifiedGroupKFold option using slide_id as the grouping key.

The split strategy is selected through Hydra config:

  • split/kfold_split_5_folds: original tile-level StratifiedKFold
  • split/stratified_group_kfold_split_5_folds: slide-grouped StratifiedGroupKFold

Changes

  • split/kfold_split.py: supports both stratified and stratified_group
    strategies via kfold_strategy
  • Restored original rare-class handling for stratified: rare tile labels are
    collapsed to background for stratification only
  • Added grouped rare-class handling for stratified_group: labels appearing in
    fewer than n_folds distinct slides are dropped; count is logged as
    dropped_rare_class_tiles
  • Added configs/experiment/split/stratified_group_kfold_split_5_folds.yaml
  • Added kfold_strategy to split config metadata/hyperparameters
  • Updated scripts/submit_kfold_split.py so the submitted approach is selected
    by changing the experiment config
  • Kept label out of the output parquet; it is only an internal stratification
    proxy, while downstream training derives labels from roi_coverage_*
  • New metrics: fold_{i}_val_tile_pct, fold_size_cv

Results (5-fold run, 1 102 086 tiles, 137 slides)

Fold Val tiles Val slides Tissue prop mean ± std
0 242 809 (22.0%) 26 0.908 ± 0.267
1 211 248 (19.2%) 26 0.881 ± 0.298
2 217 666 (19.8%) 27 0.898 ± 0.280
3 215 889 (19.6%) 27 0.870 ± 0.308
4 214 474 (19.5%) 31 0.881 ± 0.298

Fold size CV: 0.052. No rare-class drops in this dataset.

Summary by CodeRabbit

  • New Features

    • Added a stratified-group k-fold option that keeps all tiles from the same slide in one fold.
    • New example config to run 5-fold stratified-group splitting.
  • Improvements

    • Split config now exposes a kfold strategy parameter and uses it in run metadata.
    • Output dataset no longer writes a separate "label" column (keeps tissue_prop and fold).
  • Bug Fixes

    • Drops classes that appear in fewer than k distinct slides with a warning; aborts if all data would be removed.

Review Change Stack

…joint folds

Replace StratifiedKFold with StratifiedGroupKFold so all tiles from a single
slide land in the same fold. Rare classes (fewer than n_folds distinct slides)
are now dropped entirely rather than collapsed to background. Removes the
derived label column from output parquet since training re-labels via threshold.
Adds per-fold tile percentage and fold-size CV metrics.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vojtech-cifka
vojtech-cifka requested review from Adames4 and vejtek May 13, 2026 07:57
@vojtech-cifka vojtech-cifka self-assigned this May 13, 2026
@vojtech-cifka
vojtech-cifka requested a review from a team May 13, 2026 07:57
@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a new stratified_group k-fold strategy, implements rare-class slide filtering, provides group-aware fold assignment using StratifiedGroupKFold, updates fold statistics logging (including fold-size CV), and exposes kfold_strategy in configs with a new 5-fold experiment preset.

Changes

K-fold splitting refactor with group-aware stratification and rare-class filtering

Layer / File(s) Summary
Config: add kfold_strategy and experiment preset
configs/experiment/split/stratified_group_kfold_split_5_folds.yaml, configs/split/kfold_split.yaml
Adds kfold_strategy to the k-fold config and metadata hyperparams; adds an experiment preset stratified_group_kfold_split_5_folds.yaml that sets n_folds: 5 and kfold_strategy: stratified_group.
Imports and docstring tweak
split/kfold_split.py
Imports StratifiedGroupKFold and adjusts a docstring punctuation in build_stratification_labels().
Rare-class filtering and stratified/group fold assignment
split/kfold_split.py
Adds drop_rare_class_slides() to drop tiles whose label appears in fewer than n_folds distinct slides; introduces assign_stratified_folds() (StratifiedKFold) and assign_stratified_group_folds() (StratifiedGroupKFold); main() branches on kfold_strategy, applying filtering for stratified_group before assigning folds.
Fold logging and fold-size metrics
split/kfold_split.py
Refactors log_fold_statistics() to accept optional stratification_labels, conditionally log stratification distributions, compute and log fold_size_cv, and include it in the printed summary and mlflow metrics.
Saved dataset output change
split/kfold_split.py
Output parquet no longer adds a "label" column; it adds only "tissue_prop" and "fold".

Sequence Diagram

sequenceDiagram
  participant Main as main()
  participant RareFilter as drop_rare_class_slides()
  participant Assign as assign_stratified_group_folds()
  participant Log as log_fold_statistics()
  Main->>RareFilter: labels, slide_ids, n_folds
  RareFilter-->>Main: keep_mask
  Main->>Main: filter labels, tissue_props, slide_ids, dataset via keep_mask
  Main->>Assign: filtered labels, groups=slide_ids, n_folds, random_state
  Assign-->>Main: folds
  Main->>Log: labels, stratification_labels?, tissue_props, slide_ids, folds, n_folds
  Log-->>Main: logged metrics (per-fold counts, tissue_prop stats, fold_size_cv)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • RationAI/tissue-classification#3: Introduced the original k-fold splitting pipeline that this PR refactors to add group-aware stratification and rare-class slide filtering.

Suggested reviewers

  • vejtek
  • Adames4
  • ejdam87

Poem

🐰 A rabbit sorts tiles with gentle care,

Slides stay together, balanced and fair.
Rare labels tiptoe and softly depart,
Folds find their rhythm, tidy and smart.
Hooray for neat splits and a joyous chart!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main change: switching from tile-level StratifiedKFold to StratifiedGroupKFold using slide_ids as groups to prevent slide-level data leakage.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/stratified-group-kfold

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 and usage tips.

@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)
split/kfold_split.py (1)

43-50: ⚡ Quick win

Make the rare-class filter contract match the actual mask semantics.

These lines describe dropping whole slides by dominant label, but Line 64 drops individual tile rows whose label is rare. Please update the docstring/function name, or change the mask to operate at slide granularity, so callers don't reason about the wrong filtering behavior.

🤖 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 `@split/kfold_split.py` around lines 43 - 50, The docstring claims the function
drops whole slides by dominant label but the implementation currently drops
individual tile rows whose label is rare (uses the tile-level "label" mask); fix
this mismatch by either (A) updating the docstring/function name to state it
filters rare tile labels, or (B) changing the mask logic to compute each slide's
dominant label (group by slide id, e.g., "slide" or "slide_id"), identify
classes with fewer than n_folds slides, and produce a boolean keep-mask that
sets all tiles from those rare slides to False so whole slides are dropped as
described; adjust any references to StratifiedGroupKFold accordingly.
🤖 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 `@split/kfold_split.py`:
- Around line 43-50: The docstring claims the function drops whole slides by
dominant label but the implementation currently drops individual tile rows whose
label is rare (uses the tile-level "label" mask); fix this mismatch by either
(A) updating the docstring/function name to state it filters rare tile labels,
or (B) changing the mask logic to compute each slide's dominant label (group by
slide id, e.g., "slide" or "slide_id"), identify classes with fewer than n_folds
slides, and produce a boolean keep-mask that sets all tiles from those rare
slides to False so whole slides are dropped as described; adjust any references
to StratifiedGroupKFold accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 33ea0437-741e-4460-aa25-4f0b2a93bb45

📥 Commits

Reviewing files that changed from the base of the PR and between da61791 and e2d20f9.

📒 Files selected for processing (1)
  • split/kfold_split.py

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request transitions the data splitting logic from StratifiedKFold to StratifiedGroupKFold, ensuring that all tiles from a specific slide are assigned to the same fold. It replaces the previous rare-class collapsing strategy with a filtering approach that drops tiles belonging to classes present in fewer than n_folds slides. Additionally, the PR enhances logging by including fold size coefficient of variation and tile percentages. Review feedback highlights a discrepancy in the drop_rare_class_slides docstring, which incorrectly refers to 'dominant labels' instead of the actual label-frequency filtering implementation.

Comment thread split/kfold_split.py
Adames4
Adames4 previously approved these changes May 13, 2026
Adames4
Adames4 previously approved these changes May 13, 2026
@vojtech-cifka
vojtech-cifka merged commit 727aba5 into master May 13, 2026
3 checks passed
@vojtech-cifka
vojtech-cifka deleted the feat/stratified-group-kfold branch May 13, 2026 20:04
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.

3 participants