Decision trees, using C5.0

Function Works
tidypredict_fit(), tidypredict_sql(), parse_model()
tidypredict_to_column()
tidypredict_test()
parsnip

How it works

Here is a simple C5.0() classification model using the mtcars dataset:

library(dplyr)
library(tidypredict)
library(C50)

mtcars2 <- mtcars
mtcars2$vs <- factor(mtcars2$vs)

model <- C5.0(mtcars2[, c("wt", "cyl", "mpg")], mtcars2$vs)

Under the hood

C5.0 stores its fitted tree as text in the tree element of the model object. tidypredict parses that text directly, so the model can be translated even when it was fitted through the x/y interface (as parsnip does).

The tree is transformed into a dplyr, a.k.a Tidy Eval, formula. The decision tree becomes a nested dplyr::case_when() statement.

tidypredict_fit(model)
#> case_when(cyl <= 6 ~ case_when(cyl <= 4 ~ "1", .default = case_when(wt <= 
#>     2.875 ~ "0", .default = "1")), .default = "0")

From there, the Tidy Eval formula can be used anywhere where it can be operated. tidypredict provides three paths:

  • Use directly inside dplyr, mutate(mtcars2, !! tidypredict_fit(model))
  • Use tidypredict_to_column(model) to a piped command set
  • Use tidypredict_sql(model) to retrieve the SQL statement

Categorical predictors

C5.0 handles categorical predictors natively. The generated formula uses %in% for categorical splits, and multi-way splits become nested case_when() branches:

mtcars3 <- mtcars2
mtcars3$gear <- factor(mtcars3$gear)

model_cat <- C5.0(mtcars3[, c("wt", "gear", "mpg")], mtcars3$vs)
tidypredict_fit(model_cat)
#> case_when(mpg <= 21 ~ "0", .default = "1")

parsnip

tidypredict also supports C5.0 model objects fitted via the parsnip package, both as a decision_tree() and as a boosted boost_tree().

library(parsnip)

parsnip_model <- decision_tree(mode = "classification") |>
  set_engine("C5.0") |>
  fit(vs ~ wt + cyl + mpg, data = mtcars2)

tidypredict_fit(parsnip_model)
#> case_when(cyl <= 6 ~ case_when(cyl <= 4 ~ "1", .default = case_when(wt <= 
#>     2.875 ~ "0", .default = "1")), .default = "0")

Boosted models

C5.0 can boost several trees together, which parsnip exposes through boost_tree(). Each trial casts a confidence-weighted vote, and the class with the greatest total vote is predicted. tidypredict reproduces this by summing the per-trial votes and picking the winning class:

boosted_model <- boost_tree(mode = "classification", trees = 5) |>
  set_engine("C5.0") |>
  fit(Species ~ ., data = iris)

tidypredict_fit(boosted_model)
#> case_when(case_when(Petal.Length <= 1.9 ~ 0.980769230769231, 
#>     .default = case_when(Petal.Width <= 1.7 ~ case_when(Petal.Length <= 
#>         4.9000001 ~ 0, .default = 0), .default = 0)) + case_when(Petal.Length <= 
#>     1.9 ~ 0.974901173370145, .default = case_when(Petal.Width <= 
#>     1.8 ~ case_when(Sepal.Length <= 4.9000001 ~ 0, .default = case_when(Petal.Length <= 
#>     5.3000002 ~ 0, .default = 0)), .default = 0)) + case_when(Petal.Length <= 
#>     1.9 ~ 0.967511159916569, .default = case_when(Petal.Width <= 
#>     1.7 ~ 0, .default = 0)) + case_when(Petal.Length <= 1.9 ~ 
#>     0.960182840396897, .default = case_when(Petal.Width <= 1.3 ~ 
#>     0, .default = case_when(Petal.Length <= 5.0999999 ~ case_when(Sepal.Width <= 
#>     3 ~ case_when(Sepal.Length <= 6.4000001 ~ 0, .default = 0), 
#>     .default = 0), .default = 0))) + case_when(Petal.Length <= 
#>     1.9 ~ 0.949491633289054, .default = case_when(Petal.Length <= 
#>     4.8000002 ~ case_when(Petal.Width <= 1.6 ~ 0, .default = 0), 
#>     .default = 0)) >= case_when(Petal.Length <= 1.9 ~ 0, .default = case_when(Petal.Width <= 
#>     1.7 ~ case_when(Petal.Length <= 4.9000001 ~ 0.96, .default = 0), 
#>     .default = 0)) + case_when(Petal.Length <= 1.9 ~ 0, .default = case_when(Petal.Width <= 
#>     1.8 ~ case_when(Sepal.Length <= 4.9000001 ~ 0, .default = case_when(Petal.Length <= 
#>     5.3000002 ~ 0.912209071754808, .default = 0)), .default = 0)) + 
#>     case_when(Petal.Length <= 1.9 ~ 0, .default = case_when(Petal.Width <= 
#>         1.7 ~ 0.706144609436937, .default = 0)) + case_when(Petal.Length <= 
#>     1.9 ~ 0, .default = case_when(Petal.Width <= 1.3 ~ 0.946093678405667, 
#>     .default = case_when(Petal.Length <= 5.0999999 ~ case_when(Sepal.Width <= 
#>         3 ~ case_when(Sepal.Length <= 6.4000001 ~ 0, .default = 0.893828235319099), 
#>         .default = 0.891442575444509), .default = 0))) + case_when(Petal.Length <= 
#>     1.9 ~ 0, .default = case_when(Petal.Length <= 4.8000002 ~ 
#>     case_when(Petal.Width <= 1.6 ~ 0.976712070366808, .default = 0), 
#>     .default = 0)) & case_when(Petal.Length <= 1.9 ~ 0.980769230769231, 
#>     .default = case_when(Petal.Width <= 1.7 ~ case_when(Petal.Length <= 
#>         4.9000001 ~ 0, .default = 0), .default = 0)) + case_when(Petal.Length <= 
#>     1.9 ~ 0.974901173370145, .default = case_when(Petal.Width <= 
#>     1.8 ~ case_when(Sepal.Length <= 4.9000001 ~ 0, .default = case_when(Petal.Length <= 
#>     5.3000002 ~ 0, .default = 0)), .default = 0)) + case_when(Petal.Length <= 
#>     1.9 ~ 0.967511159916569, .default = case_when(Petal.Width <= 
#>     1.7 ~ 0, .default = 0)) + case_when(Petal.Length <= 1.9 ~ 
#>     0.960182840396897, .default = case_when(Petal.Width <= 1.3 ~ 
#>     0, .default = case_when(Petal.Length <= 5.0999999 ~ case_when(Sepal.Width <= 
#>     3 ~ case_when(Sepal.Length <= 6.4000001 ~ 0, .default = 0), 
#>     .default = 0), .default = 0))) + case_when(Petal.Length <= 
#>     1.9 ~ 0.949491633289054, .default = case_when(Petal.Length <= 
#>     4.8000002 ~ case_when(Petal.Width <= 1.6 ~ 0, .default = 0), 
#>     .default = 0)) >= case_when(Petal.Length <= 1.9 ~ 0, .default = case_when(Petal.Width <= 
#>     1.7 ~ case_when(Petal.Length <= 4.9000001 ~ 0, .default = 0.625), 
#>     .default = 0.958333333333333)) + case_when(Petal.Length <= 
#>     1.9 ~ 0, .default = case_when(Petal.Width <= 1.8 ~ case_when(Sepal.Length <= 
#>     4.9000001 ~ 0.860919094267197, .default = case_when(Petal.Length <= 
#>     5.3000002 ~ 0, .default = 0.875850270460186)), .default = 0.963941744282062)) + 
#>     case_when(Petal.Length <= 1.9 ~ 0, .default = case_when(Petal.Width <= 
#>         1.7 ~ 0, .default = 0.852928413186945)) + case_when(Petal.Length <= 
#>     1.9 ~ 0, .default = case_when(Petal.Width <= 1.3 ~ 0, .default = case_when(Petal.Length <= 
#>     5.0999999 ~ case_when(Sepal.Width <= 3 ~ case_when(Sepal.Length <= 
#>     6.4000001 ~ 0.828141362462419, .default = 0), .default = 0), 
#>     .default = 0.961628192534381))) + case_when(Petal.Length <= 
#>     1.9 ~ 0, .default = case_when(Petal.Length <= 4.8000002 ~ 
#>     case_when(Petal.Width <= 1.6 ~ 0, .default = 0.62012436534794), 
#>     .default = 0.742434886685921)) ~ "setosa", case_when(Petal.Length <= 
#>     1.9 ~ 0, .default = case_when(Petal.Width <= 1.7 ~ case_when(Petal.Length <= 
#>     4.9000001 ~ 0.96, .default = 0), .default = 0)) + case_when(Petal.Length <= 
#>     1.9 ~ 0, .default = case_when(Petal.Width <= 1.8 ~ case_when(Sepal.Length <= 
#>     4.9000001 ~ 0, .default = case_when(Petal.Length <= 5.3000002 ~ 
#>     0.912209071754808, .default = 0)), .default = 0)) + case_when(Petal.Length <= 
#>     1.9 ~ 0, .default = case_when(Petal.Width <= 1.7 ~ 0.706144609436937, 
#>     .default = 0)) + case_when(Petal.Length <= 1.9 ~ 0, .default = case_when(Petal.Width <= 
#>     1.3 ~ 0.946093678405667, .default = case_when(Petal.Length <= 
#>     5.0999999 ~ case_when(Sepal.Width <= 3 ~ case_when(Sepal.Length <= 
#>     6.4000001 ~ 0, .default = 0.893828235319099), .default = 0.891442575444509), 
#>     .default = 0))) + case_when(Petal.Length <= 1.9 ~ 0, .default = case_when(Petal.Length <= 
#>     4.8000002 ~ case_when(Petal.Width <= 1.6 ~ 0.976712070366808, 
#>     .default = 0), .default = 0)) >= case_when(Petal.Length <= 
#>     1.9 ~ 0, .default = case_when(Petal.Width <= 1.7 ~ case_when(Petal.Length <= 
#>     4.9000001 ~ 0, .default = 0.625), .default = 0.958333333333333)) + 
#>     case_when(Petal.Length <= 1.9 ~ 0, .default = case_when(Petal.Width <= 
#>         1.8 ~ case_when(Sepal.Length <= 4.9000001 ~ 0.860919094267197, 
#>         .default = case_when(Petal.Length <= 5.3000002 ~ 0, .default = 0.875850270460186)), 
#>         .default = 0.963941744282062)) + case_when(Petal.Length <= 
#>     1.9 ~ 0, .default = case_when(Petal.Width <= 1.7 ~ 0, .default = 0.852928413186945)) + 
#>     case_when(Petal.Length <= 1.9 ~ 0, .default = case_when(Petal.Width <= 
#>         1.3 ~ 0, .default = case_when(Petal.Length <= 5.0999999 ~ 
#>         case_when(Sepal.Width <= 3 ~ case_when(Sepal.Length <= 
#>             6.4000001 ~ 0.828141362462419, .default = 0), .default = 0), 
#>         .default = 0.961628192534381))) + case_when(Petal.Length <= 
#>     1.9 ~ 0, .default = case_when(Petal.Length <= 4.8000002 ~ 
#>     case_when(Petal.Width <= 1.6 ~ 0, .default = 0.62012436534794), 
#>     .default = 0.742434886685921)) ~ "versicolor", .default = "virginica")

Rule-based models

Instead of a tree, C5.0 can produce an ordered set of rules (rules = TRUE), which parsnip exposes through C5_rules(). Each rule that a case satisfies casts a confidence-weighted vote for its class, and the class with the greatest total vote is predicted (the default class wins ties and cases matched by no rule). tidypredict reproduces this by summing the per-rule votes and picking the winning class:

rule_model <- C5.0(iris[, 1:4], iris$Species, rules = TRUE)

tidypredict_fit(rule_model)
#> case_when(dplyr::if_else(Petal.Length <= 1.9, 0.9807692, 0) >= 
#>     dplyr::if_else(Petal.Length > 1.9 & Petal.Length <= 4.9000001 & 
#>         Petal.Width <= 1.7, 0.96, 0) & dplyr::if_else(Petal.Length <= 
#>     1.9, 0.9807692, 0) >= dplyr::if_else(Petal.Width > 1.7, 0.9583333, 
#>     0) + dplyr::if_else(Petal.Length > 4.9000001, 0.9375, 0) ~ 
#>     "setosa", dplyr::if_else(Petal.Length > 1.9 & Petal.Length <= 
#>     4.9000001 & Petal.Width <= 1.7, 0.96, 0) >= dplyr::if_else(Petal.Width > 
#>     1.7, 0.9583333, 0) + dplyr::if_else(Petal.Length > 4.9000001, 
#>     0.9375, 0) ~ "versicolor", .default = "virginica")

Limitations

Some options change how C5.0 predicts in ways that cannot be expressed as a hard-split case_when(), so they raise an error: fuzzy thresholds (fuzzyThreshold = TRUE), models fitted with a cost matrix (costs), and boosted rule-based models (rules = TRUE with trials > 1).