Skip to content

Commit 4f2ce58

Browse files
authored
refactor: name variables after what they mean (#197)
* `FSelectorBatchSequential`: `x`/`y`/`z` communicated nothing and the inner `if` re-tested the condition `z` already filtered on. The scalar `ifelse()` calls are replaced by a single `add_feature` flag. * `AutoFSelector`: the two near-identical row id checks are folded into one loop that uses `$train_set()` / `$test_set()`. * `AutoFSelector`: `stopf("Learner ''%s' cannot calculate important scores.")` had a doubled quote and said "important" instead of "importance". * `mlr_callbacks`: `load_callback_one_se_rule()` assigned to a variable that is never read, unlike its four sibling loaders. * The `({ ... })` wrapper around the `repeat` body has no effect.
1 parent ce25460 commit 4f2ce58

5 files changed

Lines changed: 55 additions & 44 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: `AutoFSelector$train()` did not check the row ids of an instantiated inner resampling for cross-validation and reported a wrong set number for holdout (#197).
45
* fix: The `mlr3fselect.svm_rfe` callback accepted support vector machines without a `type` or `kernel` setting, although only `type = "C-classification"` and `kernel = "linear"` are supported. The callback now also errors on multi-class tasks for which the importance scores are not defined (#173).
56
* fix: The asynchronous feature selection ignored the `always_included` column role. Columns with this role were excluded from the models instead of being added to every feature subset (#175).
67
* 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).

R/AutoFSelector.R

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ AutoFSelector = R6Class(
188188
#' @return Named `numeric()`.
189189
importance = function() {
190190
if ("importance" %nin% self$instance_args$learner$properties) {
191-
stopf("Learner ''%s' cannot calculate important scores.", self$instance_args$learner$id)
191+
stopf("Learner '%s' cannot calculate importance scores.", self$instance_args$learner$id)
192192
}
193193
if (is.null(self$model$learner$model)) {
194194
self$instance_args$learner$importance()
@@ -204,7 +204,7 @@ AutoFSelector = R6Class(
204204
#' @return `character()`.
205205
selected_features = function() {
206206
if ("selected_features" %nin% self$instance_args$learner$properties) {
207-
stopf("Learner ''%s' cannot select features.", self$instance_args$learner$id)
207+
stopf("Learner '%s' cannot select features.", self$instance_args$learner$id)
208208
}
209209
if (is.null(self$model$learner$model)) {
210210
self$instance_args$learner$selected_features()
@@ -349,29 +349,22 @@ AutoFSelector = R6Class(
349349
ia$task = task$clone()
350350

351351
# check if task contains all row ids required for instantiated resampling
352+
# `$train_set()` and `$test_set()` are used because the layout of `$instance` differs between resamplings
352353
if (ia$resampling$is_instantiated) {
353-
imap(ia$resampling$instance$train, function(x, i) {
354-
if (!test_subset(x, task$row_ids)) {
355-
stopf(
356-
"Train set %i of inner resampling '%s' contains row ids not present in task '%s': {%s}",
357-
i,
358-
ia$resampling$id,
359-
task$id,
360-
paste(setdiff(x, task$row_ids), collapse = ", ")
361-
)
362-
}
363-
})
364-
365-
imap(ia$resampling$instance$test, function(x, i) {
366-
if (!test_subset(x, task$row_ids)) {
367-
stopf(
368-
"Test set %i of inner resampling '%s' contains row ids not present in task '%s': {%s}",
369-
i,
370-
ia$resampling$id,
371-
task$id,
372-
paste(setdiff(x, task$row_ids), collapse = ", ")
373-
)
374-
}
354+
walk(seq_len(ia$resampling$iters), function(i) {
355+
sets = list(Train = ia$resampling$train_set(i), Test = ia$resampling$test_set(i))
356+
imap(sets, function(row_ids, set_type) {
357+
if (!test_subset(row_ids, task$row_ids)) {
358+
stopf(
359+
"%s set %i of inner resampling '%s' contains row ids not present in task '%s': {%s}",
360+
set_type,
361+
i,
362+
ia$resampling$id,
363+
task$id,
364+
paste(setdiff(row_ids, task$row_ids), collapse = ", ")
365+
)
366+
}
367+
})
375368
})
376369
}
377370

R/FSelectorBatchSequential.R

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -94,30 +94,27 @@ FSelectorBatchSequential = R6Class(
9494

9595
inst$eval_batch(states)
9696

97-
repeat {
98-
({
99-
if (archive$n_batch == pars$max_features - pars$min_features + 1) {
100-
break
101-
}
97+
# forward selection adds a feature to the best set, backward selection removes one
98+
add_feature = pars$strategy == "sfs"
10299

103-
res = archive$best(batch = archive$n_batch)
104-
best_state = as.logical(res[, feature_names, with = FALSE])
100+
repeat {
101+
if (archive$n_batch == pars$max_features - pars$min_features + 1) {
102+
break
103+
}
105104

106-
# Generate new states based on best feature set
107-
x = ifelse(pars$strategy == "sfs", FALSE, TRUE)
108-
y = ifelse(pars$strategy == "sfs", TRUE, FALSE)
109-
z = if (pars$strategy == "sfs") !best_state else best_state
105+
res = archive$best(batch = archive$n_batch)
106+
best_state = as.logical(res[, feature_names, with = FALSE])
110107

111-
states = map_dtr(seq_along(best_state)[z], function(i) {
112-
if (best_state[i] == x) {
113-
new_state = best_state
114-
new_state[i] = y
115-
set_names(as.list(new_state), feature_names)
116-
}
117-
})
108+
# generate new states by flipping one feature of the best feature set
109+
candidates = if (add_feature) which(!best_state) else which(best_state)
118110

119-
inst$eval_batch(states)
111+
states = map_dtr(candidates, function(i) {
112+
new_state = best_state
113+
new_state[i] = add_feature
114+
set_names(as.list(new_state), feature_names)
120115
})
116+
117+
inst$eval_batch(states)
121118
}
122119
}
123120
)

R/mlr_callbacks.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ load_callback_svm_rfe = function() {
172172
NULL
173173

174174
load_callback_one_se_rule = function() {
175-
callback = callback_batch_fselect(
175+
callback_batch_fselect(
176176
"mlr3fselect.one_se_rule",
177177
label = "One Standard Error Rule Callback",
178178
man = "mlr3fselect::mlr3fselect.one_se_rule",

tests/testthat/test_AutoFSelector.R

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,3 +284,23 @@ test_that("AutoFSelector works with async fselector", {
284284
expect_data_table(at$fselect_instance$result, nrows = 1)
285285
expect_data_table(at$fselect_instance$archive$data, min.rows = 4)
286286
})
287+
288+
test_that("instantiated resampling with foreign row ids is rejected", {
289+
run = function(key, ...) {
290+
resampling = rsmp(key, ...)
291+
resampling$instantiate(tsk("iris"))
292+
at = AutoFSelector$new(
293+
fselector = fs("random_search", batch_size = 1),
294+
learner = lrn("classif.rpart"),
295+
resampling = rsmp(key, ...),
296+
measure = msr("classif.ce"),
297+
terminator = trm("evals", n_evals = 2)
298+
)
299+
# the constructor rejects instantiated resamplings, so the check is only reachable via `$instance_args`
300+
at$instance_args$resampling = resampling
301+
at$train(tsk("iris")$filter(1:50))
302+
}
303+
304+
expect_error(run("cv", folds = 3), "set 1 of inner resampling 'cv' contains row ids")
305+
expect_error(run("holdout"), "set 1 of inner resampling 'holdout' contains row ids")
306+
})

0 commit comments

Comments
 (0)