Дисциплина: Анализ данных
Цель работы: освоить инструменты отбора и оценки
признаков в R: пакеты caret, FSelector,
arules и Boruta.
library(caret)
library(FSelector)
library(arules)
library(Boruta)
library(mlbench)
Функция getModelInfo() возвращает описание всех моделей,
которые caret умеет обучать через единый интерфейс
train().
models <- names(getModelInfo())
length(models)
## [1] 239
head(models, 40)
## [1] "ada" "AdaBag" "AdaBoost.M1" "adaboost"
## [5] "amdai" "ANFIS" "avNNet" "awnb"
## [9] "awtan" "bag" "bagEarth" "bagEarthGCV"
## [13] "bagFDA" "bagFDAGCV" "bam" "bartMachine"
## [17] "bayesglm" "binda" "blackboost" "blasso"
## [21] "blassoAveraged" "bridge" "brnn" "BstLm"
## [25] "bstSm" "bstTree" "C5.0" "C5.0Cost"
## [29] "C5.0Rules" "C5.0Tree" "cforest" "chaid"
## [33] "CSimca" "ctree" "ctree2" "cubist"
## [37] "dda" "deepboost" "DENFIS" "dnn"
Методы, у которых есть встроенная оценка важности признаков (тег Implicit Feature Selection):
fs_models <- names(Filter(function(m) "Implicit Feature Selection" %in% m$tags,
getModelInfo()))
length(fs_models)
## [1] 83
fs_models
## [1] "ada" "AdaBag" "AdaBoost.M1" "adaboost"
## [5] "bagEarth" "bagEarthGCV" "bagFDA" "bagFDAGCV"
## [9] "bartMachine" "blasso" "BstLm" "bstSm"
## [13] "C5.0" "C5.0Cost" "C5.0Rules" "C5.0Tree"
## [17] "cforest" "chaid" "ctree" "ctree2"
## [21] "cubist" "deepboost" "earth" "enet"
## [25] "evtree" "extraTrees" "fda" "gamboost"
## [29] "gbm_h2o" "gbm" "gcvEarth" "glmnet_h2o"
## [33] "glmnet" "glmStepAIC" "J48" "JRip"
## [37] "lars" "lars2" "lasso" "LMT"
## [41] "LogitBoost" "M5" "M5Rules" "msaenet"
## [45] "nodeHarvest" "OneR" "ordinalNet" "ordinalRF"
## [49] "ORFlog" "ORFpls" "ORFridge" "ORFsvm"
## [53] "pam" "parRF" "PART" "penalized"
## [57] "PenalizedLDA" "qrf" "ranger" "Rborist"
## [61] "relaxo" "rf" "rFerns" "rfRules"
## [65] "rotationForest" "rotationForestCp" "rpart" "rpart1SE"
## [69] "rpart2" "rpartCost" "rpartScore" "rqlasso"
## [73] "rqnc" "RRF" "RRFglobal" "sdwd"
## [77] "smda" "sparseLDA" "spikeslab" "wsrf"
## [81] "xgbDART" "xgbLinear" "xgbTree"
Кроме того, в caret есть отдельные обёртки для отбора
признаков: rfe() (рекурсивное исключение),
sbf() (отбор фильтрами), gafs() (генетический
алгоритм) и safs() (имитация отжига).
Набор данных взят из справки к featurePlot(): матрица 50
× 5 из нормально распределённых случайных чисел и бинарный класс
A/B.
set.seed(2026)
x <- matrix(rnorm(50 * 5), ncol = 5)
y <- factor(rep(c("A", "B"), 25))
colnames(x) <- paste0("X", 1:5)
summary(x)
## X1 X2 X3 X4
## Min. :-2.54688 Min. :-1.9585 Min. :-1.7388 Min. :-1.8588
## 1st Qu.:-0.71448 1st Qu.:-0.9731 1st Qu.:-0.6693 1st Qu.:-0.5812
## Median : 0.05318 Median :-0.2460 Median : 0.2343 Median : 0.1027
## Mean :-0.01627 Mean :-0.1798 Mean : 0.1264 Mean : 0.1166
## 3rd Qu.: 0.61068 3rd Qu.: 0.4479 3rd Qu.: 0.8299 3rd Qu.: 0.6114
## Max. : 1.90808 Max. : 2.4144 Max. : 2.6387 Max. : 2.1805
## X5
## Min. :-2.53523
## 1st Qu.:-0.51553
## Median : 0.09044
## Mean : 0.10325
## 3rd Qu.: 0.72517
## Max. : 2.16165
table(y)
## y
## A B
## 25 25
Строим четыре типа графиков и сохраняем каждый в
.jpg.
dir.create("plots", showWarnings = FALSE)
fp_types <- c("pairs", "ellipse", "density", "box")
fp_args <- list(
pairs = list(),
ellipse = list(),
density = list(scales = list(x = list(relation = "free"), y = list(relation = "free")),
adjust = 1.5, pch = "|", layout = c(5, 1)),
box = list(scales = list(y = list(relation = "free"), x = list(rot = 90)),
layout = c(5, 1))
)
for (p in fp_types) {
big <- p %in% c("pairs", "ellipse")
jpeg(file.path("plots", paste0("featurePlot_", p, ".jpg")),
width = if (big) 1000 else 1400, height = if (big) 900 else 500, quality = 95)
print(do.call(featurePlot, c(list(x = x, y = y, plot = p,
auto.key = list(columns = 2)), fp_args[[p]])))
dev.off()
}
list.files("plots", pattern = "featurePlot")
featurePlot(x, y, plot = "pairs", auto.key = list(columns = 2))
featurePlot(x, y, plot = "ellipse", auto.key = list(columns = 2))
featurePlot(x, y, plot = "density",
scales = list(x = list(relation = "free"), y = list(relation = "free")),
adjust = 1.5, pch = "|", layout = c(5, 1),
auto.key = list(columns = 2))
featurePlot(x, y, plot = "box",
scales = list(y = list(relation = "free"), x = list(rot = 90)),
layout = c(5, 1), auto.key = list(columns = 2))
Для сравнения проверим, есть ли разница средних между классами (t-тест по каждому признаку):
sapply(as.data.frame(x), function(col) round(t.test(col ~ y)$p.value, 3))
## X1 X2 X3 X4 X5
## 0.251 0.035 0.951 0.962 0.201
Выводы по пункту 1.
caret доступно 239 методов обучения. У 83 из них есть
встроенная оценка важности признаков (деревья, случайные леса, бустинг,
регуляризованные регрессии и др.).featurePlot() быстро строит разведочные графики
«признаки против класса» сразу для всех переменных. pairs и
ellipse показывают попарные связи признаков,
density и box показывают распределение каждого
признака в разрезе классов.A и B полностью перемешаны, эллипсы
почти совпадают, кривые плотности и «ящики» перекрываются. Признаки
между собой не коррелируют.data(iris)
str(iris)
## 'data.frame': 150 obs. of 5 variables:
## $ Sepal.Length: num 5.1 4.9 4.7 4.6 5 5.4 4.6 5 4.4 4.9 ...
## $ Sepal.Width : num 3.5 3 3.2 3.1 3.6 3.9 3.4 3.4 2.9 3.1 ...
## $ Petal.Length: num 1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 1.4 1.5 ...
## $ Petal.Width : num 0.2 0.2 0.2 0.2 0.2 0.4 0.3 0.2 0.2 0.1 ...
## $ Species : Factor w/ 3 levels "setosa","versicolor",..: 1 1 1 1 1 1 1 1 1 1 ...
ig <- information.gain(Species ~ ., iris)
gr <- gain.ratio(Species ~ ., iris)
su <- symmetrical.uncertainty(Species ~ ., iris)
chi <- chi.squared(Species ~ ., iris)
set.seed(1)
rf <- random.forest.importance(Species ~ ., iris, importance.type = 1)
imp <- data.frame(
information.gain = ig$attr_importance,
gain.ratio = gr$attr_importance,
symmetrical.uncertainty = su$attr_importance,
chi.squared = chi$attr_importance,
random.forest = rf$attr_importance,
row.names = rownames(ig)
)
round(imp, 3)
| information.gain | gain.ratio | symmetrical.uncertainty | chi.squared | random.forest | |
|---|---|---|---|---|---|
| Sepal.Length | 0.452 | 0.420 | 0.416 | 0.629 | 15.593 |
| Sepal.Width | 0.267 | 0.247 | 0.245 | 0.492 | 7.283 |
| Petal.Length | 0.940 | 0.858 | 0.857 | 0.935 | 48.076 |
| Petal.Width | 0.955 | 0.871 | 0.871 | 0.943 | 45.889 |
imp_norm <- sweep(imp, 2, apply(imp, 2, max), "/")
barplot(t(as.matrix(imp_norm)), beside = TRUE,
col = c("#4E79A7", "#F28E2B", "#59A14F", "#E15759", "#B07AA1"),
legend.text = colnames(imp_norm),
args.legend = list(x = "topleft", bty = "n", cex = 0.8),
ylab = "Важность (нормировано к максимуму)",
main = "Важность признаков iris по разным критериям FSelector")
cutoff.k(ig, 2) # два лучших признака по информационному выигрышу
## [1] "Petal.Width" "Petal.Length"
cutoff.k.percent(ig, 0.5) # лучшие 50 % признаков
## [1] "Petal.Width" "Petal.Length"
set.seed(1)
cfs(Species ~ ., iris) # корреляционный отбор подмножества (CFS)
## [1] "Petal.Length" "Petal.Width"
consistency(Species ~ ., iris) # отбор по согласованности
## [1] "Petal.Width"
f <- as.simple.formula(cutoff.k(ig, 2), "Species")
f
## Species ~ Petal.Width + Petal.Length
## <environment: 0x0000021b8e7809d8>
Выводы по пункту 2.
cutoff.k, cfs)
выбирают одну и ту же пару признаков: Petal.Length и
Petal.Width. Для классификации iris достаточно этих двух
признаков, а Sepal.Width практически ничего не
добавляет.Переменная для дискретизации: Petal.Length (длина
лепестка), три категории.
x_pl <- iris$Petal.Length
d_interval <- discretize(x_pl, method = "interval", breaks = 3)
d_frequency <- discretize(x_pl, method = "frequency", breaks = 3)
set.seed(1)
d_cluster <- discretize(x_pl, method = "cluster", breaks = 3)
d_fixed <- discretize(x_pl, method = "fixed",
breaks = c(-Inf, 2.5, 4.9, Inf),
labels = c("short", "medium", "long"))
table(d_interval)
## d_interval
## [1,2.97) [2.97,4.93) [4.93,6.9]
## 50 54 46
table(d_frequency)
## d_frequency
## [1,2.63) [2.63,4.9) [4.9,6.9]
## 50 49 51
table(d_cluster)
## d_cluster
## [1,2.85) [2.85,4.89) [4.89,6.9]
## 50 49 51
table(d_fixed)
## d_fixed
## short medium long
## 50 49 51
table(Интервал = d_interval, Вид = iris$Species)
## Вид
## Интервал setosa versicolor virginica
## [1,2.97) 50 0 0
## [2.97,4.93) 0 48 6
## [4.93,6.9] 0 2 44
table(Частота = d_frequency, Вид = iris$Species)
## Вид
## Частота setosa versicolor virginica
## [1,2.63) 50 0 0
## [2.63,4.9) 0 46 3
## [4.9,6.9] 0 4 47
table(Кластер = d_cluster, Вид = iris$Species)
## Вид
## Кластер setosa versicolor virginica
## [1,2.85) 50 0 0
## [2.85,4.89) 0 46 3
## [4.89,6.9] 0 4 47
table(Фикс = d_fixed, Вид = iris$Species)
## Вид
## Фикс setosa versicolor virginica
## short 50 0 0
## medium 0 46 3
## long 0 4 47
show_cuts <- function(d, title) {
hist(x_pl, breaks = 30, col = "grey85", border = "white",
main = title, xlab = "Petal.Length")
abline(v = attr(d, "discretized:breaks"), col = "red", lwd = 2, lty = 2)
}
par(mfrow = c(2, 2))
show_cuts(d_interval, "interval: равная ширина")
show_cuts(d_frequency, "frequency: равная частота")
show_cuts(d_cluster, "cluster: k-средних")
show_cuts(d_fixed, "fixed: заданные границы")
par(mfrow = c(1, 1))
Доля «правильно» попавших объектов, если сопоставить каждой категории вид, который в ней преобладает:
purity <- function(d) {
t <- table(d, iris$Species)
round(sum(apply(t, 1, max)) / sum(t), 3)
}
sapply(list(interval = d_interval, frequency = d_frequency,
cluster = d_cluster, fixed = d_fixed), purity)
## interval frequency cluster fixed
## 0.947 0.953 0.953 0.953
Тот же подход ко всему набору данных: все числовые столбцы iris сразу.
iris_disc <- discretizeDF(iris, default = list(method = "frequency", breaks = 3))
head(iris_disc)
| Sepal.Length | Sepal.Width | Petal.Length | Petal.Width | Species |
|---|---|---|---|---|
| [4.3,5.4) | [3.2,4.4] | [1,2.63) | [0.1,0.867) | setosa |
| [4.3,5.4) | [2.9,3.2) | [1,2.63) | [0.1,0.867) | setosa |
| [4.3,5.4) | [3.2,4.4] | [1,2.63) | [0.1,0.867) | setosa |
| [4.3,5.4) | [2.9,3.2) | [1,2.63) | [0.1,0.867) | setosa |
| [4.3,5.4) | [3.2,4.4] | [1,2.63) | [0.1,0.867) | setosa |
| [5.4,6.3) | [3.2,4.4] | [1,2.63) | [0.1,0.867) | setosa |
Выводы по пункту 3.
Набор Ozone из пакета mlbench содержит
метеорологические данные Лос-Анджелеса за 1976 год. Целевая переменная
V4 — максимальная за день среднечасовая концентрация
озона.
| Переменная | Описание |
|---|---|
| V1 | месяц |
| V2 | день месяца |
| V3 | день недели |
| V4 | максимальная концентрация озона (цель) |
| V5 | высота изобарической поверхности 500 мбар |
| V6 | скорость ветра |
| V7 | влажность |
| V8 | температура (Sandburg) |
| V9 | температура (El Monte) |
| V10 | высота основания инверсии |
| V11 | градиент давления |
| V12 | температура основания инверсии |
| V13 | видимость |
data("Ozone")
dim(Ozone)
## [1] 366 13
colSums(is.na(Ozone))
## V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13
## 0 0 0 5 12 0 15 2 139 15 1 14 0
oz <- na.omit(Ozone)
dim(oz)
## [1] 203 13
set.seed(123)
boruta_oz <- Boruta(V4 ~ ., data = oz, doTrace = 0)
print(boruta_oz)
## Boruta performed 18 iterations in 0.234077 secs.
## 9 attributes confirmed important: V1, V10, V11, V12, V13 and 4 more;
## 3 attributes confirmed unimportant: V2, V3, V6;
stats <- attStats(boruta_oz)
stats[order(-stats$meanImp), ]
| meanImp | medianImp | minImp | maxImp | normHits | decision | |
|---|---|---|---|---|---|---|
| V9 | 0.8691527 | 0.8643265 | 0.8427372 | 0.9241308 | 1.0000000 | Confirmed |
| V8 | 0.7708686 | 0.7688999 | 0.7325169 | 0.8132705 | 1.0000000 | Confirmed |
| V12 | 0.6605128 | 0.6583222 | 0.6023462 | 0.7395861 | 1.0000000 | Confirmed |
| V11 | 0.5514369 | 0.5472191 | 0.4948277 | 0.6116130 | 1.0000000 | Confirmed |
| V7 | 0.5188321 | 0.5058817 | 0.4672574 | 0.5911403 | 1.0000000 | Confirmed |
| V13 | 0.4276145 | 0.4205786 | 0.3861841 | 0.4692028 | 1.0000000 | Confirmed |
| V10 | 0.4238906 | 0.4173946 | 0.3816490 | 0.4924597 | 1.0000000 | Confirmed |
| V1 | 0.4066023 | 0.3987471 | 0.3720819 | 0.5036158 | 1.0000000 | Confirmed |
| V5 | 0.3796139 | 0.3766333 | 0.3380987 | 0.4214736 | 1.0000000 | Confirmed |
| V6 | 0.0474049 | 0.0597975 | -0.0157244 | 0.1271816 | 0.0555556 | Rejected |
| V2 | 0.0439189 | 0.0316982 | -0.0260447 | 0.1313782 | 0.1111111 | Rejected |
| V3 | -0.0323591 | -0.0452859 | -0.0803994 | 0.0317817 | 0.0000000 | Rejected |
Зелёным отмечены подтверждённые признаки, жёлтым спорные, красным отвергнутые. Синим показаны «теневые» признаки: перемешанные копии исходных, которые служат порогом случайной важности.
par(mar = c(6, 4, 3, 1))
plot(boruta_oz, las = 2, cex.axis = 0.8, xlab = "",
main = "Boruta: важность признаков для V4 (озон)")
jpeg("plots/boruta_boxplot.jpg", width = 1000, height = 700, quality = 95)
par(mar = c(6, 4, 3, 1))
plot(boruta_oz, las = 2, cex.axis = 0.8, xlab = "",
main = "Boruta: важность признаков для V4 (озон)")
dev.off()
plotImpHistory(boruta_oz, main = "История важности по итерациям")
Итоговый набор признаков и формула для модели:
getSelectedAttributes(boruta_oz, withTentative = FALSE)
## [1] "V1" "V5" "V7" "V8" "V9" "V10" "V11" "V12" "V13"
getNonRejectedFormula(boruta_oz)
## V4 ~ V1 + V5 + V7 + V8 + V9 + V10 + V11 + V12 + V13
## <environment: 0x0000021b97aad7a8>
Выводы по пункту 4.
В работе использованы четыре инструмента подготовки признаков в R:
featurePlot() для быстрого графического анализа связи
признаков с классом. На случайных данных графики правильно показали
отсутствие разделимости.Все графики featurePlot() и boxplot Boruta сохранены в
папку plots/ в формате .jpg.
sessionInfo()
## R version 4.6.1 (2026-06-24 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
##
## Matrix products: default
## LAPACK version 3.12.1
##
## locale:
## [1] LC_COLLATE=Russian_Russia.utf8 LC_CTYPE=Russian_Russia.utf8
## [3] LC_MONETARY=Russian_Russia.utf8 LC_NUMERIC=C
## [5] LC_TIME=Russian_Russia.utf8
##
## time zone: Europe/Moscow
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] mlbench_2.1-11 Boruta_10.0.0 arules_1.7.15 Matrix_1.7-5 FSelector_0.34
## [6] caret_7.0-1 lattice_0.22-9 ggplot2_4.0.3
##
## loaded via a namespace (and not attached):
## [1] tidyselect_1.2.1 timeDate_4052.112 dplyr_1.2.1
## [4] farver_2.1.2 S7_0.2.2 fastmap_1.2.0
## [7] pROC_1.19.1 digest_0.6.39 rpart_4.1.27
## [10] timechange_0.4.0 lifecycle_1.0.5 survival_3.8-6
## [13] magrittr_2.0.5 compiler_4.6.1 rlang_1.3.0
## [16] sass_0.4.10 tools_4.6.1 yaml_2.3.12
## [19] data.table_1.18.6.1 knitr_1.52 plyr_1.8.9
## [22] RColorBrewer_1.1-3 withr_3.0.3 purrr_1.2.2
## [25] nnet_7.3-20 grid_4.6.1 stats4_4.6.1
## [28] future_1.76.0 globals_0.20.0 scales_1.4.0
## [31] iterators_1.0.14 MASS_7.3-65 cli_3.6.6
## [34] ellipse_0.5.0 rmarkdown_2.32 generics_0.1.4
## [37] otel_0.2.0 rstudioapi_0.19.0 future.apply_1.20.2
## [40] reshape2_1.4.5 cachem_1.1.0 stringr_1.6.0
## [43] splines_4.6.1 parallel_4.6.1 vctrs_0.7.3
## [46] hardhat_1.4.3 jsonlite_2.0.0 RWeka_0.4-50
## [49] listenv_1.1.0 foreach_1.5.2 gower_1.0.2
## [52] jquerylib_0.1.4 recipes_1.4.0 glue_1.8.1
## [55] parallelly_1.48.0 codetools_0.2-20 lubridate_1.9.5
## [58] stringi_1.8.9 rJava_1.0-18 gtable_0.3.6
## [61] tibble_3.3.1 pillar_1.11.1 htmltools_0.5.9
## [64] ipred_0.9-16 randomForest_4.7-1.2 lava_1.9.3
## [67] entropy_1.3.2 R6_2.6.1 evaluate_1.0.5
## [70] fru_1.0.0 bslib_0.12.0 RWekajars_3.9.3-2
## [73] class_7.3-23 Rcpp_1.1.2 nlme_3.1-169
## [76] prodlim_2026.03.11 xfun_0.61 ModelMetrics_1.2.2.2
## [79] pkgconfig_2.0.3