feat: gg_roc per_class=TRUE — per-class OvR ROC curves (#88, closes #72)#91
Merged
Conversation
- NEWS.md: feature entry + version line bump to 2.7.3.9006 - Document gg_roc(per_class=) and plot.gg_roc(panel=) — fixes codoc warnings - Replace em-dashes in R/plot.gg_roc.R with ASCII (non-ASCII warning) - .Rbuildignore: exclude stray .superpowers/ dir R CMD check --as-cran: 0 errors, 0 warnings, 0 notes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #91 +/- ##
==========================================
+ Coverage 87.11% 87.31% +0.19%
==========================================
Files 38 38
Lines 3043 3098 +55
==========================================
+ Hits 2651 2705 +54
- Misses 392 393 +1
🚀 New features to boost your workflow:
|
…lint plot.gg_roc cyclomatic complexity hit 21 (lint budget 20) after the per_class branch. Move per-class rendering into a private helper; the method now delegates with a single return() call.
There was a problem hiding this comment.
Pull request overview
Adds multi-class one-vs-rest ROC support to gg_roc() for randomForest models via a new per_class mode, and updates plotting/summaries/tests/docs to support per-class output.
Changes:
gg_roc(..., per_class = TRUE)now returns long-format per-class ROC data (withclass) plus a named per-class AUC attribute (sorted by descending AUC) for multi-classrandomForestobjects.plot.gg_roc()addspanel = c("overlay", "facet")and renders per-class ROC objects accordingly;summary.gg_roc()prints named per-class AUCs.- Adds
testthatcoverage andvdiffrsnapshots; updates NEWS/versioning and Rd docs.
Reviewed changes
Copilot reviewed 8 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
R/gg_roc.R |
Implements per_class long-format ROC generation + per-class AUC attribute for randomForest. |
R/plot.gg_roc.R |
Adds panel arg and per-class plotting path. |
R/summary_methods.R |
Updates ROC summary output to print per-class AUCs when class column is present. |
tests/testthat/test_gg_roc.R |
Adds unit tests for per_class, plotting modes, and summary output. |
tests/testthat/test_snapshots.R |
Adds opt-in vdiffr snapshots for per-class overlay/facet plots. |
man/gg_roc.rfsrc.Rd |
Documents new per_class argument. |
man/plot.gg_roc.Rd |
Documents new panel argument. |
NEWS.md |
Announces per-class ROC feature and behavior notes. |
DESCRIPTION |
Bumps development version. |
.Rbuildignore |
Ignores a new dotfile. |
Files not reviewed (2)
- man/gg_roc.rfsrc.Rd: Language not supported
- man/plot.gg_roc.Rd: Language not supported
Comments suppressed due to low confidence (3)
R/plot.gg_roc.R:117
- When
xis a raw multiclassrandomForestand the user passesper_class = TRUEvia..., the existing multiclass branch will still iterate overwhich_outcomeand callgg_roc(..., which_outcome = ind, per_class = TRUE). Sincegg_roc.randomForest()ignoreswhich_outcomewhenper_class = TRUE, this produces duplicated long-format ROC objects and then falls into the list-of-curves plotting path (wrong output / repeated messages).plot.gg_roc()should detectper_class = TRUEin...for raw forests and callgg_roc(x, per_class = TRUE, ...)once, then use the per-class plotting branch.
## ---- Accept a raw rfsrc or randomForest object -----------------------
# If we call this with a forest object instead of a gg_roc object,
# automatically compute the ROC data for the appropriate class(es).
if (inherits(gg_dta, "rfsrc")) {
if (inherits(gg_dta, "class")) {
# Determine the number of outcome classes
crv <- dim(gg_dta$predicted)[2]
if (crv > 2 && is.null(which_outcome)) {
# Multi-class: compute ROC for every class in parallel
gg_dta <- mclapply(seq_len(crv), function(ind) {
gg_roc(gg_dta, which_outcome = ind, ...)
})
} else {
# Binary (or user specified a class): default to second column
if (is.null(which_outcome)) {
which_outcome <- 2
}
gg_dta <- gg_roc(gg_dta, which_outcome, ...)
}
} else {
stop("gg_roc expects a classification randomForest.")
}
} else if (inherits(gg_dta, "randomForest")) {
if (gg_dta$type == "classification") {
# Determine the number of outcome classes
crv <- length(levels(gg_dta$predicted))
if (crv > 2 && is.null(which_outcome)) {
# Multi-class: compute ROC for every class in parallel
gg_dta <- parallel::mclapply(seq_len(crv), function(ind) {
gg_roc(gg_dta, which_outcome = ind, ...)
})
} else {
R/plot.gg_roc.R:135
- In the per-class plotting branch, AUC is read from
attr(x, "auc"), butxis not guaranteed to be the per-classgg_rocobject (e.g., whenplot.gg_roc()is called with a raw forest and internally computesgg_dta). Useattr(gg_dta, "auc")so the caption works whenevergg_dtacarries the AUC attribute.
# Rendering is delegated to a helper to keep this method's cyclomatic
# complexity within the project lint budget.
if ("class" %in% names(gg_dta)) {
return(.plot_gg_roc_per_class(gg_dta, attr(x, "auc"), panel))
R/plot.gg_roc.R:88
- The new per-class plotting behavior is only exercised when
plot()is called on an already-computedgg_rocobject. Given thatplot.gg_roc()also accepts raw forest objects and forwards...togg_roc(), adding a test thatplot(rf, per_class = TRUE, panel = ...)returns aggplot(and does not emit repeated ignore-messages) would help prevent regressions in this important entry point.
plot.gg_roc <- function(x, which_outcome = NULL,
panel = c("overlay", "facet"), ...) {
panel <- match.arg(panel)
gg_dta <- x
## ---- Accept a raw rfsrc or randomForest object -----------------------
# If we call this with a forest object instead of a gg_roc object,
# automatically compute the ROC data for the appropriate class(es).
if (inherits(gg_dta, "rfsrc")) {
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…91 review) Copilot review: panel before ... is backward-incompatible — a positional caller like plot(x, 1, FALSE) previously routed FALSE into ..., but with panel as the 3rd formal it would bind to panel and error on match.arg. Placing panel after ... makes it name-only; all test calls already use panel = by name.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
gg_roc()gainsper_class = FALSE. Whenper_class = TRUEon a multi-class forest, returns a long-formatgg_rocdata frame with aclassfactor column and a named AUC vector attribute (ordered by descending AUC).plot.gg_roc()gainspanel = c("overlay", "facet"). Detects theclasscolumn and dispatches to the new multi-class overlay or faceted path.summary.gg_roc()updated to print named per-class AUC values when theclasscolumn is present.per_class = TRUEis a silent no-op.Implementation reuses the existing private
.rf_prob_matrix/.rf_one_class_roc/calc_auchelpers inR/calc_roc.R— no new abstractions.Test plan
devtools::test()— gg_roc suite 79/79 pass; full suite 0 failuresdevtools::check(args="--as-cran")— 0 errors, 0 warnings, 0 notesVDIFFR_RUN_TESTSCloses #72
🤖 Generated with Claude Code