Не одним One-Hot единым...
Привет, хабр! Хотел бы сделать краткий экскурс про работу с категориальными признаками, который часто на просторах интернета обходят стороной. В данной статье я постараюсь расширить базовые понятия по данной тематике и иллюстрировать их примерами.
Под категориальными данными мы понимаем данные, которые не имеют численного представления, они могут иметь как и два уникальных значения (бинарные признаки), так и более.
Для работы с признаками надо произвести кодирование категориальных признаков - процедуру, которая представляет собой некоторое преобразование категориальных признаков в численное представление по некоторым оговоренным ранее правилам.
Данная тема для меня очень острая, так ей не так в большинстве курсов по DS либо не уделяют совсем, либо уделяют недостаточное количество времени.
Зачем это надо?
Как мы знаем из теории, простым линейным моделям для корректной работы в качестве входных данных мы должны передавать только численные признаки, поэтому для них необходимость таких преобразований (категориальный признак -> некоторые численное представление) просто необходимо!
Однако, опять же, возвращаясь к теории, мы знаем, что решающие деревья могут работать с сырыми категориальными признаками, однако на практике имеет место следующее:
Попробуем обучить решающее дерево для задачи бинарной классификации в реализации sklearn (из модуля tree) на игрушечном датасете:
Импортируем метод DecisionTreeClassifier из sklearn.tree и конечно же подгрузим pandas:
import pandas as pd
from sklearn.tree import DecisionTreeClassifier
Далее создадим игрушечный датасет
data = pd.DataFrame()
data['A'] = ['a','a','b','a']
data['B'] = ['b','b','a','b']
data['Class'] = [0, 0, 1, 0]
Обучим модель:
tree = DecisionTreeClassifier()
tree.fit(data[['A','B']], data['Class'])
Мы видим ошибку значений ValueError, которая говорит нам, что у нас на вход реализации в sklearn надо подавать строго численные признаки!
Таким образом, мы обосновали преобразование даже для тех моделей, которые в теории могут работать с сырыми категориальными признаками.
Какие есть категориальные кодировщики?
Как говорилось ранее Encoder'ы, они же кодировщики представляют собой некоторое правило перевода категориальных признаков в численные.
В первую очередь разберем наиболее популярные из них - Label и One-Hot encoder'ы.
Label Encoder
Данный тип кодирования является наиболее часто используемым, преобразование представляет собой однозначное соответствие число <-> уникальное значение категориального признака.
Первое (выбранное каким-то образом) уникальное значение кодируется нулем, второе единицей, и так далее, последнее кодируется числом, равным количеству уникальных значений минус единица.
Давайте посмотрим на примере:
Предположим у нас есть категориальный признак бренда автомобиля, со значениями BMW, Mercedes, Nissan, Infinity, Audi, Volvo, Skoda.
Создадим искусственный признак brand и закодируем его с помощью реализации sklearn.
data = pd.DataFrame()
data['brand'] = ['BMW', 'Mercedes', 'Nissan', 'Infinity', 'Audi', 'Volvo', 'Skoda']*5
Для чистоты эксперимента перемешаем наши данные с помощью метода sample(), зафиксируем random_state для воспроизводимости и посмотрим на шапку данных:
data = data.sample(frac=1,random_state=42).reset_index(drop=True)
data.head(10)
Применим Label Encoder:
from sklearn.preprocessing import LabelEncoder
labelencoder = LabelEncoder()
data_new = labelencoder.fit_transform(data.values)
data_new[:10]
Посмотрим на "правило преобразования" с помощью метода classes_
labelencoder.classes_
Реализация Label Encoder в sklearn прежде всего сортирует по алфавиту уникальные значения, потом присваивает им порядковый номер!
Давайте поговорим про главный недостаток Label Encoder'a - создание избыточных зависимостей в данных.
После преобразования получилось, что по данному признаку значение Volvo имеет численное значение 6, а BMW 1, что дает нам право говорить, что Volvo в 6 раз больше (круче и тд.) чем BMW по признаку brand.
Однако, в исходных данных таких зависимостей не было, что и является существенным недостатком данного варианта кодирования.
One-Hot Encoder
Данный тип кодирования, основывается на создании бинарных признаков, которые показывают принадлежность к уникальному значению. Проще говоря, на примере нашего признака brand, мы создаем бинарные признаки для всех уникальных значений: brand_Volvo, brand_BMW, ..., где признак принадлежности к бренду Volvo brand_Volvo имеет значение 1, если объект в признаке brand имеет значение Volvo и нуль при всех других. Давайте посмотрим на примере признака brand:
from sklearn.preprocessing import OneHotEncoder
onehotencoder = OneHotEncoder()
data_new = onehotencoder.fit_transform(data.values)
pd.DataFrame(data_new.toarray(),
columns=onehotencoder.categories_).head(10)
Главный недостаток One-Hot Encoder'a заключается в существенном увеличении объема данных, так как большие по количеству уникальных значений признаки кодируются большим количеством бинарных признаков.
Label Encoder и One-Hot Encoder представлены в библиотеке sklearn. Далее рассмотренные кодировщики представлены в библиотеке category_encoders!
Binary Encoder
Для решения проблемы One-Hot Encoding'а с размером получаемого после кодирования пространства, была предложена идея, использующая в себе принцип перевода десятичных чисел в двоичное представление.
Принцип перевода заключается в том, что десятичное число N можно представить log(N), где log - логарифм по основанию 2, бинарными значениями, принимающими значения {0,1}. Например число 22 можно представить как 10110, т.е 5 битами.
Давайте тогда сформулируем идею кодирования. Предположим у нас есть N уникальных значений категориального признака, тогда мы можем закодировать его, используя не N бинарных признаков, а всего лишь log(N), представляя порядковый номер уникального значения в виде строки двоичного представления.
Вернемся к примеру с brand. Так, у нас всего 7 уникальных значений, то для данного типа кодирования достаточно 3 признаков, вместо 7. Тогда Volvo (первое уникальное значение в признаке, порядковый номер 1, в двоичном 001) будет закодирован как [0,0,1].
Обратите внимание, что в Binary Encoder'е уникальные значение никак не сортируются, т.е. которое попалось первым в данных будет закодировано меньшим порядковым числом! Кодирование зависит от расположения строк в данных!
Посмотрим на преобразование:
!pip install category_encoders
from category_encoders.binary import BinaryEncoder
Вызов, как в sklearn:
bn = BinaryEncoder()
bn.fit_transform(data.values)[:10]
Основная проблема данного подхода, что в погоне за оптимизацией количества признаков после преобразования, теряется интерпретируемость бинарных признаков, которая характерна признакам One-Hot Encoder.
Далее, чтобы не растягивать повествование, опишем оставшиеся две группы категориальных энкодеров, о которых вы вряд ли слышали ранее: контрастные и таргет кодировщики.
Contrast Encoding
По своей сути, контрастные энкодеры можно назвать гибридом, между Binary и One-Hot Encoder'ами.
"Гибридом" они являются потому, что они так же как и One-Hot Encoder кодируют N уникальных признаков несколькими признаками, если быть точнее N-1 признаком, однако они не являются бинарными! Данные признаки принято называть dummy переменными.
Разберем на примере Helmert Encoder и Backward-Difference Encoder:
Helmert Encoder кодирует по следующему принципу: N признаков кодируются матрицей (N, N-1), где на главной диагонали матрицы и выше нее находятся единицы со знаком минус, а сразу под главной диагональю идет порядковый номер значения и ниже него нули.
Важный момент, что при кодировании Helmert Encoder идет сортировка при кодировании уникальных значений!
Для наглядности, закодируем отсортированный список уникальных значений, а затем посмотрим как это будет выглядеть в данных.
from category_encoders.helmert import HelmertEncoder
he = HelmertEncoder(drop_invariant=True)
he.fit_transform(sorted(['BMW', 'Mercedes', 'Nissan', 'Infinity', 'Audi', 'Volvo', 'Skoda']))
['Audi', 'BMW', 'Infinity', 'Mercedes', 'Nissan', 'Skoda', 'Volvo']
Тогда на всех данных кодирование будет иметь следующий вид:
he = HelmertEncoder(drop_invariant=True)
he.fit_transform(data.values)[:10]
Backward-Difference Encoder кодирует категориальные данные по схожей схеме, что и Helmert Encoder, т.е с разницей до и после главной диагонали. Здесь проще посмотреть иллюстрацию кодирования категориального признака с N=4 уникальными значениями k = N-1 = 3 dummy признаками.
Посмотрим на кодирование нашего признака brand, сначала на отсортированных уникальных значениях, потом на всех данных:
from category_encoders.backward_difference import BackwardDifferenceEncoder
bd = BackwardDifferenceEncoder(drop_invariant=True)
bd.fit_transform(sorted(['BMW', 'Mercedes', 'Nissan', 'Infinity', 'Audi', 'Volvo', 'Skoda']))
bd = HelmertEncoder(drop_invariant=True)
bd.fit_transform(data.values)[:10]
Target Encoding
Основная цель, объядиняющая данный тип кодировщиков, заключается в использовании целевой метки, для кодирования категориальных признаков.
К Target Encoder'aм относятся Target Encoder, Leave-One-Out Encoder, James-Stein Encoder.
Target Encoder реализован "под капотом" в модели градиентного бустинга над решающими деревьями CatBoost!
Target Encoder для задачи регрессии использует среднее значение целевой метки по данному значению категориального признака.
Другими словами, если у нас задача предсказания цены авто, целевая метка - цена авто, то каждое значение марки авто в нашем признаке brand кодируется средней ценой автомобиля данного бренда.
Target Encoder для задачи бинарной классификации использует вероятность единичного класса для данного значения категориального признака.
Другими словами, если у нас задача предсказания цены продажи авто (продано/не продано), единичный класс - успешная продажа авто, то каждое значение марки авто в нашем признаке brand кодируется вероятностью продажи авто.
Важно понимать, что для тестовой выборки кодирования производится значениями, полученными на обучающей выборке. Поэтому, важно смотреть за статистической схожестью выборок, в противном случае теряется главный плюс данного кодирования - сохранение исходной зависимости между признаком и целевой меткой во время кодирования.
Leave-One-Out Encoder является расширением Target Encoder'a, в котором, кодирование конкретного объекта обучающей выборки производится с использованием для подсчета среднего/вероятности единичного класса без учета значения данного объекта (т.е мы как раз удаляем его значение, отсюда и происходит название кодировщика). Для тестовой выборки ничем не отличается от Targer Encoder'a.
James-Stein Encoder является некоторым средневзешенным между значениями для данного значения категориального признака и значением для всей выборки. Важным моментом является тот факт, что из формул для веса B, можно сказать, что данный энкодер и его оптимальный параметр B определен корректно и однозначно в случае, когда целевая метка распределена нормально!
James-Stein Encoder определен только для задачи регрессии!
Итоги
В данной статье, мы познакомились с вами с различными методами кодирования категориальных признаков, узнали плюс и минусы их использования. Надеюсь эта информация будет полезна как и начинающим DS, так и уже матерым специалистам. В данной теме мы осветили важный способ кодирования хеширование (Hashing) и, основанный на нем, трюк хеширования (Hashing Trick), который я постараюсь осветить в будущих статьях.
Материал и код подготовил: Андрей Дзись, канал в ТГ @dzis_science.
Материал защищен авторскими правами.
Копирование и использование материалов возможно только с согласия автора, с упоминанием источника.