3.2 Обнаружение и удаление “ненужных” предикторов
Разведочный анализ исходных данных играет очень важную роль в процессе создании эффективных предсказательных моделей. Главная его цель - понимание свойств имеющихся в наличии переменных, таких как закономерность распределения, наличие выбросов или эффекта очень низкой дисперсии, характер взаимоотношений между откликом и предикторами, оценка уровня мультиколлинеарности и др. Поскольку многие алгоритмы могут быть чувствительными к наличию предикторов, которые не несут в себе никакой или почти никакой информации, то уже на предварительном этапе некоторые из них разумно идентифицировать как “ненужные” и в дальнейшем исключить из рассмотрения.
В качестве примера используем набор данных GermanCredit
, входящий в состав пакета caret. Он содержит информацию по 1000 клиентам одного из немецких банков, (подробное описание см. на сайте UCI Machine Learning Repository). Каждый клиент описан в пространстве 61 признака, которые могут использоваться для предсказания класса кредитоспособности: отклика Class
, принимающего два значения Good
(хороший) и Bad
(плохой).
Предположим, что мы имеем дело с экстремальным случаем, когда некоторый предиктор представлен только одним уникальным значением (например, у всех клиентов банка значения этой переменной равны 1). При таком сценарии дисперсия предиктора равна нулю и он бесполезен для предсказания интересующего нас отклика. В других случаях дисперсия может быть отличной от 0, но все же недостаточно высокой для того, чтобы сделать соответствующую переменную полезной для предсказания отклика. Подобные предикторы с околонулевой дисперсией (near-zero variance) рекомендуется удалять из дальнейшего анализа (Kuhn, Johnson, 2013).
Но как обнаружить такие предикторы? Одним из свойств вариационного ряда является доля уникальных значений по сравнению с общим числом наблюдений:
library(caret)
data(GermanCredit, package = "caret")
(u <- unique(GermanCredit$ResidenceDuration))
## [1] 4 2 3 1
# Доля уникальных значений:
length(u)/nrow(GermanCredit)
## [1] 0.004
Как видим, 1000 клиентов представлены только четырьмя уникальными значениями, доля которых составляет лишь 0.4% от их общего числа.
Однако сама по себе эта доля ни о чем не говорит, поскольку подавляющее большинство признаков в таблице GermanCredit
являются индикаторными переменными, обозначающими наличие или отсутствие того или иного свойства у клиента, т.е. представлены только значениям 1 и 0. Важной является не только низкая доля уникальных значений, но еще и относительная частота этих значений. Поэтому рекомендуется (Kuhn, Johnson, 2013) рассчитывать отношение частоты наиболее часто встречающегося значения к частоте второго по встречаемости значения. Высокое отношение будет указывать на явный дисбаланс в частотах уникальных значений и, как следствие, на низкую дисперсию:
(t <- sort(table(GermanCredit$ResidenceDuration), decreasing = TRUE))
##
## 4 2 3 1
## 413 308 149 130
t[1]/t[2]
## 4
## 1.340909
На практике предлагается придерживаться следующих эмпирических правил для заключения о том, что некоторый предиктор обладает околонулевой дисперсией:
- доля его уникальных значений от общего числа наблюдений составляет не более 10%;
- отношение частот первых двух наиболее обычных его значений превышает 20.
В состав пакета caret входит функция nearZeroVar()
, которая позволяет автоматически обнаружить предикторы, удовлетворяющие этим двум условиям:
# Создадим копию данных без столбца с откликом Class:
gcred = GermanCredit[, -10]
# Функция nearZeroVar() возращает вектор номеров переменных,
# обладающих околонулевой дисперсией:
(nz = nearZeroVar(gcred))
## [1] 9 14 15 23 24 26 27 29 33 44 46 53 58
print("Имена этих переменных:")
## [1] "Имена этих переменных:"
names(gcred)[nz]
## [1] "ForeignWorker"
## [2] "CreditHistory.NoCredit.AllPaid"
## [3] "CreditHistory.ThisBank.AllPaid"
## [4] "Purpose.DomesticAppliance"
## [5] "Purpose.Repairs"
## [6] "Purpose.Vacation"
## [7] "Purpose.Retraining"
## [8] "Purpose.Other"
## [9] "SavingsAccountBonds.gt.1000"
## [10] "Personal.Female.Single"
## [11] "OtherDebtorsGuarantors.CoApplicant"
## [12] "OtherInstallmentPlans.Stores"
## [13] "Job.UnemployedUnskilled"
# Удаляем предикторы с околонулевой дисперсией:
gcred.clean = gcred[, -nz]
Описанная процедура вызывает, вероятно, некоторые сомнения у читателя, знакомого с основами метрологии. Во-первых, сама по себе близость дисперсии к нулевому значению ни о чем не говорит, поскольку все зависит от шкалы измерений (для субмолекулярных конструкций изменения размера в миллимикроны могут быть существенными, в то время, как для астрономических наблюдений ошибка в несколько километров является ничтожной). Поэтому дисперсия измерений считается недопустимо малой, если она не превосходит оценки погрешности измерений (или ошибки воспроизведения опыта). Во-вторых, следует уточнить, что эта процедура является эвристикой, полезной лишь для наблюдений, измеренных в порядковых или счетных шкалах с небольшим интервалом значений.
Другая проблема использования таких классических методов, как линейная регрессия или логистическая регрессия заключается в мультиколлинеарности. Наличие нескольких высоко коррелирующих друг с другом предикторов может привести к созданию неустойчивых решений или вообще сделать построение модели невозможным. Поскольку такие переменные несут, по сути, одинаковую информацию, то удаление части из них не приведет к заметному снижению качества модели.
Выберем наиболее коррелирующие пары переменных из таблицы GermanCredit. Составим для этого специальную функцию (А. Шипунов):
# Наибольшие значения треугольной матрицы
top.mat <- function(X, level = 0.45, N = 12, values = TRUE) {
X.nam <- row.names(X)
X.tri <- as.vector(lower.tri(X))
X.rep.g <- rep(X.nam, length(X.nam))[X.tri]
X.rep.e <- rep(X.nam, each = length(X.nam))[X.tri]
X.vec <- as.vector(X)[X.tri]
X.df <- data.frame(Var1 = X.rep.g, Var2 = X.rep.e, Value = X.vec)
{if (values)
{X.df <- X.df[abs(X.df$Value) >= level, ]
X.df <- X.df[order(-abs(X.df$Value)), ]}
else
{X.df <- X.df[order(-abs(X.df$Value)), ]
X.df <- X.df[1:N, ]}}
row.names(X.df) <- seq(1, along = X.df$Value)
return(X.df)
}
top.mat(cor(gcred.clean))
## Var1 Var2
## 1 OtherInstallmentPlans.None OtherInstallmentPlans.Bank
## 2 Housing.ForFree Property.Unknown
## 3 Personal.Male.Single Personal.Female.NotSingle
## 4 Housing.Own Housing.Rent
## 5 OtherDebtorsGuarantors.Guarantor OtherDebtorsGuarantors.None
## 6 CreditHistory.Critical CreditHistory.PaidDuly
## 7 Job.SkilledEmployee Job.UnskilledResident
## 8 Amount Duration
## 9 SavingsAccountBonds.Unknown SavingsAccountBonds.lt.100
## 10 Housing.ForFree Housing.Own
## 11 Job.Management.SelfEmp.HighlyQualified Job.SkilledEmployee
## 12 CreditHistory.PaidDuly NumberExistingCredits
## 13 CreditHistory.Critical NumberExistingCredits
## 14 CheckingAccountStatus.none CheckingAccountStatus.lt.0
## 15 CheckingAccountStatus.none CheckingAccountStatus.0.to.200
## 16 Housing.Own Property.Unknown
## Value
## 1 -0.8405461
## 2 0.7798526
## 3 -0.7380357
## 4 -0.7359677
## 5 -0.7314079
## 6 -0.6836174
## 7 -0.6524383
## 8 0.6249842
## 9 -0.5832811
## 10 -0.5484452
## 11 -0.5438517
## 12 -0.5403545
## 13 0.5013637
## 14 -0.4953575
## 15 -0.4891356
## 16 -0.4764963
В состав пакета caret входит функция findCorrelation()
, которая, как следует из ее названия, находит предикторы, чей уровень корреляции с другими предикторами в среднем превышает некоторый заданный пользователем порог (аргумент cutoff
):
# Функция findCorrelation() возвращает вектор
# номеров переменных с высокой корреляцией:
(highCor = findCorrelation(cor(gcred.clean), cutoff = 0.75))
## [1] 40 41
print("Имена этих переменных:"); names( gcred.clean)[highCor]
## [1] "Имена этих переменных:"
## [1] "Property.Unknown" "OtherInstallmentPlans.Bank"
# Удаляем эти переменные:
gcred.clean = gcred.clean[, -highCor]
Наконец, специальная функция findLinearCombos()
предназначена для нахождения и исключения переменных, связанных линейными зависимостями. Если в выборке, например, есть переменная, которая может быть выражена через сумму нескольких других переменных, то такой предиктор можно рассматривать как малоинформативный (несущий дублирующую информацию) и его можно исключить из построения модели. Результатом функции findLinearCombos()
является список из двух элементов: $linearCombos
- список найденных линейных комбинаций и $remove
- вектор индексов переменных, которые можно выразить через линейную комбинацию остальных переменных.
(linCombo <- findLinearCombos(gcred.clean))
## $linearCombos
## $linearCombos[[1]]
## [1] 30 9 10 11 12 26 27 28 29
##
## $linearCombos[[2]]
## [1] 34 9 10 11 12 31 32 33
##
## $linearCombos[[3]]
## [1] 43 9 10 11 12 41 42
##
##
## $remove
## [1] 30 34 43
# Удаляем эти переменные:
gcred.clean = gcred.clean[, -linCombo$remove]
dim(gcred.clean)
## [1] 1000 43
В результате всех этих операций число предикторов сократилось с 61 до 43.
Отметим, что очистка исходных данных от “ненужных” предикторов с использованием представленных функций может показаться излишне радикальной. Во-первых, например, непонятно, почему рекомендуется удалить признак OtherInstallmentPlans.None
, а не составляющий с ним корреляционную пару OtherInstallmentPlans.Bank
. Во-вторых, разработаны более продвинутые методы минимизации “корреляционных плеяд”: использование фактора инфляции дисперсии \(VIF\) или различные последовательные алгоритмы селекции. Наконец, для некоторых алгоритмов вышеописанные проблемы не несут реальной угрозы: например, деревья принятия решений нечувствительны к признакам с околонулевой дисперсией, а регрессия на главные компоненты способна преодолеть эффект мультиколлинеарности. Однако в условиях очень большого числа переменных подобные “простые” методы редукции могут оказаться неожиданно эффективными. Кроме того, при большом числе предикторов удаление “ненужных” переменных может ускорить вычисления.