Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Imports:
generics,
jsonlite,
knitr,
lifecycle,
purrr,
rlang (>= 1.1.1),
tibble,
Expand Down
24 changes: 24 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ S3method(parse_model,xgb.Booster)
S3method(parse_model,xrf)
S3method(print,tidypredict_test)
S3method(tidy,pm_regression)
S3method(tidypredict_class_exprs,default)
S3method(tidypredict_class_exprs,earth)
S3method(tidypredict_class_exprs,multnet)
S3method(tidypredict_class_exprs,party)
S3method(tidypredict_class_exprs,rpart)
S3method(tidypredict_class_trees,default)
S3method(tidypredict_class_trees,randomForest)
S3method(tidypredict_class_trees,ranger)
S3method(tidypredict_fit,"_xgb.Booster")
S3method(tidypredict_fit,C5.0)
S3method(tidypredict_fit,H2OBinomialModel)
Expand Down Expand Up @@ -111,6 +119,12 @@ S3method(tidypredict_interval,default)
S3method(tidypredict_interval,glm)
S3method(tidypredict_interval,list)
S3method(tidypredict_interval,lm)
S3method(tidypredict_n_trees,catboost.Model)
S3method(tidypredict_n_trees,default)
S3method(tidypredict_n_trees,lgb.Booster)
S3method(tidypredict_n_trees,randomForest)
S3method(tidypredict_n_trees,ranger)
S3method(tidypredict_n_trees,xgb.Booster)
S3method(tidypredict_test,"_xgb.Booster")
S3method(tidypredict_test,C5.0)
S3method(tidypredict_test,H2OBinomialModel)
Expand Down Expand Up @@ -149,6 +163,12 @@ S3method(tidypredict_test,rpart)
S3method(tidypredict_test,sda)
S3method(tidypredict_test,xgb.Booster)
S3method(tidypredict_test,xrf)
S3method(tidypredict_trees,catboost.Model)
S3method(tidypredict_trees,default)
S3method(tidypredict_trees,lgb.Booster)
S3method(tidypredict_trees,randomForest)
S3method(tidypredict_trees,ranger)
S3method(tidypredict_trees,xgb.Booster)
export(.build_case_when_tree)
export(.build_linear_pred)
export(.build_nested_case_when_tree)
Expand All @@ -175,14 +195,18 @@ export(as_parsed_model)
export(parse_model)
export(set_catboost_categories)
export(tidy)
export(tidypredict_class_exprs)
export(tidypredict_class_trees)
export(tidypredict_fit)
export(tidypredict_interval)
export(tidypredict_load)
export(tidypredict_n_trees)
export(tidypredict_save)
export(tidypredict_sql)
export(tidypredict_sql_interval)
export(tidypredict_test)
export(tidypredict_to_column)
export(tidypredict_trees)
import(rlang)
importFrom(dplyr,case_when)
importFrom(dplyr,mutate)
Expand Down
6 changes: 6 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

- Fixed `cforest()` models failing with "'language' object cannot be coerced to type 'symbol'" under partykit 1.3-0. That release added a shim to partykit's methods that identifies the caller with `as.name()`, which errors when the generic is reached as `partykit::gettree()`. (#434)

- New generics expose the pieces `tidypredict_fit()` is assembled from, so that packages generating their own code from a fitted model can reuse tidypredict's parsing: `tidypredict_trees()` returns per-tree expressions, `tidypredict_class_trees()` returns per-tree expressions for each outcome level, `tidypredict_class_exprs()` returns one finished expression per outcome level, and `tidypredict_n_trees()` returns the number of trees. See `?tidypredict_extractors`. (#433)

- The eleven `.extract_*()` functions are deprecated in favour of those generics. They were exported but documented as internal, and each is now a thin wrapper that warns. Two of them change return type under the new names: `.extract_earth_multiclass()` and `.extract_glmnet_multiclass()` returned deparsed strings, while `tidypredict_class_exprs()` returns language objects like every other extractor. (#433)

- `tidypredict_class_exprs()` on a `partykit` model is named by outcome level. The `.extract_partykit_classprob()` it replaces returned an unnamed list, which left callers assuming its order matched `levels()` of the outcome. (#433)

- The error raised when no method knows how to handle a model at all now carries the condition class `tidypredict_unsupported_model`. Many other errors also say "are not supported", but they report an unsupported *configuration* of a model that is otherwise handled, so the wording alone could not distinguish the two. Packages that wrap `tidypredict_fit()`, such as orbital, need that distinction to decide whether to fall back or to report the model as unsupported. (#432)

- New articles for `kernlab::ksvm()`, `mboost::blackboost()` and `xrf::xrf()`, and the model list menu now links to the `LiblineaR` and `quantreg` sections directly. (#317)
Expand Down
198 changes: 198 additions & 0 deletions R/deprec-extractors.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# Deprecated in favour of the generics in extractors.R.
#
# These were exported and documented with `\keyword{internal}` for orbital's
# use. They are kept as thin wrappers for one cycle rather than removed
# outright, since being exported means an unknown caller is possible.
#
# `deprecate_soft()` rather than `deprecate_warn()`: orbital still calls these
# from its own namespace, and warning there would surface a message to orbital
# users about a function they did not call and cannot avoid. Soft deprecation
# still warns a direct caller, which is who can act on it.
#
# Two of them change return type as well as name, and cannot be made
# type-compatible: `.extract_earth_multiclass()` and
# `.extract_glmnet_multiclass()` returned deparsed strings, while
# `tidypredict_class_exprs()` returns language objects like every other
# extractor. The wrappers deparse the result to keep the old behaviour intact.

#' Deprecated model extractors
#'
#' @description
#' `r lifecycle::badge("deprecated")`
#'
#' These functions have been replaced by generics with documented return
#' shapes. See [tidypredict_extractors].
#'
#' | Deprecated | Replacement |
#' | --- | --- |
#' | `.extract_xgb_trees()`, `.extract_lgb_trees()`, `.extract_catboost_trees()`, `.extract_rf_trees()`, `.extract_ranger_trees()` | [tidypredict_trees()] |
#' | `.extract_rf_classprob()`, `.extract_ranger_classprob()` | [tidypredict_class_trees()] |
#' | `.extract_rpart_classprob()`, `.extract_partykit_classprob()`, `.extract_earth_multiclass()`, `.extract_glmnet_multiclass()` | [tidypredict_class_exprs()] |
#'
#' @param model A fitted model object.
#' @param penalty The penalty value to use for coefficient extraction.
#'
#' @returns The same values these functions have always returned. Note that
#' [tidypredict_class_exprs()] returns language objects where
#' `.extract_earth_multiclass()` and `.extract_glmnet_multiclass()` return
#' strings.
#'
#' @keywords internal
#' @name deprecated-extractors
NULL

#' @rdname deprecated-extractors
#' @export
.extract_xgb_trees <- function(model) {
lifecycle::deprecate_soft(
"1.1.1",
".extract_xgb_trees()",
"tidypredict_trees()"
)
check_extractor_class(model, "xgb.Booster")
tidypredict_trees(model)
}

#' @rdname deprecated-extractors
#' @export
.extract_lgb_trees <- function(model) {
lifecycle::deprecate_soft(
"1.1.1",
".extract_lgb_trees()",
"tidypredict_trees()"
)
check_extractor_class(model, "lgb.Booster")
tidypredict_trees(model)
}

#' @rdname deprecated-extractors
#' @export
.extract_catboost_trees <- function(model) {
lifecycle::deprecate_soft(
"1.1.1",
".extract_catboost_trees()",
"tidypredict_trees()"
)
check_extractor_class(model, "catboost.Model")
tidypredict_trees(model)
}

#' @rdname deprecated-extractors
#' @export
.extract_rf_trees <- function(model) {
lifecycle::deprecate_soft(
"1.1.1",
".extract_rf_trees()",
"tidypredict_trees()"
)
check_extractor_class(model, "randomForest")
tidypredict_trees(model)
}

#' @rdname deprecated-extractors
#' @export
.extract_ranger_trees <- function(model) {
lifecycle::deprecate_soft(
"1.1.1",
".extract_ranger_trees()",
"tidypredict_trees()"
)
check_extractor_class(model, "ranger")
tidypredict_trees(model)
}

#' @rdname deprecated-extractors
#' @export
.extract_rf_classprob <- function(model) {
lifecycle::deprecate_soft(
"1.1.1",
".extract_rf_classprob()",
"tidypredict_class_trees()"
)
check_extractor_class(model, "randomForest")
tidypredict_class_trees(model)
}

#' @rdname deprecated-extractors
#' @export
.extract_ranger_classprob <- function(model) {
lifecycle::deprecate_soft(
"1.1.1",
".extract_ranger_classprob()",
"tidypredict_class_trees()"
)
check_extractor_class(model, "ranger")
tidypredict_class_trees(model)
}

#' @rdname deprecated-extractors
#' @export
.extract_rpart_classprob <- function(model) {
lifecycle::deprecate_soft(
"1.1.1",
".extract_rpart_classprob()",
"tidypredict_class_exprs()"
)
check_extractor_class(model, "rpart")
# This one was already named by outcome level, so it is passed through as is.
tidypredict_class_exprs(model)
}

#' @rdname deprecated-extractors
#' @export
.extract_partykit_classprob <- function(model) {
lifecycle::deprecate_soft(
"1.1.1",
".extract_partykit_classprob()",
"tidypredict_class_exprs()"
)
check_extractor_class(model, "party")
# The generic gained outcome-level names; this returned an unnamed list.
unname(tidypredict_class_exprs(model))
}

#' @rdname deprecated-extractors
#' @export
.extract_earth_multiclass <- function(model) {
lifecycle::deprecate_soft(
"1.1.1",
".extract_earth_multiclass()",
"tidypredict_class_exprs()"
)
check_extractor_class(model, "earth")
deparse_class_exprs(tidypredict_class_exprs(model))
}

#' @rdname deprecated-extractors
#' @export
.extract_glmnet_multiclass <- function(model, penalty = NULL) {
lifecycle::deprecate_soft(
"1.1.1",
".extract_glmnet_multiclass()",
"tidypredict_class_exprs()"
)
check_extractor_class(model, "multnet")
deparse_class_exprs(tidypredict_class_exprs(model, penalty = penalty))
}

# The old functions checked the class themselves and reported it against
# `model`. Dispatch would report it against `x` and name a different function,
# so the check is kept here to leave the deprecated messages unchanged.
check_extractor_class <- function(
model,
cls,
call = rlang::caller_env()
) {
if (!inherits(model, cls)) {
cli::cli_abort(
"{.arg model} must be {.cls {cls}}, not {.obj_type_friendly {model}}.",
call = call
)
}

invisible(model)
}

deparse_class_exprs <- function(x) {
lapply(x, function(e) deparse1(e, control = "digits17"))
}
118 changes: 118 additions & 0 deletions R/extractors.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#' Extract model internals as expressions
#'
#' @description
#' These generics expose the pieces `tidypredict_fit()` is assembled from,
#' rather than the finished formula. They exist so that packages generating
#' their own code from a fitted model, such as orbital, can reuse
#' tidypredict's parsing instead of reimplementing it per model class.
#'
#' Each generic has a single fixed return shape, described below. A model class
#' implements whichever generics make sense for it: a random forest has trees
#' and a tree count, a `glmnet` multinomial model has neither.
#'
#' @param x A fitted model object.
#' @param ... Additional arguments passed to methods. `multnet` models accept
#' `penalty`, which is required when the model was fitted with more than one
#' value of lambda.
#'
#' @returns
#' `tidypredict_trees()` returns an unnamed list with one element per tree,
#' each a language object.
#'
#' `tidypredict_class_trees()` returns a list named by outcome level, in model
#' order. Each element is itself an unnamed list of per-tree language objects
#' for that level, so the result is `tidypredict_trees()` nested one level
#' deeper. What the leaves hold depends on the model: `randomForest` gives
#' 0/1 votes, `ranger` gives class probabilities.
#'
#' `tidypredict_class_exprs()` returns a list named by outcome level, in model
#' order, with one language object per level. Unlike
#' `tidypredict_class_trees()` there is no per-tree structure and nothing to
#' combine: each expression computes that level's value on its own.
#'
#' `tidypredict_n_trees()` returns a single integer, the number of trees in the
#' ensemble. For multiclass boosters this counts every tree, including the
#' per-class copies, so it is not the same as the number of boosting rounds.
#'
#' Wherever an expression is described above, a **bare numeric value** may
#' appear in its place when the model has nothing to branch on. Callers must
#' handle both. This happens for a single-leaf tree, a stump, and also for a
#' degenerate expression such as a `glmnet` class whose coefficients are all
#' zero. Note that the constant can appear alongside language objects in the
#' same result, so the element type is not uniform within one list.
#'
#' @details
#' Two shapes that look similar are worth keeping apart.
#' `tidypredict_class_trees()` returns many trees per level that a caller has
#' to sum or average, and needs `tidypredict_n_trees()` to do it.
#' `tidypredict_class_exprs()` returns one finished expression per level. Both
#' are named by outcome level so that callers never have to assume the order
#' matches `levels()` of the outcome.
#'
#' @examplesIf rlang::is_installed("randomForest")
#' model <- randomForest::randomForest(mpg ~ ., data = mtcars, ntree = 5)
#'
#' tidypredict_n_trees(model)
#'
#' trees <- tidypredict_trees(model)
#' length(trees)
#' trees[[1]]
#'
#' @name tidypredict_extractors
NULL

#' @rdname tidypredict_extractors
#' @export
tidypredict_trees <- function(x, ...) {
UseMethod("tidypredict_trees")
}

#' @export
tidypredict_trees.default <- function(x, ...) {
abort_no_extractor(x, "tidypredict_trees")
}

#' @rdname tidypredict_extractors
#' @export
tidypredict_class_trees <- function(x, ...) {
UseMethod("tidypredict_class_trees")
}

#' @export
tidypredict_class_trees.default <- function(x, ...) {
abort_no_extractor(x, "tidypredict_class_trees")
}

#' @rdname tidypredict_extractors
#' @export
tidypredict_class_exprs <- function(x, ...) {
UseMethod("tidypredict_class_exprs")
}

#' @export
tidypredict_class_exprs.default <- function(x, ...) {
abort_no_extractor(x, "tidypredict_class_exprs")
}

#' @rdname tidypredict_extractors
#' @export
tidypredict_n_trees <- function(x, ...) {
UseMethod("tidypredict_n_trees")
}

#' @export
tidypredict_n_trees.default <- function(x, ...) {
abort_no_extractor(x, "tidypredict_n_trees")
}

# Distinct from abort_model_unsupported(): the model class may well be
# supported by tidypredict_fit() and simply not expose this particular piece.
# A random forest has trees, a glmnet model does not.
abort_no_extractor <- function(x, generic, call = rlang::caller_env()) {
cli::cli_abort(
"{.fn {generic}} is not available for models of class
{.cls {class(x)[[1]]}}.",
class = "tidypredict_no_extractor",
call = call
)
}
Loading
Loading