Skip to content

Class balancing is ignored in cross-validation - #144

Open
ellispatrick wants to merge 3 commits into
review/17-new-classifiersfrom
review/18-balancing
Open

ellispatrick wants to merge 3 commits into
review/17-new-classifiersfrom
review/18-balancing

Conversation

@ellispatrick

Copy link
Copy Markdown
Member

Problem. ModellingParams(balancing = "downsample") (or "upsample") has no effect in runTests. runTest calls .rebalanceTrainingClasses only inside if(is.null(.iteration)), the branch for a user calling runTest directly, so within cross-validation the training classes are never rebalanced. This is also the case on devel. crossValidate hard-codes balancing = "none" and is unaffected.

Reproduce (imbalanced data, DLDA): predictions with balancing = "none" and "downsample" are identical (66 vs 14 calls in both).

Change. Rebalance within runTests whenever balancing is not "none". With "none" the training samples are left as they are, so default results are unchanged; I checked identical predictions for DLDA, random forest and SVM from crossValidate. Direct runTest calls behave as before.

Test. test-core.R: counts the training samples given to the classifier; with downsampling every training set is smaller and has equal class sizes.

Stacked on #143.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TfsjwkbDbHkX4kJZNe4ER4

Copilot AI balanced review requested due to automatic review settings October 10, 2026 23:22

Copilot AI 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.

🟡 Changes recommended

Balancing before nested-CV splitting can alter validation sets and leak upsampled observations across folds.

2 open findings
What changed in this PR

Fixes class balancing during cross-validation while preserving default behavior.

Changes:

  • Applies configured balancing to cross-validation training sets.
  • Adds a downsampling regression test.
File Description
R/​runTest.R Enables balancing for cross-validation iterations.
tests/​testthat/​test-core.R Tests downsampling of training sets.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread R/runTest.R

# Rebalance the class sizes of the training samples by either downsampling or upsampling, also when called by
# runTests. Within runTests and with no balancing, the training samples are left as they are (and in the same order).
if((is.null(.iteration) || modellingParams@balancing != "none") && !is(outcomeTrain, "Surv"))
Comment thread tests/testthat/test-core.R Outdated
Comment on lines +231 to +245
trainSizes <- function(balancing) {
sizes <- integer()
counting <- function(measurementsTrain, classesTrain, ...) {
sizes <<- c(sizes, length(classesTrain))
ClassifyR:::DLDAtrainInterface(measurementsTrain, classesTrain, ...)
}
params <- ModellingParams(balancing = balancing, selectParams = NULL, trainParams = TrainParams(counting),
predictParams = PredictParams("DLDA"))
set.seed(1)
runTests(measurements, classes, CrossValParams(permutations = 1, folds = 4, parallelParams = BiocParallel::SerialParam()), params)
sizes
}
none <- trainSizes("none"); down <- trainSizes("downsample")
expect_true(all(down < none))
expect_true(all(down %% 2 == 0)) # Equal numbers of each class.
…, not only in direct runTest calls

runTest rebalanced the training classes only inside its `if(is.null(.iteration))` branch, which runs when a user
calls runTest directly. Within runTests, balancing = "downsample" or "upsample" was silently ignored. Rebalancing now
also happens within runTests when balancing is not "none"; with "none" the training samples are left as they are,
so default results are unchanged (identical predictions checked for DLDA, random forest and SVM). crossValidate
still uses balancing = "none". Adds a test that counts the training samples given to the classifier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfsjwkbDbHkX4kJZNe4ER4
ellispatrick and others added 2 commits October 14, 2026 13:05
…ning samples before rebalancing

runTest rebalanced the training classes before feature selection and tuning, so .innerCVperformance built its inner
folds from the rebalanced samples. With upsampling, copies of one sample could be in both an inner training and test
set (and the inner predictions with duplicated sample names failed to assemble, so upsampling with nested CV gave
errors); with downsampling, the inner test sets were downsampled too. runTest now keeps the samples before
rebalancing and passes them to the selection and training stages, whose nested cross-validation makes its folds from
them, using the features being evaluated; runTest rebalances each inner training set. Ranking, the final fit and
resubstitution tuning use the rebalanced training samples, as before. This applies to runTests and direct runTest
calls.

Results with balancing = "none", and with balancing but no nested-CV tuning, are identical (regression scenarios plus
runTests/runTest scenarios with each balancing and tuneMode). Results of balancing with nested-CV tuning change.

The balancing test now checks the class counts given to the classifier, and a new test checks that no sample is in both
an inner training and test set when upsampling with nested-CV tuning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y1PuRkfDQ3ApzuNoe5ovxW
The result's sample names and actual outcomes were taken from the rebalanced training set, so upsampling repeated
sample names (calcCVperformance then failed with "factor level is duplicated") and downsampling dropped training
samples. They now come from the training samples before rebalancing. Results without balancing are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y1PuRkfDQ3ApzuNoe5ovxW

This branch has not been deployed

No deployments
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