Дисциплина: Анализ данных

Цель работы: освоить инструменты отбора и оценки признаков в R: пакеты caret, FSelector, arules и Boruta.

library(caret)
library(FSelector)
library(arules)
library(Boruta)
library(mlbench)

1 Пакет CARET: модели и разведочный анализ

1.1 Список доступных методов

Функция 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() (имитация отжига).

1.2 Графический разведочный анализ: featurePlot()

Набор данных взят из справки к 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")

1.2.1 Матрица диаграмм рассеяния (pairs)

featurePlot(x, y, plot = "pairs", auto.key = list(columns = 2))

1.2.2 Диаграммы рассеяния с эллипсами (ellipse)

featurePlot(x, y, plot = "ellipse", auto.key = list(columns = 2))

1.2.3 Плотности распределения по классам (density)

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))

1.2.4 Диаграммы размаха по классам (box)

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 полностью перемешаны, эллипсы почти совпадают, кривые плотности и «ящики» перекрываются. Признаки между собой не коррелируют.
  • t-тест подтверждает картину: у четырёх признаков из пяти p-value заметно выше 0,05. У признака X2 p ≈ 0,035, и на графике density его кривые немного сдвинуты. Но это случайность: при пяти проверках на уровне 0,05 вероятность хотя бы одного ложного «открытия» около 23 %, а с поправкой Бонферрони (порог 0,05 / 5 = 0,01) различие уже не значимо.
  • Ни один признак здесь не годится для классификации, это и должен показывать разведочный анализ на чистом шуме. Заодно пример показывает, почему кажущиеся различия на графиках нужно проверять статистически с учётом множественных сравнений.

2 Пакет FSelector: важность признаков (iris)

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 ...

2.1 Фильтры на основе энтропии

ig  <- information.gain(Species ~ ., iris)
gr  <- gain.ratio(Species ~ ., iris)
su  <- symmetrical.uncertainty(Species ~ ., iris)

2.2 Статистический фильтр χ² и случайный лес

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")

2.3 Отбор подмножества признаков

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.

  • Все пять критериев дают одинаковый порядок важности: Petal.Width ≈ Petal.Length ≫ Sepal.Length > Sepal.Width.
  • По информационному выигрышу признаки лепестка дают около 0,94–0,96, а признаки чашелистика только 0,45 и 0,27. Это значит, что размеры лепестка почти полностью снимают неопределённость вида ириса.
  • Методы отбора подмножеств (cutoff.k, cfs) выбирают одну и ту же пару признаков: Petal.Length и Petal.Width. Для классификации iris достаточно этих двух признаков, а Sepal.Width практически ничего не добавляет.
  • Фильтровые методы (энтропийные, χ²) быстрые и не зависят от модели. Важность по случайному лесу учитывает взаимодействия признаков, но здесь приводит к тому же выводу.

3 Пакет arules: дискретизация непрерывной переменной

Переменная для дискретизации: 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"))

3.1 Границы и размеры интервалов

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

3.2 Соответствие категорий видам ирисов

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

3.3 Визуальное сравнение

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.

  • interval делит диапазон [1; 6,9] на три равных по ширине отрезка. Размеры групп получаются неравными (50 / 54 / 46), потому что распределение длины лепестка двухвершинное.
  • frequency подбирает границы так, чтобы в каждой группе было примерно поровну наблюдений (50 / 49 / 51). Этот способ удобен, когда важен баланс категорий.
  • cluster (k-средних) ставит границы в «провалах» распределения и даёт почти те же интервалы, что и frequency, с границами около 2,85 и 4,89.
  • fixed позволяет задать осмысленные границы вручную. Значения 2,5 и 4,9 выбраны по гистограмме, а категории получили понятные названия short / medium / long.
  • Все методы полностью отделяют setosa (короткие лепестки). Методы frequency, cluster и fixed разделяют versicolor и virginica лучше (чистота около 0,953), чем равная ширина (0,947). Дискретизация по структуре данных сохраняет больше информации о классе, чем механическое деление на равные отрезки.

4 Пакет Boruta: отбор признаков (Ozone)

Набор 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

4.1 График boxplot важности признаков

Зелёным отмечены подтверждённые признаки, жёлтым спорные, красным отвергнутые. Синим показаны «теневые» признаки: перемешанные копии исходных, которые служат порогом случайной важности.

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.

  • После удаления пропусков осталось 203 наблюдений из 366. Boruta завершил работу за 0.2 с. и не оставил ни одного спорного (Tentative) признака.
  • Подтверждены (9): V9, V8, V12, V11, V7, V13, V10, V1, V5. Самые важные — температура воздуха в двух точках (V9, V8) и температура основания инверсии (V12). Это физически логично: озон образуется в фотохимических реакциях, которые идут быстрее в жару, а температурная инверсия удерживает загрязнения у земли. Влажность (V7), градиент давления (V11), видимость (V13), высота инверсии (V10), высота поверхности 500 мбар (V5) и месяц (V1, то есть сезонность) тоже заметно влияют.
  • Отвергнуты (3): V2 (день месяца), V3 (день недели) и V6 (скорость ветра). На boxplot их «ящики» лежат на уровне теневых признаков или ниже, то есть их важность неотличима от случайной.
  • Boxplot наглядно показывает идею метода: признак признаётся важным, только если его важность стабильно выше максимальной важности теневых признаков на протяжении всех итераций.

5 Общий вывод

В работе использованы четыре инструмента подготовки признаков в R:

  1. caret: единый интерфейс к 239 моделям и функция featurePlot() для быстрого графического анализа связи признаков с классом. На случайных данных графики правильно показали отсутствие разделимости.
  2. FSelector: фильтровые методы (information gain, gain ratio, symmetrical uncertainty, χ², важность по случайному лесу) и методы отбора подмножеств. Для iris все они указали на признаки лепестка как основные.
  3. arules::discretize(): четыре способа перевода непрерывной переменной в категориальную. Методы, учитывающие структуру данных (frequency, cluster, fixed), сохраняют больше информации о классе, чем равная ширина интервалов.
  4. Boruta: обёрточный метод на основе случайного леса с теневыми признаками. Для данных Ozone он отобрал 9 из 12 признаков и отбросил календарные V2, V3 и скорость ветра V6.

Все графики 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