Skip to content

Commit 20fcde4

Browse files
authored
fastVoteR 0.0.3 update (#158)
* link to fastVoteR PR * convert to data.table * add rm_zero_features() method * update NEWS * github version of fastVoteR * force minimum version (0.0.3) for fastVoteR package * use lgr logging
1 parent c7a1695 commit 20fcde4

6 files changed

Lines changed: 110 additions & 25 deletions

File tree

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Imports:
3838
stabm
3939
Suggests:
4040
e1071,
41-
fastVoteR,
41+
fastVoteR (>= 0.0.3),
4242
genalg,
4343
mirai,
4444
mlr3learners,

NEWS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# mlr3fselect (development version)
22

33
* refactor: Remove rush backward compatibility.
4-
* docs: add hEFS reference.
4+
* docs: Add hEFS reference.
5+
* compatibility: `fastVoteR` 0.0.3
6+
* feat: Add `$rm_zero_features()` method in `EnsembleFSResult` to remove result rows where no features were selected.
57

68
# mlr3fselect 1.5.1
79

R/EnsembleFSResult.R

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,55 @@ EnsembleFSResult = R6Class(
152152
open_help(self$man)
153153
},
154154

155+
#' @description
156+
#' Removes rows from the ensemble feature selection result where no features were selected.
157+
#'
158+
#' If a benchmark result is stored, the corresponding resampling iterations are removed as well.
159+
#' The stability measures are reset and need to be recalculated after this operation.
160+
#'
161+
#' This method modifies the object by reference.
162+
#' To preserve the original state, explicitly `$clone()` the object beforehand.
163+
#'
164+
#' @return
165+
#' Returns the object itself, but modified **by reference**.
166+
rm_zero_features = function() {
167+
keep = private$.result$n_features > 0L
168+
n_removed = sum(!keep)
169+
170+
if (n_removed > 0L) {
171+
if (!is.null(self$benchmark_result)) {
172+
# filter the resample results of the benchmark result
173+
resample_results = self$benchmark_result$resample_results$resample_result
174+
filtered_resample_results = list()
175+
keep_rows = which(keep)
176+
row_start = 0L
177+
178+
for (rr in resample_results) {
179+
row_end = row_start + rr$iters
180+
iters = keep_rows[keep_rows > row_start & keep_rows <= row_end] - row_start
181+
row_start = row_end
182+
183+
if (length(iters)) {
184+
rr$filter(iters = iters)
185+
filtered_resample_results = c(filtered_resample_results, list(rr))
186+
}
187+
}
188+
189+
self$benchmark_result = if (length(filtered_resample_results)) {
190+
do.call(c, filtered_resample_results)
191+
} else {
192+
NULL
193+
}
194+
}
195+
private$.result = private$.result[keep]
196+
private$.stability_global = NULL
197+
private$.stability_learner = NULL
198+
lg$info(sprintf("%s results with zero selected features have been removed.", n_removed))
199+
}
200+
201+
invisible(self)
202+
},
203+
155204
#' @description
156205
#' Use this function to change the active measure.
157206
#'
@@ -259,8 +308,8 @@ EnsembleFSResult = R6Class(
259308
#' i.e. it can be used to compare the feature rankings across different methods.
260309
#'
261310
#' We shuffle the input candidates/features so that we enforce random tie-breaking.
262-
#' Users should set the same `seed` for consistent comparison between the different feature ranking methods
263-
#' and for reproducibility.
311+
#' Users should set the same `seed` for consistent comparison between the different
312+
#' feature ranking methods and for reproducibility.
264313
#'
265314
#' @param method (`character(1)`)\cr
266315
#' The method to calculate the feature ranking. See [fastVoteR::rank_candidates()]
@@ -269,12 +318,14 @@ EnsembleFSResult = R6Class(
269318
#' @param use_weights (`logical(1)`)\cr
270319
#' The default value (`TRUE`) uses weights equal to the performance scores
271320
#' of each voter/model (or the inverse scores if the measure is minimized).
272-
#' If `FALSE`, we treat all voters as equal and assign them all a weight equal to 1.
321+
#' Note that the performance scores need to be non-negative for the weights
322+
#' to be meaningful. If the scores can be negative, it is recommended to set
323+
#' `use_weights = FALSE`, which treats all voters as equal and assigns them
324+
#' the same weight equal to 1.
273325
#' @param committee_size (`integer(1)`)\cr
274-
#' Number of top selected features in the output ranking.
275-
#' This parameter can be used to speed-up methods that build a committee sequentially
276-
#' (`"seq_pav"`), by requesting only the top N selected candidates/features
277-
#' and not the complete feature ranking.
326+
#' The number of top-ranked features to return.
327+
#' This can speed up methods that build a committee sequentially (e.g., `"seq_pav"`)
328+
#' by computing only the top N candidates rather than the full ranking.
278329
#' @param shuffle_features (`logical(1)`)\cr
279330
#' Whether to shuffle the task features randomly before computing the ranking.
280331
#' Shuffling ensures consistent random tie-breaking across methods and prevents
@@ -293,7 +344,6 @@ EnsembleFSResult = R6Class(
293344
#' where the top feature receives a score of 1 and the lowest-ranked feature receives a score of 0.
294345
#' This column is always included so that feature ranking methods that output only rankings
295346
#' have also a feature-wise score.
296-
#'
297347
feature_ranking = function(method = "av", use_weights = TRUE, committee_size = NULL, shuffle_features = TRUE) {
298348
requireNamespace("fastVoteR")
299349

@@ -315,14 +365,16 @@ EnsembleFSResult = R6Class(
315365
}
316366

317367
# get consensus feature ranking
318-
res = fastVoteR::rank_candidates(
319-
voters = voters,
320-
candidates = candidates,
321-
weights = weights,
322-
committee_size = committee_size,
323-
method = method,
324-
borda_score = TRUE,
325-
shuffle_candidates = shuffle_features
368+
res = as.data.table(
369+
fastVoteR::rank_candidates(
370+
voters = voters,
371+
candidates = candidates,
372+
weights = weights,
373+
committee_size = committee_size,
374+
method = method,
375+
borda_score = TRUE,
376+
shuffle_candidates = shuffle_features
377+
)
326378
)
327379

328380
setnames(res, "candidate", "feature")

man/ensemble_fs_result.Rd

Lines changed: 9 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test_embedded_ensemble_fselect.R

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@ test_that("embedded efs works", {
6262
feature_ranking = efsr$feature_ranking()
6363
expect_data_table(feature_ranking, nrows = length(task$feature_names))
6464
expect_equal(names(feature_ranking), c("feature", "score", "norm_score", "borda_score"))
65+
66+
# remove zero features (all rows with the featureless learner should be removed)
67+
efsr_zero = efsr$clone(deep = TRUE)
68+
efsr_zero$rm_zero_features()
69+
expect_data_table(efsr_zero$result, nrows = 5L)
70+
expect_true(all(efsr_zero$result$n_features > 0L))
71+
expect_equal(efsr_zero$n_learners, 1L)
72+
expect_equal(efsr_zero$n_resamples, 5L)
73+
expect_equal(efsr_zero$benchmark_result$n_resample_results, 1L)
6574
})
6675

6776
test_that("combine embedded efs results", {

tests/testthat/test_ensemble_fselect.R

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,26 @@ test_that("combining EnsembleFSResult objects", {
383383
expect_null(get_private(comb_all)$.inner_measure$id)
384384
})
385385

386+
test_that("EnsembleFSResult can remove zero-feature results", {
387+
result = data.table(
388+
resampling_iteration = c(1L, 2L, 3L),
389+
learner_id = c("lrn1", "lrn1", "lrn2"),
390+
n_features = c(0L, 2L, 0L),
391+
features = list(character(), c("V1", "V2"), character()),
392+
classif.ce = c(0.5, 0.2, 0.4)
393+
)
394+
efsr = EnsembleFSResult$new(result = result, features = paste0("V", 1:2), measure = msr("classif.ce"))
395+
396+
efsr$rm_zero_features()
397+
expect_data_table(efsr$result, nrows = 1L)
398+
expect_equal(efsr$result$resampling_iteration, 2L)
399+
expect_equal(efsr$result$learner_id, "lrn1")
400+
expect_equal(efsr$result$n_features, 2L)
401+
expect_equal(efsr$result$features[[1L]], c("V1", "V2"))
402+
expect_equal(efsr$n_learners, 1L)
403+
expect_equal(efsr$n_resamples, 1L)
404+
})
405+
386406
test_that("different callbacks can be set", {
387407
callback_test = callback_batch_fselect("mlr3fselect.test", on_eval_before_archive = function(callback, context) {
388408
context$aggregated_performance[, callback_active := context$instance$objective$learner$id == "classif.rpart"]

0 commit comments

Comments
 (0)