Repository navigation
Base XGBoost's shuffle warning on the fit loader, not the dataset flag - #1313
Conversation
get_dataloader's loaders set the dataset's shuffle flag at the start of each pass, so after another model trains with a shuffled loader on the same dataset, the flag stays True. XGBoostModel.fit then warned that an unshuffled loader shuffles. Read the loader's own setting when it has one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PhbzTePoLMBYwjMLG8R9ek
Test CoverageTotal: 69.80% (17918 / 25671 lines)
|
|
/review |
There was a problem hiding this comment.
Previous review
AI review: findings
This change makes XGBoostModel._collect (pyhealth/models/gradient_boosted_trees.py, around line 351) decide whether to warn about shuffling from the loader's own shuffle_samples setting when it has one, instead of the dataset's shuffle flag. The flag is only stale for get_dataloader's _SeededDataLoader, which applies its shuffle setting to the dataset at the start of each pass, so after another shuffled loader has run the flag can be left True. Plain DataLoaders have no shuffle_samples attribute and keep the old RandomSampler/dataset-flag check, which is still accurate because iterating a SampleDataset shuffles according to that same flag at iteration time. I checked that SampleDataset.set_shuffle keeps litdata's shuffle attribute in sync, and that the logger name the new test captures matches the module's getLogger(name). The new test test_shuffle_warning_follows_the_fit_loader covers both directions (no false warning after a shuffled loader on the same dataset, and the warning still fires for a shuffled fit loader), and the logic is correct on inspection. I could not run the test suite in this environment, so the claim that it fails without the fix and passes with it is unverified. No blocking problems found; the change is a warning-only edit inside one model's private method, with no duplicate of existing functionality and no effect on the core.
Reviewed commit a1f238d
There was a problem hiding this comment.
Previous review
AI Review
suggestion: need change
decision: need change
Thank you for this fix. Basing XGBoost's shuffle warning on the fit loader's own setting removes a false warning, and the new test covers both the quiet and the warning case.
The change itself looks correct. The one blocker is coverage.
To get this merged:
- Raise test coverage for
pyhealth/models/gradient_boosted_trees.py. CI reports 27.86% (78 of 280 statements), which is below the 80% bar for a modified file. The whole repository is at 67.87%, which meets the 60% bar. Please add tests for the uncovered parts of this file. If the low figure comes from XGBoost-dependent tests being skipped in CI, please make sure they run there. If most of the uncovered lines belong to other models in that file, add tests for them, or explain in the PR why they can't be covered.
Optional suggestions:
- Add a test for a plain
DataLoader, which has noshuffle_samplesattribute. It would cover the fallback to theRandomSampleror dataset-flag check.
Reviewed commit a1f238d
…emory datasets Every test of gradient_boosted_trees.py needed xgboost, which the CI test environment does not install, so CI ran none of them (27.86% coverage). The base class does not depend on the tree library, so the new tests/core/test_gradient_boosted_trees.py fills its hooks with scikit-learn linear models and covers the feature layout, every label mode, fitting checks, scale_pos_weight, prediction, attributions and checkpoints. Without xgboost the file is now at 96.79%. The tests include a plain torch DataLoader, which has no shuffle_samples attribute. They showed that the fallback missed shuffled in-memory datasets: InMemorySampleDataset, the default of create_sample_dataset, keeps its flag in _shuffle rather than litdata's shuffle, so a plain loader over one never warned. The check now reads both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ntMLEmFsQEQJKTmPGGw4T
|
Thanks for the review. Both points are addressed in 0e053f0. Coverage of The low figure came from skipped tests. Every test of this file was in
I ran these two test files with xgboost made unimportable. Coverage of the file went from 27.86% (78 of 280), matching CI, to 96.79% (271 of 280). What's left is the abstract hooks that raise If you'd rather have the XGBoost tests run in CI too, xgboost would need to be added to the pixi Optional: a plain
The XGBoost tests (22) and the new tests (13) pass locally, and the PR rules check passes. |
|
/review |
There was a problem hiding this comment.
AI review: findings
The PR changes GradientBoostedTreeModel._collect (pyhealth/models/gradient_boosted_trees.py) so the "shuffles" warning for a training loader reads the loader's own shuffle_samples when it has one. get_dataloader's _SeededDataLoader always sets that attribute, so the stale-dataset-flag false warning is fixed for PyHealth's SampleDataset loaders. Plain loaders keep the old check, now also reading the in-memory dataset's _shuffle. The change only affects a log message, not row order or fit results, and nothing else in the repo performs this check, so it is not redundant. Blocking (medium): the new test_shuffle_warning_follows_the_fit_loader in tests/core/test_xgboost_model.py (line 336) builds an in-memory dataset with create_sample_dataset's default in_memory=True. That dataset has no shuffle attribute, so the "no warning after a shuffled pass" half passes even without the fix, and only the second assertion fails on the old code. The reported disk-backed case is therefore not covered. The same test is duplicated in the new tests/core/test_gradient_boosted_trees.py (line 271) with the same gap. Non-blocking (low): the fallback still trusts a flag that get_dataloader sets only at creation for non-SampleDataset iterables, and the 335-line new base-class test file is not mentioned in the description. I could not run the test suite (read-only tools), so the claimed pass results are unverified. Overall risk is low for the code change and moderate for the test gap; manual review is yes because the change edits a base-class method.
Reviewed commit 0e053f0
| XGBoostModel(ds).fit(loader) | ||
|
|
||
| def test_shuffle_warning_follows_the_fit_loader(self): | ||
| ds = create_sample_dataset(_mixed_samples(), {"labs": "tensor"}, {"label": "binary"}) |
There was a problem hiding this comment.
[medium] Shuffle test uses an in-memory dataset, so it does not reproduce the reported bug
create_sample_dataset defaults to in_memory=True, which returns InMemorySampleDataset (pyhealth/datasets/sample_dataset.py:933). That class never calls the litdata base init and keeps its flag in _shuffle (sample_dataset.py:751), so loader.dataset.shuffle does not exist. The original getattr(loader.dataset, 'shuffle', False) check therefore always returns False here, and the first assertNoLogs passes on the unfixed code. The test only fails on the unfixed code because of the second assertLogs, which checks an unrelated behavior. The reported bug needs a disk-backed SampleDataset (in_memory=False), where shuffle is the litdata flag that stays stale. Fix: build the dataset with in_memory=False for the first half, or assert the flag is set after the shuffled pass, so the test fails on the old code for the reason the description gives.
| LinearBackend(ds).fit(_plain_loader(ds, drop_last=True)) | ||
|
|
||
| def test_shuffle_warning_follows_the_fit_loader(self): | ||
| ds = create_sample_dataset(_samples(), {"labs": "tensor"}, {"label": "binary"}) |
There was a problem hiding this comment.
[medium] Same in-memory gap in the duplicated shuffle test
This test repeats the test in test_xgboost_model.py and uses the default in-memory dataset, so its no-warning half does not exercise the stale flag from the description. Use in_memory=False for the stale-flag case, or drop the duplicate and keep one test per dataset type.
| # Iterating a SampleDataset shuffles when its flag is set: | ||
| # litdata's ``shuffle`` on disk, ``_shuffle`` in memory. | ||
| shuffles = isinstance(loader.sampler, RandomSampler) or bool( | ||
| getattr(loader.dataset, "shuffle", False) |
There was a problem hiding this comment.
[low] Fallback still trusts a creation-time flag for non-SampleDataset iterables
The description says get_dataloader's loaders set the flag on each pass. That is only true for SampleDataset (_SeededDataLoader). For other iterables with set_shuffle, get_dataloader sets dataset.shuffle once at creation (pyhealth/datasets/utils.py:414-416), so a later get_dataloader(..., shuffle=True) call on the same dataset can still make an unshuffled loader warn falsely. This is a limitation, not a regression, so it does not block the merge.
| @@ -0,0 +1,335 @@ | |||
| """GradientBoostedTreeModel with a scikit-learn backend, so it runs without xgboost. | |||
There was a problem hiding this comment.
[low] Base-class test file is outside the PR description
The description covers only the XGBoost shuffle warning and its one test, but this PR adds a 335-line test file for GradientBoostedTreeModel with a scikit-learn LinearBackend. The file looks valid and scikit-learn is a core dependency (pyproject.toml:35), so it does not block the merge. Update the description to list it, or split it into a separate PR.
There was a problem hiding this comment.
AI Review
suggestion: human review
decision: not available
Haiku found that this change needs a human review: The change edits _collect in GradientBoostedTreeModel, a base class that XGBoostModel builds on and that a planned LightGBM model will also build on. The edit only changes the shuffle-warning condition, but it modifies a base class directly, so the rules put it in the core.
Reviewed commit 0e053f0
…ck's limit test_shuffle_warning_follows_the_fit_loader built an in-memory dataset, which has no litdata shuffle flag, so its no-warning half passed on the unfixed code too. It now uses a disk-backed dataset, checks that the shuffled pass leaves the flag set, and fails without the fix. The copy of that test in test_gradient_boosted_trees.py is dropped. That file keeps the plain-DataLoader test, which covers in-memory datasets and map-style rows, so each dataset type is tested once. A comment at the dataset-flag fallback notes that get_dataloader sets the flag of other iterable datasets once, when a loader is created, so it follows the most recently created loader. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ntMLEmFsQEQJKTmPGGw4T
test_shuffle_warning_follows_the_fit_loader needed xgboost, which CI does not install, so CI skipped the PR's main regression test. The check is in GradientBoostedTreeModel, so the disk-backed case now runs with the scikit-learn backend in test_gradient_boosted_trees.py, next to the in-memory and plain-loader test. The XGBoost copy is dropped: it checked nothing XGBoost-specific. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ntMLEmFsQEQJKTmPGGw4T
|
Thanks for the second review. Everything is addressed in 5992de9 and 4e7550b. Medium: the shuffle test now reproduces the reported bug, and CI runs it
Medium: no duplicated test The XGBoost copy is dropped, since it checked nothing XGBoost-specific. The base-class file now has one shuffle test per dataset type:
Low: other iterable datasets A comment at the fallback now notes that Low: PR description The description now lists All 21 tests in |
Problem
XGBoostModel.fitwarns when its training loader shuffles, because row order changes row subsampling. The check readsloader.dataset.shuffle. Loaders fromget_dataloader(_SeededDataLoader) set that flag on the shared dataset at the start of each pass. So after any shuffled loader on the same dataset has run, for example another model's training loader, the flag staysTrue.fitwithget_dataloader(ds, ..., shuffle=False)then warns that the loader shuffles, which is false.This showed up in the new Tutorial 00, which trains an RNN and XGBoost on the same split. The notebook currently works around it by training XGBoost first.
Fix
Use the loader's own
shuffle_samplessetting when it has one. That is what applies when the loader iterates. Other loaders keep the old check: aRandomSampler, or the dataset's flag.The dataset-flag check also missed in-memory datasets.
InMemorySampleDataset, whichcreate_sample_datasetreturns by default, keeps its flag in_shuffle, not litdata'sshuffle. So a plainDataLoaderover a shuffled in-memory dataset never warned. The check now reads both. A comment at the check notes a limitation for other iterable datasets:get_dataloadersets their flag once, when a loader is created, so the flag follows the most recently created loader.Test
New
tests/core/test_gradient_boosted_trees.py, for CI coverage without xgboost. Every test ofgradient_boosted_trees.pyneeded xgboost, which the CI test environment does not install, so CI ran none of them (27.86% coverage of the file).GradientBoostedTreeModeldoes not depend on the tree library, so this file fills its hooks with scikit-learn linear models. scikit-learn is a core dependency. It covers:scale_pos_weight;test_shuffle_warning_follows_the_fit_loader, on a disk-backed dataset. After iterating a shuffled loader, the dataset'sshuffleflag is left set. Fitting with an unshuffled loader then logs no warning, and fitting with a shuffled loader still warns. Without the fix, the no-warning assertion fails.test_shuffle_warning_with_a_plain_dataloader, a plainDataLoaderover map-style rows and over an in-memory dataset. Without the_shufflefix, it fails.None of these tests needs xgboost, so CI runs them all, the regression test included. Run without xgboost, the file's coverage is 96.79% (271 of 280 statements).
All 21 tests in
tests/core/test_xgboost_model.pyand 13 intests/core/test_gradient_boosted_trees.pypass, ruff is clean, andtools/check_pr_rules.pypasses.🤖 Generated with Claude Code
https://claude.ai/code/session_01PhbzTePoLMBYwjMLG8R9ek