Цель работы — освоить инструменты выбора признаков и предварительного
анализа данных в языке R с использованием пакетов
caret, FSelector,
arules и Boruta.
В отчёте приведены: код, полученные результаты, графики (сохранённые в
формате JPG) и выводы.
names(getModelInfo()) и ознакомиться
со списком доступных методов/моделей.x и метки классов
y (как в задании).featurePlot() и сохранить графики в
JPG.getModelInfo() содержит информацию о моделях, доступных
в caret.
Команда names(getModelInfo()) выводит названия всех
моделей.
library(caret)
m <- names(getModelInfo())
cat("Всего моделей в caret:", length(m), "\n")
## Всего моделей в caret: 239
head(m, 15)
## [1] "ada" "AdaBag" "AdaBoost.M1" "adaboost" "amdai"
## [6] "ANFIS" "avNNet" "awnb" "awtan" "bag"
## [11] "bagEarth" "bagEarthGCV" "bagFDA" "bagFDAGCV" "bam"
Пояснение: вывод (список из десятков/сотен методов) подтверждает, что caret поддерживает множество алгоритмов машинного обучения и методов построения моделей.
Согласно заданию создаём матрицу признаков x размером
50×5 и вектор классов y из двух классов A и B.
Дополнительно выполним центрирование и
масштабирование признаков (scale()), чтобы
сравнение распределений по графикам было удобнее (масштабирование не
меняет сути данных, но упрощает визуальное сравнение признаков между
собой).
set.seed(42)
x <- matrix(rnorm(50*5), ncol=5)
y <- factor(rep(c("A","B"), 25))
colnames(x) <- c("F1","F2","F3","F4","F5")
x_scaled <- scale(x)
Что показывает:
pairs-график строит диаграммы рассеяния для всех пар признаков. Это
помогает увидеть: - есть ли группы/кластеры по классам A и B; - есть ли
линейные зависимости между признаками; - есть ли визуально разделимые
области.
jpeg("plots/eda_pairs_v3.jpg", width=1800, height=1200, res=200)
featurePlot(x = x_scaled, y = y, plot = "pairs")
dev.off()
## quartz_off_screen
## 2
Что показывает:
Boxplot по классам показывает: - медиану значений признака в каждом
классе; - разброс (межквартильный размах); - возможные выбросы.
jpeg("plots/eda_box_v3.jpg", width=1800, height=1200, res=200)
featurePlot(x = x_scaled, y = y, plot = "box")
dev.off()
## quartz_off_screen
## 2
Что показывает:
Density-график сравнивает распределения признаков в классах A и B: -
если плотности сильно расходятся → признак может хорошо разделять
классы; - если графики почти совпадают → разделяющей способности
мало.
jpeg("plots/eda_density_v3.jpg", width=1800, height=1200, res=200)
featurePlot(x = x_scaled, y = y, plot = "density")
dev.off()
## quartz_off_screen
## 2
Чтобы подкрепить визуальные наблюдения, вычислим средние значения признаков по классам.
x_df <- as.data.frame(x_scaled)
x_df$Class <- y
aggregate(. ~ Class, data = x_df, FUN = mean)
## Class F1 F2 F3 F4 F5
## 1 A 0.08293395 -0.09414798 -0.150313 0.005747935 -0.1552154
## 2 B -0.08293395 0.09414798 0.150313 -0.005747935 0.1552154
Поскольку матрица x создана случайно
(rnorm()), классы A и B не имеют реальной
зависимости от признаков, и это видно по графикам:
Следовательно, в данном примере признаки слабо информативны для классификации.
iris.Species).Используем две оценки: - random.forest.importance() —
важность признаков по случайному лесу;
- information.gain() — информационная выгода.
library(FSelector)
data(iris)
rf_imp <- random.forest.importance(Species ~ ., iris)
rf_imp[order(-rf_imp$attr_importance), , drop=FALSE]
## attr_importance
## Petal.Length 48.621282
## Petal.Width 46.945379
## Sepal.Length 16.330769
## Sepal.Width 7.677002
Для сравнения:
ig <- information.gain(Species ~ ., iris)
ig[order(-ig$attr_importance), , drop=FALSE]
## attr_importance
## Petal.Width 0.9554360
## Petal.Length 0.9402853
## Sepal.Length 0.4521286
## Sepal.Width 0.2672750
Обе оценки показывают, что наиболее значимыми признаками являются: - Petal.Length - Petal.Width
Это означает, что параметры лепестков лучше всего разделяют виды
ирисов (классы Species). Признаки чашелистиков
(Sepal.*) обычно оказываются менее информативными.
Для iris выполнить преобразование непрерывной переменной
в категориальную с помощью discretize() четырьмя методами:
- interval (равная ширина интервала), -
frequency (равная частота), - cluster
(кластеризация), - fixed (границы задаются вручную).
Дискретизируем Sepal.Width (ширина чашелистика).
library(arules)
x_cont <- iris$Sepal.Width
summary(x_cont)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 2.000 2.800 3.000 3.057 3.300 4.400
d_interval <- discretize(x_cont, method = "interval", categories = 3)
table(d_interval)
## d_interval
## [2,2.8) [2.8,3.6) [3.6,4.4]
## 47 88 15
Смысл: диапазон значений делится на 3 одинаковых по ширине интервала.
d_frequency <- discretize(x_cont, method = "frequency", categories = 3)
table(d_frequency)
## d_frequency
## [2,2.9) [2.9,3.2) [3.2,4.4]
## 47 47 56
Смысл: интервалы подбираются так, чтобы в каждом было примерно одинаковое число наблюдений.
d_cluster <- discretize(x_cont, method = "cluster", categories = 3)
table(d_cluster)
## d_cluster
## [2,2.85) [2.85,3.44) [3.44,4.4]
## 47 78 25
Смысл: границы выбираются по группам значений (кластеризация), чтобы учесть структуру распределения.
d_fixed <- discretize(
x_cont,
method = "fixed",
breaks = c(-Inf, 2.8, 3.4, Inf),
labels = c("narrow", "normal", "wide")
)
table(d_fixed)
## d_fixed
## narrow normal wide
## 33 80 37
Смысл: границы задаются вручную — удобно, если есть экспертные пороги.
Разные методы дают разное распределение по категориям: -
interval — равная ширина, частоты могут быть сильно
разными; - frequency — частоты примерно равны; -
cluster — учитывает “естественные” группы; -
fixed — зависит от заданных порогов и удобен для
интерпретации.
data("Ozone").В Ozone есть пропуски, поэтому удалим строки с NA (иначе
Boruta может работать некорректно).
library(mlbench)
library(Boruta)
data("Ozone")
oz <- na.omit(Ozone)
# целевая переменная (последний столбец)
target <- names(oz)[ncol(oz)]
form <- as.formula(paste(target, "~ ."))
target
## [1] "V13"
set.seed(42)
bor <- Boruta(form, data = oz, maxRuns = 80, doTrace = 0)
bor
## Boruta performed 30 iterations in 2.524732 secs.
## 9 attributes confirmed important: V1, V10, V11, V12, V4 and 4 more;
## 3 attributes confirmed unimportant: V2, V3, V6;
stats <- attStats(bor)
stats
## meanImp medianImp minImp maxImp normHits decision
## V1 4.96247429 4.97885767 2.2631618 6.827074 0.93333333 Confirmed
## V2 0.02440981 -0.02535998 -1.4046393 1.640035 0.00000000 Rejected
## V3 -0.21441706 -0.46782979 -1.6209755 1.050368 0.00000000 Rejected
## V4 12.87769681 12.86010088 11.6884754 14.335631 1.00000000 Confirmed
## V5 4.20993059 4.14222674 2.9120333 5.418729 0.80000000 Confirmed
## V6 1.85585034 1.71640544 0.5144455 3.340914 0.03333333 Rejected
## V7 14.90759871 14.94806376 12.4303221 17.106776 1.00000000 Confirmed
## V8 4.78508987 4.89070775 1.9851514 6.709040 0.83333333 Confirmed
## V9 9.31790018 9.36054997 7.8708506 10.761351 0.96666667 Confirmed
## V10 6.68504080 6.84815171 5.1578172 8.431285 0.96666667 Confirmed
## V11 4.73479101 4.84182192 2.1729111 6.490110 0.83333333 Confirmed
## V12 10.34224059 10.29480786 8.9026290 11.587005 0.96666667 Confirmed
Пояснение:
В таблице видно, какие признаки получили статус: - Confirmed (важные), -
Rejected (неважные), - Tentative (неопределённые).
jpeg("plots/boruta_boxplot_v3.jpg", width=2000, height=1200, res=200)
plot(bor, las = 2, cex.axis = 0.85)
dev.off()
## quartz_off_screen
## 2
Что показывает график:
Boxplot отражает распределение важности признаков (по итерациям
случайного леса) и “теневых” признаков (shadow).
Если важность признака стабильно выше, чем у shadow — признак
подтверждается как важный.
getSelectedAttributes(bor, withTentative = TRUE)
## [1] "V1" "V4" "V5" "V7" "V8" "V9" "V10" "V11" "V12"
bor_final <- TentativeRoughFix(bor)
getSelectedAttributes(bor_final, withTentative = FALSE)
## [1] "V1" "V4" "V5" "V7" "V8" "V9" "V10" "V11" "V12"
Boruta отбирает признаки по принципу сравнения с “теневыми”
признаками. Это помогает исключить случайно значимые признаки и оставить
те, что действительно влияют на целевую переменную.
Boxplot позволяет визуально увидеть, какие признаки превосходят shadow и
являются наиболее информативными.