diff --git a/NEWS.md b/NEWS.md index 6edd86916..77bbd2dde 100644 --- a/NEWS.md +++ b/NEWS.md @@ -33,6 +33,38 @@ ggRandomForests v3.5.1 `method = "unsupv"`. It is benign -- the pointer is formed, never dereferenced -- and the fix belongs upstream (`kogalur/randomForestSRC` PR #478); `method = "rnd"` avoids it in the meantime. +* `gg_roc()` on an `rfsrc` forest now honors `which_outcome = 0`. The help page + has always documented `0` as the numeric spelling of `"all"`, but only the + string was normalized, so `0` fell through to `predicted[, 0]`. That is a + legal zero-column subset rather than an error, so the threshold sweep ran on + empty input and returned a two-row frame with no `sens`/`spec` columns, which + then broke `calc_auc()`. Both spellings now take the same route: a warning, + and a fallback to class 1. The macro-average that will replace the fallback + is still tracked under #72. +* The three ROC entry points still disagree about what "all classes" means -- + `gg_roc()` on a `randomForest` fit macro-averages, `gg_roc()` on an `rfsrc` + fit falls back to class 1, and a direct `plot.gg_roc()` call on a raw + multi-class forest overlays one curve per class. That divergence is + unchanged here, but `?gg_roc` and `?plot.gg_roc` now say so instead of + implying the paths agree. Both also correct a longer-standing claim: a raw + forest passed to plain `plot()` never reaches `plot.gg_roc()` at all, + because `randomForestSRC` and `randomForest` register their own `plot` + methods and S3 dispatch prefers them. That branch is reachable only by + naming the method outright. `?gg_roc` further stops advertising character + class names on the `rfsrc` path, which only the `randomForest` method + accepts. +* `gg_partial_rfsrc()` validates `rf_model` before using it. It read `$xvar` + and `$xvar.names` first, so a non-forest failed with base R's "argument is of + length zero" rather than naming the problem. It now matches the error style + already used by `gg_error()`, `gg_vimp()`, `gg_variable()` and `gg_rfsrc()`. +* The `pbc` examples on `?gg_error`, `?plot.gg_error`, `?gg_vimp` and + `?plot.gg_rfsrc` lost their editorial asides and a stray trailing comma in + the `data()` call. The munging block itself is still duplicated across those + pages; consolidating it is deferred. +* `tests/testthat/test_lint.R` runs again, wrapped in `skip_on_cran()`. It had + been commented out entirely, so the suite enforced nothing about style + locally even though CI kept its own lint job. The guard keeps it off the + `R CMD check` clock. ggRandomForests v3.5.0 ====================== diff --git a/R/calc_roc.R b/R/calc_roc.R index d7914e111..9658cd4b7 100644 --- a/R/calc_roc.R +++ b/R/calc_roc.R @@ -12,10 +12,24 @@ ####********************************************************************** ####********************************************************************** # Internal helper: normalize the which_outcome argument. -# "all" is not yet fully supported; fall back to class 1 with a warning. +# "all" is not yet fully supported on the rfsrc path; fall back to class 1 +# with a warning. `0` is the documented numeric spelling of "all" and must +# take the same route: left unnormalized it indexes as `predicted[, 0]`, +# which is legal R that yields a zero-column matrix, so the whole threshold +# sweep runs on empty input and returns a degenerate two-row object with no +# sens/spec columns rather than raising an error. See #72 for the +# macro-average that will eventually replace this fallback. .validate_which_outcome <- function(which_outcome) { - if (identical(which_outcome, "all")) { - warning("Must specify which_outcome for now.") + is_zero <- is.numeric(which_outcome) && + length(which_outcome) == 1L && + !is.na(which_outcome) && + which_outcome == 0 + if (identical(which_outcome, "all") || is_zero) { + warning( + "which_outcome = ", if (is_zero) "0" else "\"all\"", + " is not yet supported for rfsrc forests; falling back to class 1. ", + "Pass an explicit class index to select a different class." + ) return(1L) } which_outcome @@ -39,8 +53,10 @@ #' \code{object$y} for randomForest. #' @param which_outcome Integer index of the class for which the ROC curve is #' computed (e.g. \code{1} for the first class, \code{2} for the second). -#' Use \code{"all"} to request all classes (currently falls back to class 1 -#' with a warning). +#' Use \code{"all"}, or its numeric spelling \code{0}, to request all +#' classes. The \code{randomForest} method returns a macro-averaged +#' one-vs-rest curve; the \code{rfsrc} method warns and falls back to +#' class 1 (see #72). #' @param oob Logical; if \code{TRUE} (default for rfsrc) use OOB predicted #' probabilities. Forced to \code{FALSE} for \code{randomForest} objects. #' @param ... Extra arguments passed to helper functions (currently unused). diff --git a/R/gg_error.R b/R/gg_error.R index d198c9537..c1a97bd23 100644 --- a/R/gg_error.R +++ b/R/gg_error.R @@ -159,10 +159,8 @@ #' plot(gg_dta) #' #' ## ------------- pbc data -#' # Load a cached randomForestSRC object -#' # We need to create this dataset -#' data(pbc, package = "randomForestSRC",) -#' # For whatever reason, the age variable is in days... makes no sense to me +#' data(pbc, package = "randomForestSRC") +#' # Recode: binary columns to logical, low-cardinality columns to factors. #' for (ind in seq_len(dim(pbc)[2])) { #' if (!is.factor(pbc[, ind])) { #' if (length(unique(pbc[which(!is.na(pbc[, ind])), ind])) <= 2) { diff --git a/R/gg_partial_rfsrc.R b/R/gg_partial_rfsrc.R index 0392ec164..7206b43fe 100644 --- a/R/gg_partial_rfsrc.R +++ b/R/gg_partial_rfsrc.R @@ -124,6 +124,15 @@ gg_partial_rfsrc <- function(rf_model, partial.type = c("surv", "chf", "mort"), cat_limit = 10, n_eval = 25) { + # Validate before dereferencing. $xvar and $xvar.names on a non-forest give + # NULL, and the ncol()/sum() comparison below then fails with base R's + # "argument is of length zero" rather than naming the real problem. + if (!inherits(rf_model, "rfsrc")) { + stop("gg_partial_rfsrc: expected an 'rfsrc' object; ", + "got an object of class ", paste(class(rf_model), collapse = "/"), ".", + call. = FALSE) + } + if (is.null(newx)) { newx <- rf_model$xvar } diff --git a/R/gg_roc.R b/R/gg_roc.R index bc7e5f395..5168cedad 100644 --- a/R/gg_roc.R +++ b/R/gg_roc.R @@ -27,10 +27,11 @@ #' \code{\link[randomForest]{randomForest}} object. Only forests with #' \code{family == "class"} (rfsrc) or \code{type == "classification"} #' (randomForest) are supported. -#' @param which_outcome Integer index or character name of the class to score. -#' For binary forests this is usually \code{1} or \code{2}; for multi-class -#' forests, any valid class index or level name. \code{which_outcome = "all"} -#' or \code{0} behaves differently by engine: +#' @param which_outcome Index of the class to score: \code{1} or \code{2} for +#' a binary forest, any valid class index for a multi-class one. A character +#' level name is accepted by the \code{randomForest} method only; the +#' \code{rfsrc} method requires an integer index. +#' \code{which_outcome = "all"} or \code{0} behaves differently by engine: #' \describe{ #' \item{\code{randomForest} method}{Returns a macro-averaged #' one-vs-rest ROC computed over the per-class probabilities.} @@ -60,6 +61,15 @@ #' } #' Pass it to \code{\link{calc_auc}} for the area under the curve. #' +#' @section Defaults across entry points: +#' \code{plot(gg_roc(x))} and a direct \code{plot.gg_roc(x)} call on a raw +#' multi-class forest do not draw the same figure: \code{gg_roc(x)} returns a +#' single curve, while \code{\link{plot.gg_roc}} given a raw forest overlays +#' one curve per class. Plain \code{plot(x)} on a forest reaches neither, as +#' it dispatches to the forest's own method in \code{randomForestSRC} or +#' \code{randomForest}. Pass \code{which_outcome} explicitly when the +#' distinction matters; issue #72 tracks reconciling the two. +#' #' @seealso \code{\link{plot.gg_roc}}, \code{\link{calc_roc}}, #' \code{\link{calc_auc}}, #' \code{\link[randomForestSRC]{rfsrc}}, diff --git a/R/gg_vimp.R b/R/gg_vimp.R index d0970dc84..c36ad7123 100644 --- a/R/gg_vimp.R +++ b/R/gg_vimp.R @@ -156,10 +156,8 @@ #' plot(gg_dta) #' #' ## -------- pbc data -#' # We need to create this dataset -#' data(pbc, package = "randomForestSRC", ) -#' # For whatever reason, the age variable is in days... -#' # makes no sense to me +#' data(pbc, package = "randomForestSRC") +#' # Recode: binary columns to logical, low-cardinality columns to factors. #' for (ind in seq_len(dim(pbc)[2])) { #' if (!is.factor(pbc[, ind])) { #' if (length(unique(pbc[which(!is.na(pbc[, ind])), ind])) <= 2) { diff --git a/R/plot.gg_error.R b/R/plot.gg_error.R index 6e8f5a49a..a65d34680 100644 --- a/R/plot.gg_error.R +++ b/R/plot.gg_error.R @@ -145,10 +145,8 @@ #' plot(gg_dta) #' #' ## ------------- pbc data -#' # Load a cached randomForestSRC object -#' # We need to create this dataset -#' data(pbc, package = "randomForestSRC",) -#' # For whatever reason, the age variable is in days... makes no sense to me +#' data(pbc, package = "randomForestSRC") +#' # Recode: binary columns to logical, low-cardinality columns to factors. #' for (ind in seq_len(dim(pbc)[2])) { #' if (!is.factor(pbc[, ind])) { #' if (length(unique(pbc[which(!is.na(pbc[, ind])), ind])) <= 2) { diff --git a/R/plot.gg_rfsrc.R b/R/plot.gg_rfsrc.R index fb9638885..690666f3a 100644 --- a/R/plot.gg_rfsrc.R +++ b/R/plot.gg_rfsrc.R @@ -132,7 +132,7 @@ #' ## -------- pbc data (larger dataset -- skipped on CRAN) #' \donttest{ #' data(pbc, package = "randomForestSRC") -#' # For whatever reason, the age variable is in days; convert to years +#' # Recode: binary columns to logical, low-cardinality columns to factors. #' for (ind in seq_len(dim(pbc)[2])) { #' if (!is.factor(pbc[, ind])) { #' if (length(unique(pbc[which(!is.na(pbc[, ind])), ind])) <= 2) { diff --git a/R/plot.gg_roc.R b/R/plot.gg_roc.R index 9c72c6b40..aeb24a1f2 100644 --- a/R/plot.gg_roc.R +++ b/R/plot.gg_roc.R @@ -17,8 +17,23 @@ #' #' @param x A \code{\link{gg_roc}} object, or a raw #' \code{\link[randomForestSRC]{rfsrc}} or -#' \code{\link[randomForest]{randomForest}} classification forest. Hand it a -#' forest and \code{\link{gg_roc}} is called for you. +#' \code{\link[randomForest]{randomForest}} classification forest. +#' +#' A raw forest is accepted, but plain \code{plot(forest)} does not arrive +#' here. Both \code{randomForestSRC} and \code{randomForest} register their +#' own \code{plot} methods, so S3 dispatch sends a raw forest to +#' \code{\link[randomForestSRC]{plot.rfsrc}} or \code{plot.randomForest} +#' instead. This branch is reached only by naming the method outright, as +#' \code{plot.gg_roc(forest)}. +#' +#' That branch also does not use \code{gg_roc}'s own default for +#' \code{which_outcome}: given a multi-class forest and +#' \code{which_outcome = NULL} it calls \code{\link{gg_roc}} once per class +#' and overlays the one-vs-rest curves, where \code{gg_roc(x)} alone returns +#' a single curve -- a macro-average for \code{randomForest}, or class 1 +#' with a warning for \code{rfsrc}. Prefer +#' \code{plot(gg_roc(x, which_outcome))}, which is explicit about both the +#' class and the engine. Issue #72 tracks reconciling the entry points. #' @param which_outcome Integer; for multi-class problems, the index of the #' class to plot. When \code{NULL} (default) and the forest has more than two #' classes, the curves for all classes are overlaid in one plot. For binary diff --git a/README.md b/README.md index 8047ca788..c076d8ab1 100644 --- a/README.md +++ b/README.md @@ -82,17 +82,69 @@ vignette("uvarpro", package = "ggRandomForests") ## Function reference +Grouped by what you are trying to look at. The first column is the function you +call, the second is what you hand it. + +### The forest itself + | Function | Input | What you get | |---|---|---| | `gg_error()` | `rfsrc` / `randomForest` | OOB error vs. number of trees | | `gg_vimp()` | `rfsrc` / `randomForest` | Variable importance ranking | | `gg_rfsrc()` | `rfsrc` / `randomForest` | Predicted vs. observed values | | `gg_variable()` | `rfsrc` / `randomForest` | Marginal dependence data frame | + +### Partial dependence + +| Function | Input | What you get | +|---|---|---| | `gg_partial()` | `plot.variable` output | Partial dependence (continuous + categorical) | | `gg_partial_rfsrc()` | `rfsrc` model | Partial dependence via `partial.rfsrc` | -| `gg_survival()` | `rfsrc` survival forest | Kaplan–Meier / Nelson–Aalen estimates | -| `gg_roc()` | `rfsrc` / `randomForest` (class) | ROC curve data | +| `surv_partial.rfsrc()` | `rfsrc` survival forest | Survival partial dependence, one or more predictors | +| `quantile_pts()` | numeric vector | Quantile cut points for coplot panels | + +### Survival + +| Function | Input | What you get | +|---|---|---| +| `gg_survival()` | `rfsrc` survival forest, or a data frame | Kaplan–Meier / Nelson–Aalen estimates | | `gg_brier()` | `rfsrc` (survival) | Time-resolved Brier score and CRPS | +| `kaplan()` | data frame + interval/censor columns | Nonparametric Kaplan–Meier estimate | +| `nelson()` | data frame + interval/censor columns | Nonparametric Nelson–Aalen estimate | + +### Classification and ROC + +| Function | Input | What you get | +|---|---|---| +| `gg_roc()` | `rfsrc` / `randomForest` (class) | ROC curve data | +| `calc_roc()` | `rfsrc` / `randomForest` (class) | The sensitivity/specificity sweep behind `gg_roc()` | +| `calc_auc()` | `gg_roc` object | Area under the curve | + +### varPro — variable priority + +These read a `varPro` fit rather than a forest. `varpro()` is the supervised +fit; `uvarpro()` is the unsupervised one, which needs no outcome. + +| Function | Input | What you get | +|---|---|---| +| `gg_varpro()` | `varpro` fit | Release-rule variable importance | +| `gg_beta_varpro()` | `varpro` fit | Per-variable lasso-beta importance | +| `gg_ivarpro()` | `varpro` fit | Individual (local) variable importance | +| `gg_partial_varpro()` | `varpro` fit | Partial dependence (alias: `gg_partialpro()`) | +| `gg_isopro()` | `isopro` fit | Isolation-forest anomaly scores | +| `gg_udependent()` | `uvarpro` fit | Variable dependency graph | +| `gg_beta_uvarpro()` | `uvarpro` fit | Per-variable lasso-beta importance | +| `gg_sdependent()` | `uvarpro` fit | Signal-variable detection | +| `varpro_feature_names()` | character vector | Original names behind one-hot encoded features | + +### SHAP + +| Function | Input | What you get | +|---|---|---| +| `gg_shap()` | `rfsrc` / `randomForest` | Shapley additive explanation values | +| `shap_importance()` | `gg_shap` object | Global importance bar chart | +| `shap_beeswarm()` | `gg_shap` object | Beeswarm summary plot | +| `shap_dependence()` | `gg_shap` object | Dependence plot for one predictor | Each `gg_*` function has a matching `plot()` S3 method that hands back a single plottable object: a `ggplot` you extend with `+`, or a `patchwork` composite for the multi-panel methods. Every `gg_*` object also has `print()` and `summary()` methods: `print()` @@ -118,6 +170,8 @@ entirely and build the figure from the tidy data yourself. See [NEWS.md](NEWS.md) for the full changelog. Recent highlights: +- **v3.5.1** `gg_roc()` on an `rfsrc` forest now honors the documented `which_outcome = 0`, which had been returning an unusable two-row object; `gg_partial_rfsrc()` rejects a non-forest with a real error instead of "argument is of length zero". Also a test-only fix for the `gcc-UBSAN` report filed against 3.5.0. +- **v3.5.0** varPro fixes: `plot.gg_varpro()` no longer draws a phantom "NA" category, `gg_partial_varpro()` warns when you name a variable the fit cannot reach, and `scale = "chf"` now honors `xvar.names` instead of computing every variable. Vignette figures render with `ragg`, which cut the source tarball from 4.7 MB to 2.3 MB. - **v3.4.0** Unsupervised varPro wrappers (`gg_beta_uvarpro()`, `gg_sdependent()`) with their own vignette; `gg_partial_rfsrc()` now handles factor predictors correctly. - **v3.3.0** varPro partial plots default to interpretable scales — probability for classification, survival S(τ) for survival. - **v3.1.0** varPro integration: release-rule importance, partial dependence, local importance, anomaly scores, and the dependency graph. diff --git a/man/calc_roc.rfsrc.Rd b/man/calc_roc.rfsrc.Rd index fc8e59f6e..fbf9a188f 100644 --- a/man/calc_roc.rfsrc.Rd +++ b/man/calc_roc.rfsrc.Rd @@ -20,8 +20,10 @@ labels, one per observation. Typically \code{object$yvar} for rfsrc or \item{which_outcome}{Integer index of the class for which the ROC curve is computed (e.g. \code{1} for the first class, \code{2} for the second). -Use \code{"all"} to request all classes (currently falls back to class 1 -with a warning).} +Use \code{"all"}, or its numeric spelling \code{0}, to request all +classes. The \code{randomForest} method returns a macro-averaged +one-vs-rest curve; the \code{rfsrc} method warns and falls back to +class 1 (see #72).} \item{oob}{Logical; if \code{TRUE} (default for rfsrc) use OOB predicted probabilities. Forced to \code{FALSE} for \code{randomForest} objects.} diff --git a/man/gg_error.Rd b/man/gg_error.Rd index 050595184..216bc0c34 100644 --- a/man/gg_error.Rd +++ b/man/gg_error.Rd @@ -138,10 +138,8 @@ gg_dta <- gg_error(rfsrc_veteran) plot(gg_dta) ## ------------- pbc data -# Load a cached randomForestSRC object -# We need to create this dataset -data(pbc, package = "randomForestSRC",) -# For whatever reason, the age variable is in days... makes no sense to me +data(pbc, package = "randomForestSRC") +# Recode: binary columns to logical, low-cardinality columns to factors. for (ind in seq_len(dim(pbc)[2])) { if (!is.factor(pbc[, ind])) { if (length(unique(pbc[which(!is.na(pbc[, ind])), ind])) <= 2) { diff --git a/man/gg_roc.rfsrc.Rd b/man/gg_roc.rfsrc.Rd index 2ea4beffe..877b8c469 100644 --- a/man/gg_roc.rfsrc.Rd +++ b/man/gg_roc.rfsrc.Rd @@ -14,10 +14,11 @@ \code{family == "class"} (rfsrc) or \code{type == "classification"} (randomForest) are supported.} -\item{which_outcome}{Integer index or character name of the class to score. -For binary forests this is usually \code{1} or \code{2}; for multi-class -forests, any valid class index or level name. \code{which_outcome = "all"} -or \code{0} behaves differently by engine: +\item{which_outcome}{Index of the class to score: \code{1} or \code{2} for +a binary forest, any valid class index for a multi-class one. A character +level name is accepted by the \code{randomForest} method only; the +\code{rfsrc} method requires an integer index. +\code{which_outcome = "all"} or \code{0} behaves differently by engine: \describe{ \item{\code{randomForest} method}{Returns a macro-averaged one-vs-rest ROC computed over the per-class probabilities.} @@ -61,6 +62,17 @@ curve traces that trade-off. For one class of a classification threshold and records sensitivity (the true positive rate) against specificity (1 minus the false positive rate). } +\section{Defaults across entry points}{ + +\code{plot(gg_roc(x))} and a direct \code{plot.gg_roc(x)} call on a raw +multi-class forest do not draw the same figure: \code{gg_roc(x)} returns a +single curve, while \code{\link{plot.gg_roc}} given a raw forest overlays +one curve per class. Plain \code{plot(x)} on a forest reaches neither, as +it dispatches to the forest's own method in \code{randomForestSRC} or +\code{randomForest}. Pass \code{which_outcome} explicitly when the +distinction matters; issue #72 tracks reconciling the two. +} + \examples{ ## ------------------------------------------------------------ ## classification example diff --git a/man/gg_vimp.Rd b/man/gg_vimp.Rd index 372a2bdf9..4566435b6 100644 --- a/man/gg_vimp.Rd +++ b/man/gg_vimp.Rd @@ -148,10 +148,8 @@ gg_dta <- gg_vimp(rfsrc_veteran) plot(gg_dta) ## -------- pbc data -# We need to create this dataset -data(pbc, package = "randomForestSRC", ) -# For whatever reason, the age variable is in days... -# makes no sense to me +data(pbc, package = "randomForestSRC") +# Recode: binary columns to logical, low-cardinality columns to factors. for (ind in seq_len(dim(pbc)[2])) { if (!is.factor(pbc[, ind])) { if (length(unique(pbc[which(!is.na(pbc[, ind])), ind])) <= 2) { diff --git a/man/plot.gg_error.Rd b/man/plot.gg_error.Rd index 96a26dbe1..96d5fbd92 100644 --- a/man/plot.gg_error.Rd +++ b/man/plot.gg_error.Rd @@ -129,10 +129,8 @@ gg_dta <- gg_error(rfsrc_veteran) plot(gg_dta) ## ------------- pbc data -# Load a cached randomForestSRC object -# We need to create this dataset -data(pbc, package = "randomForestSRC",) -# For whatever reason, the age variable is in days... makes no sense to me +data(pbc, package = "randomForestSRC") +# Recode: binary columns to logical, low-cardinality columns to factors. for (ind in seq_len(dim(pbc)[2])) { if (!is.factor(pbc[, ind])) { if (length(unique(pbc[which(!is.na(pbc[, ind])), ind])) <= 2) { diff --git a/man/plot.gg_rfsrc.Rd b/man/plot.gg_rfsrc.Rd index dc2bfc666..73bb24f65 100644 --- a/man/plot.gg_rfsrc.Rd +++ b/man/plot.gg_rfsrc.Rd @@ -117,7 +117,7 @@ plot(gg_dta) ## -------- pbc data (larger dataset -- skipped on CRAN) \donttest{ data(pbc, package = "randomForestSRC") -# For whatever reason, the age variable is in days; convert to years +# Recode: binary columns to logical, low-cardinality columns to factors. for (ind in seq_len(dim(pbc)[2])) { if (!is.factor(pbc[, ind])) { if (length(unique(pbc[which(!is.na(pbc[, ind])), ind])) <= 2) { diff --git a/man/plot.gg_roc.Rd b/man/plot.gg_roc.Rd index 7e03515fb..24a75ef1d 100644 --- a/man/plot.gg_roc.Rd +++ b/man/plot.gg_roc.Rd @@ -9,8 +9,23 @@ \arguments{ \item{x}{A \code{\link{gg_roc}} object, or a raw \code{\link[randomForestSRC]{rfsrc}} or -\code{\link[randomForest]{randomForest}} classification forest. Hand it a -forest and \code{\link{gg_roc}} is called for you.} +\code{\link[randomForest]{randomForest}} classification forest. + +A raw forest is accepted, but plain \code{plot(forest)} does not arrive +here. Both \code{randomForestSRC} and \code{randomForest} register their +own \code{plot} methods, so S3 dispatch sends a raw forest to +\code{\link[randomForestSRC]{plot.rfsrc}} or \code{plot.randomForest} +instead. This branch is reached only by naming the method outright, as +\code{plot.gg_roc(forest)}. + +That branch also does not use \code{gg_roc}'s own default for +\code{which_outcome}: given a multi-class forest and +\code{which_outcome = NULL} it calls \code{\link{gg_roc}} once per class +and overlays the one-vs-rest curves, where \code{gg_roc(x)} alone returns +a single curve -- a macro-average for \code{randomForest}, or class 1 +with a warning for \code{rfsrc}. Prefer +\code{plot(gg_roc(x, which_outcome))}, which is explicit about both the +class and the engine. Issue #72 tracks reconciling the entry points.} \item{which_outcome}{Integer; for multi-class problems, the index of the class to plot. When \code{NULL} (default) and the forest has more than two diff --git a/tests/testthat/test_gg_partial_rfsrc.R b/tests/testthat/test_gg_partial_rfsrc.R index 3e9405020..78db6d93c 100644 --- a/tests/testthat/test_gg_partial_rfsrc.R +++ b/tests/testthat/test_gg_partial_rfsrc.R @@ -53,3 +53,17 @@ test_that("gg_partial_rfsrc factor partial dependence matches ground truth", { expect_s3_class(g$categorical$x, "factor") expect_identical(levels(g$categorical$x), lv) }) + +test_that("gg_partial_rfsrc rejects a non-rfsrc rf_model with a package error", { + # Previously $xvar / $xvar.names were dereferenced before any class check, + # so a bad model died with base R's "argument is of length zero". + expect_error( + gg_partial_rfsrc(list(), xvar.names = "x"), + "expected an 'rfsrc' object" + ) + expect_error( + gg_partial_rfsrc(list(), xvar.names = "x"), + "class list" + ) + expect_error(gg_partial_rfsrc(NULL), "expected an 'rfsrc' object") +}) diff --git a/tests/testthat/test_gg_roc.R b/tests/testthat/test_gg_roc.R index f7c2a3058..b4f5afb2c 100644 --- a/tests/testthat/test_gg_roc.R +++ b/tests/testthat/test_gg_roc.R @@ -250,6 +250,28 @@ test_that("calc_roc.rfsrc output is unchanged for an explicit which_outcome (gua expect_gte(calc_auc(g), 0.9) # rfsrc iris setosa-vs-rest stays strong }) +test_that("gg_roc rfsrc: which_outcome = 0 takes the same route as 'all'", { + set.seed(42) + rfsrc_iris <- randomForestSRC::rfsrc(Species ~ ., data = iris, ntree = 50) + + # 0 is the documented numeric spelling of "all". Before the fix it fell + # through to predicted[, 0], which is a legal zero-column subset, so the + # threshold sweep ran on empty input and produced a two-row frame whose + # columns were X1/X2/pct -- calc_auc() then failed on the missing $spec. + expect_warning(g_zero <- gg_roc(rfsrc_iris, which_outcome = 0), + "falling back to class 1") + expect_warning(g_all <- gg_roc(rfsrc_iris, which_outcome = "all"), + "falling back to class 1") + + expect_s3_class(g_zero, "gg_roc") + expect_true(all(c("sens", "spec", "pct") %in% colnames(g_zero))) + expect_gt(nrow(g_zero), 2L) + expect_equal(as.data.frame(g_zero), as.data.frame(g_all)) + + # calc_auc() has to survive the object the fallback hands back. + expect_equal(calc_auc(g_zero), calc_auc(gg_roc(rfsrc_iris, which_outcome = 1))) +}) + ## ── per_class = TRUE (PR #88) ────────────────────────────────────────────── test_that("gg_roc per_class=TRUE: long format with class column", { diff --git a/tests/testthat/test_lint.R b/tests/testthat/test_lint.R index b3aa7bb28..3a6db183d 100644 --- a/tests/testthat/test_lint.R +++ b/tests/testthat/test_lint.R @@ -1,5 +1,11 @@ if (requireNamespace("lintr", quietly = TRUE)) { - context("lints") - # test_that("Package Style", - # lintr::expect_lint_free()) + test_that("Package Style", { + # Skipped on CRAN for two reasons: lint_package() needs the package + # source tree, which a check of the installed package does not have, + # and the overall check has a hard sub-10-minute budget. Locally this + # costs ~15s under devtools::test(); CI also runs a dedicated lint job. + skip_on_cran() + skip_if_not_installed("lintr") + lintr::expect_lint_free() + }) }