05 июля 2015

Подготовка данных для создания предсказательных моделей: обнаружение и удаление "ненужных" предикторов



Стадия подготовки исходных данных играет очень важную роль в процессе создании эффективных предсказательных моделей. Следующие сообщения будут посвящены основным процедурам, используемым на этой стадии. Начнем с обнаружения и удаления "ненужных" предикторов (признаков).





Удаление переменных из исходного набора данных может быть полезным по нескольким причинам:
  • Меньшее количество переменных может привести к значительному ускорению вычислений.
  • Наличие нескольких высоко коррелирующих друг с другом предикторов может привести к созданию неустойчивых решений или вообще сделать вычисления невозможными (например, при использовании таких классических методов, как линейная регрессия или логистическая регрессия; подробнее о проблеме мультиколлинеарности см. здесь). Кроме того, включение в модель нескольких высоко коррелирующих друг с другом предикторов часто не имеет смысла, поскольку, по сути, они несут в себе одинаковую информацию. Соответственно, удаление некоторых из таких предикторов не приведет к заметному снижению качества модели.
  • Некоторые алгоритмы могут быть чувствительными также к наличию предикторов, которые не несут в себе никакой или почти никакой информации. С математической точки зрения, такие переменные характеризуются очень низкой дисперсией.
В качестве примера используем набор данных GermanCredit, входящий в состав пакета caret. Этот широко известный и хорошо изученный набор данных часто применяется в качестве эталона при сравнениии эффективности методов машинного обучения, и мы также неоднократно будем к нему возвращаться. В состав таблицы GermanCredit входит информация по 1000 клиентам одного из немецких банков, собранная проф. Г. Хофманном из Гамбургского университета (подробное описание см. на сайте UCI Machine Learning Repository). Каждый клиент описан по 61 признаку, многие из которых являются индикаторными переменными (т.е. представлены значениям 1 и 0, обозначающими наличие или отсутствие того или иного признака у соответствующего клиента). Кроме того, имеется переменная-отклик Class с двумя значениями - Good (хороший) и Bad (плохой), отражающими кредитоспособность клиентов. Информация по 61 признаку предназначена для предсказания класса кредитоспособности, однако в этом сообщении мы ограничимся лишь обнаружением "ненужных" предикторов.

# Загрузка данных:
library(caret)
data(GermanCredit)
 
# Просмотр структуры данных:
str(GermanCredit)
'data.frame': 1000 obs. of  62 variables:
 $ Duration                              : int  6 48 12 42 24 36 24 36 12 30 ...
 $ Amount                                : int  1169 5951 2096 7882 4870 9055 2835 6948 3059 5234 ...
 $ InstallmentRatePercentage             : int  4 2 2 2 3 2 3 2 2 4 ...
 $ ResidenceDuration                     : int  4 2 3 4 4 4 4 2 4 2 ...
 $ Age                                   : int  67 22 49 45 53 35 53 35 61 28 ...
 $ NumberExistingCredits                 : int  2 1 1 1 2 1 1 1 1 2 ...
 $ NumberPeopleMaintenance               : int  1 1 2 2 2 2 1 1 1 1 ...
 $ Telephone                             : num  0 1 1 1 1 0 1 0 1 1 ...
 $ ForeignWorker                         : num  1 1 1 1 1 1 1 1 1 1 ...
 $ Class                                 : Factor w/ 2 levels "Bad","Good": 2 1 2 2 1 2 2 2 2 1 ...
 $ CheckingAccountStatus.lt.0            : num  1 0 0 1 1 0 0 0 0 0 ...
 $ CheckingAccountStatus.0.to.200        : num  0 1 0 0 0 0 0 1 0 1 ...
 $ CheckingAccountStatus.gt.200          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ CheckingAccountStatus.none            : num  0 0 1 0 0 1 1 0 1 0 ...
 $ CreditHistory.NoCredit.AllPaid        : num  0 0 0 0 0 0 0 0 0 0 ...
 $ CreditHistory.ThisBank.AllPaid        : num  0 0 0 0 0 0 0 0 0 0 ...
 $ CreditHistory.PaidDuly                : num  0 1 0 1 0 1 1 1 1 0 ...
 $ CreditHistory.Delay                   : num  0 0 0 0 1 0 0 0 0 0 ...
 $ CreditHistory.Critical                : num  1 0 1 0 0 0 0 0 0 1 ...
 $ Purpose.NewCar                        : num  0 0 0 0 1 0 0 0 0 1 ...
 $ Purpose.UsedCar                       : num  0 0 0 0 0 0 0 1 0 0 ...
 $ Purpose.Furniture.Equipment           : num  0 0 0 1 0 0 1 0 0 0 ...
 $ Purpose.Radio.Television              : num  1 1 0 0 0 0 0 0 1 0 ...
 $ Purpose.DomesticAppliance             : num  0 0 0 0 0 0 0 0 0 0 ...
 $ Purpose.Repairs                       : num  0 0 0 0 0 0 0 0 0 0 ...
 $ Purpose.Education                     : num  0 0 1 0 0 1 0 0 0 0 ...
 $ Purpose.Vacation                      : num  0 0 0 0 0 0 0 0 0 0 ...
 $ Purpose.Retraining                    : num  0 0 0 0 0 0 0 0 0 0 ...
 $ Purpose.Business                      : num  0 0 0 0 0 0 0 0 0 0 ...
 $ Purpose.Other                         : num  0 0 0 0 0 0 0 0 0 0 ...
 $ SavingsAccountBonds.lt.100            : num  0 1 1 1 1 0 0 1 0 1 ...
 $ SavingsAccountBonds.100.to.500        : num  0 0 0 0 0 0 0 0 0 0 ...
 $ SavingsAccountBonds.500.to.1000       : num  0 0 0 0 0 0 1 0 0 0 ...
 $ SavingsAccountBonds.gt.1000           : num  0 0 0 0 0 0 0 0 1 0 ...
 $ SavingsAccountBonds.Unknown           : num  1 0 0 0 0 1 0 0 0 0 ...
 $ EmploymentDuration.lt.1               : num  0 0 0 0 0 0 0 0 0 0 ...
 $ EmploymentDuration.1.to.4             : num  0 1 0 0 1 1 0 1 0 0 ...
 $ EmploymentDuration.4.to.7             : num  0 0 1 1 0 0 0 0 1 0 ...
 $ EmploymentDuration.gt.7               : num  1 0 0 0 0 0 1 0 0 0 ...
 $ EmploymentDuration.Unemployed         : num  0 0 0 0 0 0 0 0 0 1 ...
 $ Personal.Male.Divorced.Seperated      : num  0 0 0 0 0 0 0 0 1 0 ...
 $ Personal.Female.NotSingle             : num  0 1 0 0 0 0 0 0 0 0 ...
 $ Personal.Male.Single                  : num  1 0 1 1 1 1 1 1 0 0 ...
 $ Personal.Male.Married.Widowed         : num  0 0 0 0 0 0 0 0 0 1 ...
 $ Personal.Female.Single                : num  0 0 0 0 0 0 0 0 0 0 ...
 $ OtherDebtorsGuarantors.None           : num  1 1 1 0 1 1 1 1 1 1 ...
 $ OtherDebtorsGuarantors.CoApplicant    : num  0 0 0 0 0 0 0 0 0 0 ...
 $ OtherDebtorsGuarantors.Guarantor      : num  0 0 0 1 0 0 0 0 0 0 ...
 $ Property.RealEstate                   : num  1 1 1 0 0 0 0 0 1 0 ...
 $ Property.Insurance                    : num  0 0 0 1 0 0 1 0 0 0 ...
 $ Property.CarOther                     : num  0 0 0 0 0 0 0 1 0 1 ...
 $ Property.Unknown                      : num  0 0 0 0 1 1 0 0 0 0 ...
 $ OtherInstallmentPlans.Bank            : num  0 0 0 0 0 0 0 0 0 0 ...
 $ OtherInstallmentPlans.Stores          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ OtherInstallmentPlans.None            : num  1 1 1 1 1 1 1 1 1 1 ...
 $ Housing.Rent                          : num  0 0 0 0 0 0 0 1 0 0 ...
 $ Housing.Own                           : num  1 1 1 0 0 0 1 0 1 1 ...
 $ Housing.ForFree                       : num  0 0 0 1 1 1 0 0 0 0 ...
 $ Job.UnemployedUnskilled               : num  0 0 0 0 0 0 0 0 0 0 ...
 $ Job.UnskilledResident                 : num  0 0 1 0 0 1 0 0 1 0 ...
 $ Job.SkilledEmployee                   : num  1 1 0 1 1 0 1 0 0 0 ...
 $ Job.Management.SelfEmp.HighlyQualified: num  0 0 0 0 0 0 0 1 0 1 ...


Предикторы с околонулевой дисперсией

Предположим, что мы имеем дело с экстремальным случаем, когда некоторый предиктор представлен только одним уникальным значением (например, у всех клиентов банка значения этой переменной равны 1). При таком сценарии дисперсия предиктора равна нулю и он бесполезен для предсказания интересующего нас отклика. В других случаях дисперсия может быть отличной от 0, но все же недостаточно высокой для того, чтобы сделать соответствующий предиктор полезным для предсказания отклика. Подобные переменные с околонулевой дисперсией ("near-zero variance") рекомендуется удалять из дальнейшего анализа (Kuhn & Johnson 2013).

Но как обнаружить такие предикторы? Одним из их отличительных свойств является низкая доля уникальных значений по сравнению с общим числом наблюдений. Рассмотрим простой пример количественной переменной, обладающей этим свойством:

x = c(1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 
      1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1,
      1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
      1, 1, 1, 2, 3, 1, 1, 1, 1, 1, 1,
      2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)
 
# Уникальные значения x:
unique(x)
[1] 1 2 3
 
# Доля уникальных значений:
3/55
[1] 0.05454545


Как видим, 55 наблюдений, входящих в вектор x, представлены только тремя уникальными значениями, доля которых составляет лишь 5.5% от общего числа наблюдений.

Однако только низкой доли уникальных значений недостаточно. Так, многие (но не все!) индикаторные переменные, представленные только двумя значениями (0 и 1), подпадали бы под это условие. Важной является не только низкая доля уникальных значений, но еще и относительная частота этих значений. В частности, Kuhn & Johnson (2013) рекомендуют рассчитывать отношение частоты наиболее часто встречающегося значения к частоте второго по встречаемости значения - высокое отношение будет указывать на явный дисбаланс в частотах уникальных значений (и. как следствие, на низкую дисперсию):

# Частоты встречаемости имеющихся значений:
table(x)
x
 1  2  3 
50  3  2
 
# Отношение частот первых двух наиболее обычных значений:
50/3
[1] 16.666

На практике можно придерживаться следующих эмпирических правил для заключения о том, что некоторый предиктор обладает околонулевой дисперсией (Kuhn & Johnson 2013):
  • Доля его уникальных значений от общего числа наблюдений составляет не более 10%;
  • Отношение частот первых двух наиболее обычных его значений превышает 20.
В состав пакета caret входит функция nearZeroVar(), которая позволяет автоматически обнаружить предикторы, удовлетворяющие этим двум условиям:

# Создадим копию данных без столбца с откликом Class:
gcred = GermanCredit[, -10]
 
# Функция nearZeroVar() возращает вектор с номерами переменных,
# обладающих околонулевой дисперсией:
nz = nearZeroVar(gcred)
 
nz
 [1]  9 14 15 23 24 26 27 29 33 44 46 53 58
 
# Имена этих переменных:
names(gcred)[nz]
 [1] "ForeignWorker"                      "CreditHistory.NoCredit.AllPaid"    
 [3] "CreditHistory.ThisBank.AllPaid"     "Purpose.DomesticAppliance"         
 [5] "Purpose.Repairs"                    "Purpose.Vacation"                  
 [7] "Purpose.Retraining"                 "Purpose.Other"                     
 [9] "SavingsAccountBonds.gt.1000"        "Personal.Female.Single"            
[11] "OtherDebtorsGuarantors.CoApplicant" "OtherInstallmentPlans.Stores"      
[13] "Job.UnemployedUnskilled"
 
# Удаляем предикторы с околонулевой дисперсией:
gcred.clean = gcred[, -nz]

Функция nearZeroVar() имеет несколько управляющих аргументов, включая uniqueCut и freqCut, позволяющих изменять принятые по умолчанию пороговые величины доли уникальных значений и отношения частот первых двух наиболее обычных значений соответственно. Кроме того, при помощи аргументов foreach и allowParallel вычисления, выполняемые nearZeroVar(), можно запустить одновременно в несколько потоков, что увеличит скорость этих вычислений (актуально при работе с большими объемами данных).


Высоко коррелирующие предикторы

Обнаружение сильно коррелирующих предикторов при небольшом их количестве не составляет особого труда - для этого достаточно воспользоваться базовой функцией cor() и рассчитать корреляционную матрицу. Однако при большом числе предикторов визуальное исследование такой матрицы становится почти невозможным, что требует использования других подходов. Одним из удобных средств визуализации корреляционных отношений между переменными в таких случаях является использование "тепловых карт" (heat maps), ячейки которых залиты цветом в соответствии с уровнем корреляции. Более того, для выявления групп высоко коррелирующих переменных ячейки на таких диаграммах часто упорядочивают в соответствии с результатами параллельно выполняемого кластерного анализа. Построить такую диаграмму в R можно при помощи нескольких функций. Удобной, в частности, является функция corrplot() из одноименного пакета (эта функция имеет большое количество параметров, позволяющих выполнять тонкую настройку внешнего вида диаграммы - подробнее см. ?corrplot):

library(corrplot)
corVar = cor(gcred.clean, method = "spearman")
corrplot(corVar, method = "square")


Чем интенсивнее цвет заливки квадрата в каждой ячейке приведенной тепловой карты и чем больше размер этого квадрата, тем выше (по модулю) корреляция между соответствующими переменными. Можно заметить небольшие группы коррелирующих друг с другом переменных (например, группа переменных, чьи имена начинаются с "CheckingAccountStatus").

В состав пакета caret входит функция findCorrelation(), которая, как следует из ее названия, находит предикторы, чей уровень корреляции с другими предикторами в среднем превышает некоторый заданный пользователем порог (аргумент cutoff):

highCor = findCorrelation(corVar, cutoff = 0.75)
 
# Функция findCorrelation() возращает вектор с номерами переменных,
# значительно коррелирующих с другими переменными:
highCor
[1] 40 42
 
# Имена этих переменных:
names(gcred.clean)[highCor]
[1] "Property.Unknown"          
[2] "OtherInstallmentPlans.None"
 
# Удалим эти переменные:
gcred.clean = gcred.clean[, -highCor]


Обратите внимание, что использование рассмотренных выше подходов для обработки исходных данных, т.е. удаление предикторов с околонулевой дисперсией и высоко коррелирующих предикторов, не является обязательным. Для некоторых алгоритмов создания предсказательных моделей наличие таких предикторов не обязательно представляет проблему (например, деревья принятия решений не чувствительны к признакам с околонулевой дисперсией). Кроме того, проблему мультиколлинеарности можно преодолеть при помощи метода главных компонент. Необходимость удаления тех или иных предикторов из исходных данных определяется несколькими факторами, такими как общее количество имеющихся предикторов, планируемые к использованию алгоритмы построения моделей, количество "проблемных" переменных, имеющиеся вычислительные мощности и др.

4 комментария :

Анонимный комментирует...

Отличная статья. Спасибо за ценную информацию.

Анонимный комментирует...

Сергей,
как всегда все просто и понятно написано. Скорее бы приступить к самим моделям:)
Спасибо Вам.

Ирина Голощапова комментирует...

Наверное, смысловое значение переменной также может иметь значение. В случае, если это фиктивная переменная, обозначающая кризис, редко случающиеся события будут, скорее всего, оказывать высокое влияние на зависимую переменную (например, уровень зарплат).

Olga комментирует...

+1. Поэтому нужно обращать внимание на корреляцию зависимой и независимых переменных

Отправить комментарий