Tidymodels bridge for greedy CI trees
This flat file registers ineqTrees as a parsnip engine
for decision_tree() and rand_forest(), and
exposes a yardstick-compatible concentration-index gain metric.
Assessment gain applies the fitted partition to held-out observations and compares its weighted within-leaf impurity with the unsplit assessment sample. Training gain makes the same comparison on the analysis sample used to fit that model. Ranks are recomputed within each scored leaf for the rank-dependent criteria. Forest gain averages these partition gains over member trees.
Predicted risk alone cannot identify the partition: two leaves can
have the same mean, and averaged forest predictions combine several
partitions. The engine therefore retains terminal-node IDs alongside
.pred. It also caches the analysis-sample assignments used
for training diagnostics in
attr(engine, "ci_training_diagnostics"). These assignments
matter when partykit routes a factor level absent from a member’s
training sample probabilistically. A fresh prediction can make a
different routing draw; the saved metadata identifies the draw actually
scored.
Tune on assessment gain and inspect training gain
The example uses synthetic child-level observations with complete data and unequal importance weights. Zero-weight observations, when present, do not contribute to impurity calculations. Include the socioeconomic variable in the workflow inputs so the engine can recover it. The bridge removes it from the splitting predictors in the single-outcome interface. Recipes must retain the rank/level variable and the case-weight role. Do not transform the socioeconomic levels when using L unless that transformation is part of the intended estimand.
example_data <- data.frame(
rank = seq_len(48),
outcome = as.numeric(seq_len(48) %% 7 == 0 | seq_len(48) > 28),
group = rep(seq_len(6), each = 8),
weight = rep(c(1, 2, 3), 16)
)
example_data$case_wt <- hardhat::importance_weights(example_data$weight)
tree_spec <- parsnip::decision_tree(
tree_depth = tune::tune(),
min_n = 2
)
tree_spec <- parsnip::set_engine(
tree_spec,
"ineqTrees",
rank_name = "rank",
outcome_name = "outcome",
type = "CI",
minsplit = 4,
minprob = 0
)
tree_spec <- parsnip::set_mode(tree_spec, "regression")
tree_workflow <- workflows::workflow()
tree_workflow <- workflows::add_model(tree_workflow, tree_spec)
tree_workflow <- workflows::add_formula(
tree_workflow, outcome ~ rank + group
)
tree_workflow <- workflows::add_case_weights(tree_workflow, case_wt)
set.seed(71)
example_folds <- rsample::vfold_cv(example_data, v = 3)
gain_metrics <- ci_tuning_metric_set(
example_data,
rank_name = "rank",
case_weight_name = "weight",
type = "CI",
metrics = c(
"validation_gain", "relative_validation_gain",
"training_gain", "relative_training_gain"
)
)
tree_tuning <- tune::tune_grid(
tree_workflow,
resamples = example_folds,
grid = data.frame(tree_depth = c(1L, 2L)),
metrics = gain_metrics,
control = tune::control_grid(save_pred = TRUE, allow_par = FALSE)
)
tune::collect_metrics(tree_tuning)
#> # A tibble: 8 × 7
#> tree_depth .metric .estimator mean n std_err .config
#> <int> <chr> <chr> <dbl> <int> <dbl> <chr>
#> 1 1 relative_training_gain standard 0.552 3 0.113 pre0_mod1_…
#> 2 1 relative_validation_gain standard 0.254 3 0.311 pre0_mod1_…
#> 3 1 training_gain standard 0.258 3 0.0705 pre0_mod1_…
#> 4 1 validation_gain standard 0.182 3 0.201 pre0_mod1_…
#> 5 2 relative_training_gain standard 0.630 3 0.157 pre0_mod2_…
#> 6 2 relative_validation_gain standard 0.367 3 0.266 pre0_mod2_…
#> 7 2 training_gain standard 0.298 3 0.0950 pre0_mod2_…
#> 8 2 validation_gain standard 0.219 3 0.184 pre0_mod2_…
tune::select_best(tree_tuning, metric = "validation_gain")
#> # A tibble: 1 × 2
#> tree_depth .config
#> <int> <chr>
#> 1 2 pre0_mod2_post0Relative training gain uses the analysis root as its denominator;
relative validation gain uses the assessment root. Either relative gain
is undefined when its root impurity is numerically zero and is reported
as NA. Use validation gain for selection. Training gain
describes the fitted sample and helps reveal a training-validation gap.
It does not establish generalisation or a causal effect. Keep the
metric’s type consistent with the engine, and compare
tuning candidates within the same criterion.
For reproducible numerical checks, run
/usr/bin/Rscript dev/run_tidymodels_scoring_experiments.R
from the package root. The script checks actual node assignments against
an independent weighted calculation and saves fold scores, selection
checks and versions under
dev/validation/tidymodels_scoring/.
Example usage
library(ineqTrees)
library(parsnip)
library(hardhat)
register_ineqtrees_parsnip()
tree_spec <- decision_tree(tree_depth = 4, min_n = 100) |>
set_engine(
"ineqTrees",
rank_name = "wealth",
outcome_name = "deadu5_num",
type = "CIg",
minsplit = 500,
minprob = 0.01,
min_gain = 0.001
) |>
set_mode("regression")
tree_fit <- fit(
tree_spec,
cbind(wealth, deadu5_num) ~ rural + male + reg,
data = congo_model_dt,
case_weights = hardhat::importance_weights(congo_model_dt$sample_weight)
)
predict(tree_fit, new_data = congo_model_dt)
predict(tree_fit, new_data = congo_model_dt, type = "raw")
forest_spec <- rand_forest(trees = 500, mtry = 4, min_n = 100) |>
set_engine(
"ineqTrees",
rank_name = "wealth",
outcome_name = "deadu5_num",
type = "CIg",
minsplit = 500,
minprob = 0.01,
maxdepth = 5,
min_gain = 0.001,
perturb = list(replace = FALSE, fraction = 0.632)
) |>
set_mode("regression")
forest_fit <- fit(
forest_spec,
cbind(wealth, deadu5_num) ~ rural + male + reg,
data = congo_model_dt,
case_weights = hardhat::importance_weights(congo_model_dt$sample_weight)
)
predict(forest_fit, new_data = congo_model_dt)