Skip to content

Commit 79ce7a5

Browse files
authored
fix(one_se_rule): handle small archives and keep n_features an integer (#174)
1 parent deabd37 commit 79ce7a5

3 files changed

Lines changed: 26 additions & 3 deletions

File tree

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# mlr3fselect (development version)
22

33
* fix: `ArchiveAsyncFSelect` pushed results with the removed `rush::Rush$push_results()` method.
4+
* fix: The `mlr3fselect.one_se_rule` callback errored on archives with a single evaluation or with missing scores, and wrote the `n_features` column as a list column instead of an integer column (#174).
45
* fix: `extract_inner_fselect_results()` added the `iteration` and `fselect_instance` columns to the result of the inner `FSelectInstance` by reference, which created a circular reference between the instance and its own result (#172).
56
* fix: `fs("rfe")` and `fs("rfecv")` failed with an internal `data.table` error when `store_benchmark_result = FALSE` was set because the importance scores were read from the benchmark result of the archive (#169).
67
* fix: `fs("rfecv", recursive = FALSE)` failed with an internal `data.table` error because the importance scores of all resampling iterations were written to a single archive row (#168).

R/mlr_callbacks.R

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,13 @@ load_callback_one_se_rule = function() {
166166
on_optimization_end = function(callback, context) {
167167
archive = context$instance$archive
168168
data = as.data.table(archive)
169-
data[, "n_features" := map(get("features"), length)]
169+
data[, "n_features" := lengths(get("features"))]
170170

171171
# standard error
172+
# `sd()` is `NA` for a single evaluation, in this case the smallest feature set is selected
172173
y = data[[archive$cols_y]]
173-
se = sd(y) / sqrt(length(y))
174+
n = sum(!is.na(y))
175+
se = if (n < 2L) 0 else sd(y, na.rm = TRUE) / sqrt(n)
174176

175177
columns_to_keep = setdiff(names(context$instance$result), "x_domain")
176178
if (se == 0) {
@@ -179,9 +181,10 @@ load_callback_one_se_rule = function() {
179181
data[, columns_to_keep, with = FALSE][which.min(n_features)]
180182
} else {
181183
# select smallest future set within one standard error of the best
184+
# `which()` drops feature sets without a score
182185
best_y = context$instance$result_y
183186
context$instance$.__enclos_env__$private$.result =
184-
data[y > best_y - se & y < best_y + se, columns_to_keep, with = FALSE][which.min(n_features)]
187+
data[which(y > best_y - se & y < best_y + se), columns_to_keep, with = FALSE][which.min(n_features)]
185188
}
186189
}
187190
)

tests/testthat/test_mlr_callbacks.R

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,25 @@ test_that("one_se_rule callback works", {
5252
)
5353

5454
expect_equal(instance$result_feature_set, c("x1", "x2", "x3"))
55+
# the number of features is a scalar and not a list column
56+
expect_integer(instance$result$n_features, len = 1)
57+
})
58+
59+
test_that("one_se_rule callback works with a single evaluation", {
60+
instance = fselect(
61+
fselector = fs("random_search", batch_size = 1),
62+
task = TEST_MAKE_TSK(),
63+
learner = lrn("regr.rpart"),
64+
resampling = rsmp("cv", folds = 3),
65+
measures = msr("dummy"),
66+
term_evals = 1,
67+
callbacks = clbk("mlr3fselect.one_se_rule")
68+
)
69+
70+
# the standard error of a single evaluation is `NA`
71+
expect_data_table(instance$archive$data, nrows = 1)
72+
expect_data_table(instance$result, nrows = 1)
73+
expect_equal(instance$result$features[[1]], as.data.table(instance$archive)$features[[1]])
5574
})
5675

5776
test_that("internal tuning callback works", {

0 commit comments

Comments
 (0)