Цель: выполнить классификацию методом kNN
(class::knn()) на наборе данных iris,
нормализовать признаки, разделить выборку на обучающую и тестовую,
оценить модель с помощью CrossTable() (пакет
gmodels), построить матрицу ошибок и диагональную оценку
качества прогноза.
library(class) # knn(), knn.cv()
library(gmodels) # CrossTable()
library(caret) # createDataPartition(), confusionMatrix()
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 ...
summary(iris)
## Sepal.Length Sepal.Width Petal.Length Petal.Width
## Min. :4.300 Min. :2.000 Min. :1.000 Min. :0.100
## 1st Qu.:5.100 1st Qu.:2.800 1st Qu.:1.600 1st Qu.:0.300
## Median :5.800 Median :3.000 Median :4.350 Median :1.300
## Mean :5.843 Mean :3.057 Mean :3.758 Mean :1.199
## 3rd Qu.:6.400 3rd Qu.:3.300 3rd Qu.:5.100 3rd Qu.:1.800
## Max. :7.900 Max. :4.400 Max. :6.900 Max. :2.500
## Species
## setosa :50
## versicolor:50
## virginica :50
##
##
##
table(iris$Species)
##
## setosa versicolor virginica
## 50 50 50
Набор iris содержит 150 цветков трёх видов (по 50
каждого) и 4 числовых признака. Признаки измерены в сантиметрах, но
имеют разный разброс (например, Petal.Length изменяется от
1 до 6.9, а Sepal.Width — от 2 до 4.4).
Метод kNN основан на расстояниях, поэтому признаки необходимо привести к одному масштабу. Чтобы информация о тестовых данных не «просачивалась» в модель, сначала выполняется разбиение, а параметры min-max нормализации (минимум и максимум каждого признака) вычисляются только по обучающей выборке и затем применяются к тестовой.
Разбиение стратифицированное (createDataPartition): доли
видов в обеих выборках одинаковы.
set.seed(123) # воспроизводимость
train_idx <- createDataPartition(iris$Species, p = 0.7, list = FALSE)
train_raw <- iris[train_idx, 1:4]
test_raw <- iris[-train_idx, 1:4]
train_labels <- iris$Species[train_idx]
test_labels <- iris$Species[-train_idx]
cat("Обучающая выборка:", nrow(train_raw), "наблюдений\n")
## Обучающая выборка: 105 наблюдений
cat("Тестовая выборка: ", nrow(test_raw), "наблюдений\n")
## Тестовая выборка: 45 наблюдений
table(Обучающая = train_labels)
## Обучающая
## setosa versicolor virginica
## 35 35 35
table(Тестовая = test_labels)
## Тестовая
## setosa versicolor virginica
## 15 15 15
\[x' = \frac{x - x_{\min}}{x_{\max} - x_{\min}}\]
mins <- sapply(train_raw, min)
maxs <- sapply(train_raw, max)
minmax <- function(df, lo, hi) {
as.data.frame(mapply(function(x, a, b) (x - a) / (b - a),
df, lo, hi, SIMPLIFY = FALSE))
}
train_data <- minmax(train_raw, mins, maxs)
test_data <- minmax(test_raw, mins, maxs)
# Проверка: обучающая выборка целиком в [0, 1]
rbind(min = sapply(train_data, min), max = sapply(train_data, max))
## Sepal.Length Sepal.Width Petal.Length Petal.Width
## min 0 0 0 0
## max 1 1 1 1
head(train_data)
Тестовые значения при такой схеме могут незначительно выходить за пределы \([0, 1]\), если в тесте встретились значения вне диапазона обучающей выборки, — это нормально.
Значение k выбирается по обучающей выборке с помощью
перекрёстной проверки «исключай по одному» (knn.cv), а не
по тестовой — иначе тестовая выборка перестаёт быть независимой.
k_values <- seq(1, 25, by = 2)
cv_acc <- sapply(k_values, function(k) {
mean(knn.cv(train = train_data, cl = train_labels, k = k) == train_labels)
})
k_table <- data.frame(k = k_values, cv_accuracy_pct = round(cv_acc * 100, 1))
k_table
# среди k с максимальной точностью берём наименьшее (более простая модель)
k_best <- k_values[which.max(cv_acc)]
cat("Выбрано k =", k_best, "\n")
## Выбрано k = 7
plot(k_values, cv_acc * 100, type = "b", pch = 19, col = "steelblue",
xlab = "k (число соседей)", ylab = "Точность на обучающей выборке, LOO (%)",
main = "Подбор k для kNN")
abline(v = k_best, col = "red", lty = 2)
legend("bottomright", legend = paste("k =", k_best),
col = "red", lty = 2, bty = "n")
Алгоритм knn() не строит модель в явном виде: для
каждого объекта тестовой выборки он находит k ближайших обучающих
объектов и назначает класс голосованием.
knn_pred <- knn(train = train_data,
test = test_data,
cl = train_labels,
k = k_best)
knn_pred
## [1] setosa setosa setosa setosa setosa setosa
## [7] setosa setosa setosa setosa setosa setosa
## [13] setosa setosa setosa versicolor versicolor versicolor
## [19] versicolor versicolor versicolor versicolor versicolor versicolor
## [25] versicolor versicolor versicolor versicolor versicolor versicolor
## [31] virginica virginica virginica virginica virginica virginica
## [37] versicolor virginica virginica virginica versicolor virginica
## [43] virginica virginica virginica
## Levels: setosa versicolor virginica
Строки — реальные классы, столбцы — предсказанные. Идеальный результат — все объекты на главной диагонали; всё, что вне диагонали, — ошибки.
CrossTable(x = test_labels, y = knn_pred,
prop.chisq = FALSE,
dnn = c("Реальный класс", "Предсказанный класс"))
##
##
## Cell Contents
## |-------------------------|
## | N |
## | N / Row Total |
## | N / Col Total |
## | N / Table Total |
## |-------------------------|
##
##
## Total Observations in Table: 45
##
##
## | Предсказанный класс
## Реальный класс | setosa | versicolor | virginica | Row Total |
## ---------------|------------|------------|------------|------------|
## setosa | 15 | 0 | 0 | 15 |
## | 1.000 | 0.000 | 0.000 | 0.333 |
## | 1.000 | 0.000 | 0.000 | |
## | 0.333 | 0.000 | 0.000 | |
## ---------------|------------|------------|------------|------------|
## versicolor | 0 | 15 | 0 | 15 |
## | 0.000 | 1.000 | 0.000 | 0.333 |
## | 0.000 | 0.882 | 0.000 | |
## | 0.000 | 0.333 | 0.000 | |
## ---------------|------------|------------|------------|------------|
## virginica | 0 | 2 | 13 | 15 |
## | 0.000 | 0.133 | 0.867 | 0.333 |
## | 0.000 | 0.118 | 1.000 | |
## | 0.000 | 0.044 | 0.289 | |
## ---------------|------------|------------|------------|------------|
## Column Total | 15 | 17 | 13 | 45 |
## | 0.333 | 0.378 | 0.289 | |
## ---------------|------------|------------|------------|------------|
##
##
cm_knn <- confusionMatrix(knn_pred, test_labels)
cm_knn
## Confusion Matrix and Statistics
##
## Reference
## Prediction setosa versicolor virginica
## setosa 15 0 0
## versicolor 0 15 2
## virginica 0 0 13
##
## Overall Statistics
##
## Accuracy : 0.9556
## 95% CI : (0.8485, 0.9946)
## No Information Rate : 0.3333
## P-Value [Acc > NIR] : < 2.2e-16
##
## Kappa : 0.9333
##
## Mcnemar's Test P-Value : NA
##
## Statistics by Class:
##
## Class: setosa Class: versicolor Class: virginica
## Sensitivity 1.0000 1.0000 0.8667
## Specificity 1.0000 0.9333 1.0000
## Pos Pred Value 1.0000 0.8824 1.0000
## Neg Pred Value 1.0000 1.0000 0.9375
## Prevalence 0.3333 0.3333 0.3333
## Detection Rate 0.3333 0.3333 0.2889
## Detection Prevalence 0.3333 0.3778 0.2889
## Balanced Accuracy 1.0000 0.9667 0.9333
Диагональная оценка качества прогноза: на главной диагонали матрицы ошибок стоят верно классифицированные объекты.
tab <- cm_knn$table # строки — предсказание, столбцы — реальный класс
diag_quality <- data.frame(
`Верно классифицировано` = diag(tab),
`Всего в классе` = colSums(tab),
`Полнота, % (recall)` = round(diag(tab) / colSums(tab) * 100, 1),
`Точность, % (precision)`= round(diag(tab) / rowSums(tab) * 100, 1),
check.names = FALSE
)
diag_quality
cat("\nОбщая точность (сумма диагонали / все объекты):",
sum(diag(tab)), "/", sum(tab), "=",
round(sum(diag(tab)) / sum(tab) * 100, 2), "%\n")
##
## Общая точность (сумма диагонали / все объекты): 43 / 45 = 95.56 %
Вывод. Модель kNN с k = 7 правильно классифицировала 43 из 45 тестовых объектов (точность 95.6 %, каппа Коэна 0.933). Вид setosa отделяется от остальных без ошибок; возможные ошибки сосредоточены между versicolor и virginica, чьи области в пространстве признаков частично перекрываются. Так как тестовая выборка небольшая (45 объектов), одна ошибка меняет точность примерно на 2.2 %, поэтому доверительный интервал оценки достаточно широк (84.9 – 99.5 %).
Цель: реализовать SVM через
e1071::svm(), построить линейный классификатор и подобрать
параметры с помощью перекрёстной проверки с делением выборки на 10
частей (cross = 10).
Идея метода: SVM строит разделяющую гиперплоскость с максимальным
зазором между классами; объекты, лежащие на границе зазора или
нарушающие его, — опорные векторы. Параметр
cost задаёт штраф за нарушения: малое значение — «мягкая»
граница с большим зазором, большое — «жёсткая», более подверженная
переобучению. Для трёх классов svm() использует схему
«каждый против каждого».
library(e1071)
set.seed(42)
idx_svm <- createDataPartition(iris$Species, p = 0.7, list = FALSE)
train_svm <- iris[idx_svm, ]
test_svm <- iris[-idx_svm, ]
cat("Обучающая:", nrow(train_svm), " Тестовая:", nrow(test_svm), "\n")
## Обучающая: 105 Тестовая: 45
При cross = 10 обучающая выборка делится на 10 равных
частей: модель обучается на 9 частях и проверяется на оставшейся;
процедура повторяется 10 раз. Признаки стандартизуются автоматически
(scale = TRUE).
set.seed(42)
svm_linear <- svm(Species ~ ., data = train_svm,
kernel = "linear", cost = 1, scale = TRUE,
cross = 10)
print(svm_linear)
##
## Call:
## svm(formula = Species ~ ., data = train_svm, kernel = "linear", cost = 1,
## cross = 10, scale = TRUE)
##
##
## Parameters:
## SVM-Type: C-classification
## SVM-Kernel: linear
## cost: 1
##
## Number of Support Vectors: 22
cat("\nЧисло опорных векторов по классам:\n")
##
## Число опорных векторов по классам:
print(setNames(svm_linear$nSV, levels(train_svm$Species)))
## setosa versicolor virginica
## 2 11 9
cat("\nТочность по каждому из 10 блоков, %:\n")
##
## Точность по каждому из 10 блоков, %:
print(round(svm_linear$accuracies, 2))
## [1] 100.00 100.00 100.00 100.00 100.00 81.82 100.00 100.00 90.00 90.91
cat("\nОбщая точность перекрёстной проверки (tot.acc):",
round(svm_linear$tot.acc, 2), "%\n")
##
## Общая точность перекрёстной проверки (tot.acc): 96.19 %
Параметр cost подбирается перебором по логарифмической
сетке с помощью tune(); для каждого значения качество
оценивается 10-кратной перекрёстной проверкой.
set.seed(42)
tune_lin <- tune(svm, Species ~ ., data = train_svm,
kernel = "linear", scale = TRUE,
ranges = list(cost = 10^seq(-3, 3)),
tunecontrol = tune.control(cross = 10))
# таблица ошибок по сетке
transform(tune_lin$performances,
error_pct = round(error * 100, 2),
dispersion_pct = round(dispersion * 100, 2))[, c("cost", "error_pct", "dispersion_pct")]
cat("\nЛучшее значение cost:", tune_lin$best.parameters$cost, "\n")
##
## Лучшее значение cost: 1
cat("Ошибка перекрёстной проверки лучшей модели:",
round(tune_lin$best.performance * 100, 2), "%\n")
## Ошибка перекрёстной проверки лучшей модели: 3.82 %
plot(tune_lin, main = "SVM (linear): ошибка перекрёстной проверки в зависимости от cost")
Если несколько значений cost дают одинаковую ошибку,
tune() возвращает первое из них (наименьшее).
best_lin <- tune_lin$best.model
svm_pred <- predict(best_lin, test_svm)
cm_svm <- confusionMatrix(svm_pred, test_svm$Species)
cm_svm$table
## Reference
## Prediction setosa versicolor virginica
## setosa 15 0 0
## versicolor 0 14 1
## virginica 0 1 14
tab_svm <- cm_svm$table
cat("\nДиагональ (верно классифицировано по классам):\n")
##
## Диагональ (верно классифицировано по классам):
print(diag(tab_svm))
## setosa versicolor virginica
## 15 14 14
cat("\nОбщая точность SVM на тестовой выборке:",
round(cm_svm$overall["Accuracy"] * 100, 2), "%\n")
##
## Общая точность SVM на тестовой выборке: 95.56 %
Модель по четырём признакам нельзя нарисовать на плоскости, поэтому
для иллюстрации строится линейный SVM на двух самых информативных
признаках — Petal.Length и Petal.Width.
Крестики — опорные векторы, круги — прочие объекты обучающей выборки;
цвет фона — предсказанный класс.
train_2d <- train_svm[, c("Petal.Length", "Petal.Width", "Species")]
svm_2d <- svm(Species ~ ., data = train_2d,
kernel = "linear", cost = tune_lin$best.parameters$cost,
scale = TRUE)
plot(svm_2d, train_2d, formula = Petal.Width ~ Petal.Length)
acc_2d <- mean(predict(svm_2d, test_svm[, c("Petal.Length", "Petal.Width")]) ==
test_svm$Species)
cat("Точность двухпризнаковой модели на тесте:", round(acc_2d * 100, 2), "%\n")
## Точность двухпризнаковой модели на тесте: 93.33 %
Для сравнения — SVM с радиальным ядром (RBF), у которого подбираются
два параметра: cost и gamma.
set.seed(42)
tune_rad <- tune(svm, Species ~ ., data = train_svm, kernel = "radial",
ranges = list(cost = c(0.1, 1, 10, 100),
gamma = c(0.01, 0.1, 1)),
tunecontrol = tune.control(cross = 10))
cat("Лучшие параметры RBF-модели:\n")
## Лучшие параметры RBF-модели:
print(tune_rad$best.parameters)
## cost gamma
## 3 10 0.01
cat("Ошибка перекрёстной проверки:", round(tune_rad$best.performance * 100, 2), "%\n")
## Ошибка перекрёстной проверки: 2.91 %
rad_acc <- mean(predict(tune_rad$best.model, test_svm) == test_svm$Species)
data.frame(
Модель = c("Линейный SVM", "SVM с RBF-ядром"),
`CV-ошибка, %` = round(c(tune_lin$best.performance,
tune_rad$best.performance) * 100, 2),
`Точность на тесте, %` = round(c(cm_svm$overall["Accuracy"],
rad_acc) * 100, 2),
check.names = FALSE
)
Вывод. Линейный SVM (cost = 1) достигает точности 95.6 % на тестовой выборке; ошибка 10-кратной перекрёстной проверки — 3.8 %. Модель с радиальным ядром даёт CV-ошибку 2.9 % и точность на тесте 93.3 %. Различие невелико (при 45 тестовых объектах один объект = 2.2 %), поэтому переходить к нелинейному ядру нет веских оснований: классы iris почти линейно разделимы. Setosa отделяется идеально, а немногочисленные ошибки относятся к перекрывающимся versicolor и virginica.
Цель: рассчитать главные компоненты с помощью
функции rda() пакета vegan, построить
ординационную диаграмму и сделать выводы.
library(vegan)
iris_num <- iris[, 1:4]
round(cor(iris_num), 2)
## Sepal.Length Sepal.Width Petal.Length Petal.Width
## Sepal.Length 1.00 -0.12 0.87 0.82
## Sepal.Width -0.12 1.00 -0.43 -0.37
## Petal.Length 0.87 -0.43 1.00 0.96
## Petal.Width 0.82 -0.37 0.96 1.00
Между Petal.Length, Petal.Width и
Sepal.Length наблюдается сильная корреляция (r =
0.82–0.96), то есть признаки во многом дублируют друг друга — это
хорошая предпосылка для снижения размерности.
rda() без правой части формулы выполняет PCA. Аргумент
scale = TRUE приводит признаки к единичной дисперсии (PCA
на матрице корреляций) и обязателен, если признаки имеют разный
разброс.
pca <- rda(iris_num, scale = TRUE)
summary(pca, display = NULL) # только собственные числа, без 150 координат объектов
##
## Call:
## rda(X = iris_num, scale = TRUE)
##
## Partitioning of correlations:
## Inertia Proportion
## Total 4 1
## Unconstrained 4 1
##
## Eigenvalues, and their contribution to the correlations
##
## Importance of components:
## PC1 PC2 PC3 PC4
## Eigenvalue 2.9185 0.9140 0.14676 0.020715
## Proportion Explained 0.7296 0.2285 0.03669 0.005179
## Cumulative Proportion 0.7296 0.9581 0.99482 1.000000
Проверка результата независимой реализацией (prcomp):
собственные числа должны совпасть.
all.equal(as.numeric(eigenvals(pca)),
prcomp(iris_num, scale. = TRUE)$sdev^2)
## [1] TRUE
eig <- as.numeric(eigenvals(pca))
prop_var <- eig / sum(eig) * 100
var_table <- data.frame(
PC = paste0("PC", seq_along(eig)),
Eigenvalue = round(eig, 3),
`Дисперсия, %` = round(prop_var, 2),
`Накопленная, %` = round(cumsum(prop_var), 2),
`Broken stick, %` = round(bstick(pca) / sum(eig) * 100, 2),
check.names = FALSE
)
var_table
screeplot(pca, bstick = TRUE, type = "barplot", legend = FALSE,
main = "Scree plot: собственные числа и модель «сломанной палки»",
ylab = "Собственное число")
abline(h = 1, col = "blue", lty = 2, lwd = 2)
legend("topright",
legend = c("Порог Кайзера (λ = 1)", "Модель «сломанной палки»"),
col = c("blue", "red"), lty = c(2, 1), pch = c(NA, 1), bty = "n")
Для выбора числа компонент применяются два критерия: критерий Кайзера
(оставляем компоненты с λ > 1) и модель «сломанной палки» (оставляем
компоненты, у которых собственное число больше ожидаемого при случайном
разбиении общей дисперсии). Первая компонента (λ = 2.92) проходит оба
критерия с большим запасом. Вторая компонента (λ = 0.91) формально
находится чуть ниже обоих порогов (1 по Кайзеру и 1.08 по «сломанной
палке»), то есть является пограничной. Тем не менее для ординационной
диаграммы сохраняются две компоненты: PC2 несёт 22.9 % дисперсии и
связана с отдельным признаком (Sepal.Width), а две оси
необходимы для построения плоского биплота. Вместе PC1 и PC2 объясняют
95.8 % общей дисперсии.
Точки — объекты (цветки), цвет — вид; эллипсы — области рассеяния
видов (стандартное отклонение); стрелки — исходные признаки.
Используется scaling = 2 (корреляционный биплот): углы
между стрелками отражают корреляции между признаками. Длины стрелок
пропорционально увеличены для читаемости — важны их направления и
относительные длины.
sp_colors <- c(setosa = "#E74C3C", versicolor = "#27AE60", virginica = "#2980B9")
pt_colors <- sp_colors[as.character(iris$Species)]
sites <- scores(pca, display = "sites", scaling = 2, choices = 1:2)
vars <- scores(pca, display = "species", scaling = 2, choices = 1:2)
# коэффициент, вписывающий стрелки в область объектов
mult <- 0.9 * min(max(abs(sites[, 1])) / max(abs(vars[, 1])),
max(abs(sites[, 2])) / max(abs(vars[, 2])))
lab <- sprintf("PC%d (%.1f %%)", 1:2, prop_var[1:2])
plot(sites, type = "n", asp = 1,
xlab = lab[1], ylab = lab[2],
xlim = range(c(sites[, 1], vars[, 1] * mult)) * 1.15,
ylim = range(c(sites[, 2], vars[, 2] * mult)) * 1.15,
main = "PCA (rda): ординационная диаграмма iris")
abline(h = 0, v = 0, lty = 3, col = "gray50")
points(sites, col = pt_colors, pch = 19, cex = 0.9)
ordiellipse(pca, groups = iris$Species, kind = "sd", scaling = 2,
col = sp_colors, lwd = 2, label = FALSE)
arrows(0, 0, vars[, 1] * mult, vars[, 2] * mult,
length = 0.1, col = "navy", lwd = 1.5)
label_pos <- c(Sepal.Length = 4, Sepal.Width = 1, Petal.Length = 3, Petal.Width = 1)
text(vars[, 1] * mult, vars[, 2] * mult, labels = rownames(vars),
pos = label_pos[rownames(vars)], col = "navy", cex = 0.9, font = 2)
legend("topleft", legend = names(sp_colors), col = sp_colors,
pch = 19, bty = "n", title = "Вид")
Нагрузки — вклад исходных признаков в главные компоненты (собственные
векторы корреляционной матрицы, scaling = 0).
loadings_mat <- scores(pca, display = "species", scaling = 0, choices = 1:2)
round(loadings_mat, 3)
## PC1 PC2
## Sepal.Length 0.521 -0.377
## Sepal.Width -0.269 -0.923
## Petal.Length 0.580 -0.024
## Petal.Width 0.565 -0.067
## attr(,"const")
## [1] 4.940963
cat("\nВклад признаков в PC1 (по убыванию |нагрузки|):\n")
##
## Вклад признаков в PC1 (по убыванию |нагрузки|):
print(round(sort(abs(loadings_mat[, 1]), decreasing = TRUE), 3))
## Petal.Length Petal.Width Sepal.Length Sepal.Width
## 0.580 0.565 0.521 0.269
cat("\nВклад признаков в PC2 (по убыванию |нагрузки|):\n")
##
## Вклад признаков в PC2 (по убыванию |нагрузки|):
print(round(sort(abs(loadings_mat[, 2]), decreasing = TRUE), 3))
## Sepal.Width Sepal.Length Petal.Width Petal.Length
## 0.923 0.377 0.067 0.024
Petal.Length, Petal.Width и
Sepal.Length имеют близкие по величине положительные
нагрузки, поэтому чем больше значение PC1, тем крупнее цветок.
Sepal.Width вносит в PC1 небольшой отрицательный
вклад.Sepal.Width, |нагрузка| ≈ 0.92; знак
нагрузок в PCA условен) — это компонента «формы» цветка, слабо связанная
с общим размером.Petal.Length и
Petal.Width почти параллельны и направлены в одну сторону —
эти признаки сильно положительно коррелируют (r ≈ 0.96).
Sepal.Width образует с остальными стрелками тупой угол —
это отрицательная (и заметно более слабая) корреляция, например с
Petal.Length r ≈ -0.43; этот признак несёт информацию, не
сводимую к общему размеру цветка.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] vegan_2.7-6 permute_0.9-10 e1071_1.7-17 caret_7.0-1 lattice_0.22-9
## [6] ggplot2_4.0.3 gmodels_2.19.1 class_7.3-23
##
## loaded via a namespace (and not attached):
## [1] gtable_0.3.6 xfun_0.60 bslib_0.12.0
## [4] recipes_1.4.0 vctrs_0.7.3 tools_4.6.1
## [7] generics_0.1.4 stats4_4.6.1 parallel_4.6.1
## [10] proxy_0.4-29 tibble_3.3.1 cluster_2.1.8.2
## [13] ModelMetrics_1.2.2.2 pkgconfig_2.0.3 Matrix_1.7-5
## [16] data.table_1.18.6.1 RColorBrewer_1.1-3 S7_0.2.2
## [19] lifecycle_1.0.5 compiler_4.6.1 farver_2.1.2
## [22] stringr_1.6.0 codetools_0.2-20 htmltools_0.5.9
## [25] sass_0.4.10 yaml_2.3.12 prodlim_2026.03.11
## [28] pillar_1.11.1 jquerylib_0.1.4 MASS_7.3-65
## [31] gdata_3.0.1 cachem_1.1.0 gower_1.0.2
## [34] iterators_1.0.14 rpart_4.1.27 foreach_1.5.2
## [37] nlme_3.1-169 parallelly_1.48.0 lava_1.9.3
## [40] gtools_3.9.5 tidyselect_1.2.1 digest_0.6.39
## [43] stringi_1.8.9 future_1.76.0 dplyr_1.2.1
## [46] reshape2_1.4.5 purrr_1.2.2 listenv_1.0.0
## [49] splines_4.6.1 fastmap_1.2.0 grid_4.6.1
## [52] cli_3.6.6 magrittr_2.0.5 survival_3.8-6
## [55] future.apply_1.20.2 withr_3.0.3 scales_1.4.0
## [58] lubridate_1.9.5 timechange_0.4.0 rmarkdown_2.32
## [61] globals_0.19.1 otel_0.2.0 nnet_7.3-20
## [64] timeDate_4052.112 evaluate_1.0.5 knitr_1.52
## [67] hardhat_1.4.3 mgcv_1.9-4 rlang_1.3.0
## [70] Rcpp_1.1.2 glue_1.8.1 pROC_1.19.1
## [73] ipred_0.9-16 rstudioapi_0.19.0 jsonlite_2.0.0
## [76] R6_2.6.1 plyr_1.8.9