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\) или различные последовательные алгоритмы селекции. Наконец, для некоторых алгоритмов вышеописанные проблемы не несут реальной угрозы: например, деревья принятия решений нечувствительны к признакам с околонулевой дисперсией, а регрессия на главные компоненты способна преодолеть эффект мультиколлинеарности. Однако в условиях очень большого числа переменных подобные “простые” методы редукции могут оказаться неожиданно эффективными. Кроме того, при большом числе предикторов удаление “ненужных” переменных может ускорить вычисления.