Кодирование категориальных переменных

Все курсы > Анализ и обработка данных > Занятие 11

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

Откроем ноутбук к этому занятию

Подготовим простые учебные данные кредитного скоринга.

данные кредитного скоринга

Про категориальные переменные

Вначале в целом повторим как выявлять и исследовать категориальные переменные.

Методы .info(), .unique(), value_counts()

Начать исследование категориальных переменных можно с изучения типа данных. Для этого подойдут метод .info() или атрибут dtypes.

При этом категориальные признаки часто могут «прятаться» в типах int и float. В этом случае для их выявления можно изучить распределение данных.

Отдельные категории можно посмотреть с помощью метода .unique().

С помощью методов .values_counts() библиотеки Pandas и np.unique() библиотеки Numpy можно посмотреть и количество объектов в каждой категории.

Последовательное применение методов .value_counts() и .count() выведет общее количество уникальных категорий.

Выведем категории на графике.

распределение данных по категориям

Тип данных category

Хорошая практика — перевести категориальную переменную в тип данных category. Зачастую (но не всегда, например, если много категорий) это ускоряет работу с категориями и уменьшает использование памяти.

Можно воспользоваться уже знакомым нам методом .astype().

Функция pd.Categorical() позволяет прописать, в частности, сами категории, а также указать, есть ли в переданных категориях порядок или нет.

Воспользуемся атрибутами categories и dtype.

Атрибут codes выводит коды каждой из категорий (мы воспользуемся этим в дальнейшем при кодировании).

Категории можно переименовать.

применение cat.rename_categories() для переименования категорий

Убедимся, что нужные нам признаки преобразованы в тип category.

Кардинальность данных

Большое количество уникальных категорий в столбце называется высокой кардинальностью (high cardinality) признака. В частности, потенциально (если бы у нас было больше данных) признак City мог бы обладать высокой кардинальностью.

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

создание нового признака на основе признака с высокой кардинальностью

Дополнительным полезным свойством нового признака будет то, что на основе изначальных данных алгоритм бы не увидел разницы между Москвой и Владивостоком и Москвой и Екатеринбургом (а вполне вероятно, что в данных она есть). В новом же признаке, по сути делящем города по принадлежности к европейской и азиатской частям России, такую разницу выявить получится.

Базовые методы кодирования

Кодирование через cat.codes

Как уже было сказано выше, кодировать категориальную переменную можно через атрибут cat.codes.

кодирование через атрибут cat.codes

Mapping

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

кодирование категориальной переменной с помощью фунции map(), вариант 1

Словарь в функцию map() можно передать и так.

кодирование категориальной переменной с помощью фунции map(), вариант 2

Label Encoder

Рассмотрим класс LabelEncoder библиотеки sklearn. Этот класс преобразует n категорий в числа от 1 до n. Применим его к целевой переменной (бинарная категориальная переменная).

На вход LabelEncoder принимает только одномерные массивы (например, Series)

LabelEncoder (бинарный признак)

Для категорий, в которых больше двух классов, но нет внутренней иерархии (номинальные данные), этот encoder подходит хуже, потому что построенная на основе преобразованных данных модель может подумать, что между категориями есть иерархия, когда в действительности ее нет.

LabelEncoder (многоклассовый признак, номинальные данные)

Но даже для порядковых категориальных данных этот способ вряд ли подойдет, потому что LabelEncoder не видит порядка в данных.

LabelEncoder (многоклассовый признак, порядковые данные)

Как вы видите, на второе место в иерархии LabelEncoder поместил класс Good, что конечно является ошибкой. Таким образом, можно сказать, что LabelEncoder лучше всего справляется с бинарными категориальными данными.

Ordinal Encoder

С порядковыми категориальными данными справится OrdinalEncoder, которому при создании объекта класса можно передать иерархию категорий.

На вход OrdinalEncoder принимает только двумерные массивы.

Ordinal Encoder

Убедимся, что иерархия категорий не нарушена.

OneHotEncoding

Как уже было сказано, номинальные данные нельзя заменять числами 1, 2, 3,…, так как алгоритм ML на этапе обучения подумает, что речь идет о порядковых данных. Нужно использовать one-hot encoder. С этим инструментом мы уже познакомились, когда рассматривали основы нейронных сетей.

Класс OneHotEncoder

Вначале применим класс OneHotEncoder библиотеки sklearn.

класс OneHotEncoder библиотеки sklearn (без названия столбцов)

Выведем новые признаки с помощью метода .get_feature_names_out().

Используем вывод этого метода, чтобы добавить названия столбцов.

класс OneHotEncoder библиотеки sklearn (с названиями столбцов)

Присоединим новые признаки к исходному датафрейму, удалив, разумеется, признак City.

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

класс OneHotEncoder библиотеки sklearn (drop = 'first')

Функция pd.get_dummies()

Еще один способ — использовать функцию pd.get_dummies() библиотеки Pandas. Применим функцию к столбцу City.

pd.get_dummies() библиотеки Pandas

Уменьшить длину новых столбцов можно через параметры prefix и prefix_sep.

pd.get_dummies(): параметры prefix и prefix_sep

Опять же, можно не использовать первую dummy-переменную.

pd.get_dummies(): drop_first = True

Библиотека category_encoders

Рассмотрим еще один способ выполнить one-hot encoding через соответствующий инструмент⧉ очень полезной библиотеки category_encoders.

Импортируем библиотеку и применим класс OneHotEncoder.

one-hot encoding библиотеки category_encoders

Что очень удобно, класс OneHotEncoder библиотеки category_encoders вставил новые столбцы сразу в исходный датафрейм и удалил исходный признак.

Сравнение инструментов

Создадим два очень простых датасета из одного признака: один обучающий, второй — тестовый. В первом в этом признаке (назовем его recom) будет три категории: yes, no, maybe. Во втором, только две, yes и no.

простой обущающий датасет
простой тестовый датасет

Теперь применим каждый из приведенных выше инструментов к этим датасетам (напомню, что обучать кодировщик мы должны на обущающей выборке, чтобы избежать утечки данных).

pd.get_dummies()

Функция pd.get_dummies() не «запоминает» категории при обучении.

функция pd.get_dummies() не "запоминает" категории при обучении: обучающая выборка
функция pd.get_dummies() не "запоминает" категории при обучении: тестовая выборка

При попытке обучить модель будет ошибка.

OHE sklearn

Посмотрим, как с этим справится класс OneHotEncoder библиотеки sklearn.

запоминание категорий при обучении: класс OneHotEncoder библиотеки sklearn (train)
запоминание категорий при обучении: класс OneHotEncoder библиотеки sklearn (test)

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

OHE category_encoders

Попробуем инструмент из библиотеки category_encoders.

запоминание категорий при обучении: класс OneHotEncoder библиотеки category_encoders (train)
запоминание категорий при обучении: класс OneHotEncoder библиотеки category_encoders (test)

Проблема OHE

У OneHotEncoding есть одна проблема. При высокой кардинальности признака, создается очень много новых столбцов, а сама матрица становится разреженной.

OneHotEncoding и высокая кардинальность признака

Способы преодоления этой проблемы будут рассмотрены на курсе ML для продолжающих.

Binning

Некоторые (в частности, мультимодальные) количественные распределения не поддаются трансформации и приведению, например, к нормальному распределению.

Для того чтобы извлечь ценную информацию из таких признаков можно попробовать сделать переменные категориальными, разбив данные на интервалы, которые и будут классами нового признака. Такой подход называется binning или bucketing.

Вновь обратимся к датасету о недвижимости в Бостоне и, в частности, рассмотрим переменную TAX.

признак TAX датасета boston

Как мы видим, распределение вряд ли можно трансформировать, используя какое-либо преобразование. Применим binning.

На равные интервалы

Подход binning на равные интервалы (binning with equally spaced boundaries) предполагает, что мы берем диапазон от минимального до максимального значений и делим его на нужное нам количество равных частей (если мы хотим получить три интервала, то нам нужно четыре границы).

Создадим названия категорий.

Применим функцию pd.cut(). В параметр bins мы передадим интервалы, в labels — названия категорий.

Посмотрим на результат.

binning на равные интервалы

Границы и количество элементов в них можно получить с помощью метода .value_counts().

Результат этого метода позволяет выявить недостаток подхода binning на равные интервалы. Количество объектов внутри интревалов сильно различается. Преодолеть эту особенность можно с помощью деления по квантилям.

По квантилям

Binning по квантилям (quantile binning) позволяет разделить наблюдения не по значениям признака, а по количеству объектов в интервале. Например, выберем разделение на три части.

Применим функцию pd.qcut().

binning по квантилям

Как вы видите, в данном случае количество объектов примерно одинаковое. Наглядную иллюстрацию двух подходов можно посмотреть здесь⧉.

KBinsDiscretizer

Эти же задачи можно решить с помощью класса KBinsDiscretizer⧉ библиотеки sklearn. Рассмотрим три основных параметра класса:

  • параметр strategy определяет как будут делиться интервалы
  • параметр encode определяет как закодировать интервалы
    • ordinal, т.е. числами от 1 до n интервалов
    • one-hot encoding
  • количество интервалов n_bins

Применим каждую из стратегий. Так как в категориях заложен порядок, выберем ordinal кодировку.

strategy = uniform

strategy = quantile

strategy = kmeans

Еще одно сравнение стратегий разделения на интервалы можно посмотреть здесь⧉.

С помощью статистических показателей

Дополнительно замечу, что интервалы можно заполнить каким-либо статистическим показателем. Например, медианой. Для наглядности снова создадим только три интервала и найдем мединное значение внутри каждого из них.

Воспользуемся функцией binned_statistic()⧉ модуля scipy.stats. Функция возвращает медианы каждого из интервалов, границы интервалов, а также к какому из интервалов относится каждое из наблюдений.

Нас будут интересовать метрики и границы интервалов.

Подставим эти значения в функцию pd.cut().

Алгоритм Дженкса

Алгоритм естественных границ Дженкса (Jenks natural breaks optimization) делит данные на группы (кластеры) таким образом, чтобы минимизировать отклонение наблюдений от среднего каждого класса (дисперсию внутри классов) и максимизировать отклонение среднего каждого класса от среднего других классов (дисперсию между классами).

Этот алгоритм также можно использовать для определения границ интервалов. Установим библиотеку jenkspy⧉.

Найдем оптимальные границы. Количество интервалов (n_classes) нужно по-прежнему указывать вручную.

Подставим интервалы в функцию pd.cut().

Подведем итог

Сегодня мы рассмотрели базовые методы кодирования категориальных переменных, а также стратегии binning/bucketing.