Не одним 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 признаками.


кодирование признака с 4 значениями
кодирование признака с 4 значениями

Посмотрим на кодирование нашего признака 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 определен только для задачи регрессии!

формулы для определения значения коэффициента James-Stein энкодера
формулы для определения значения коэффициента James-Stein энкодера

Итоги

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


Материал и код подготовил: Андрей Дзись, канал в ТГ @dzis_science.


Материал защищен авторскими правами.

Копирование и использование материалов возможно только с согласия автора, с упоминанием источника.

Комментарии (0)