Как бороться с категориальными особенностями и распространенные ошибки, которых следует избегать

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

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

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

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

Найдите весь код вместе с пояснениями также как блокнот на jupyter.

Отображение неотъемлемой порядочности объекта в неправильном масштабе будет иметь очень негативное влияние на производительность модели (то есть пропорционально релевантности функции).

Как уже упоминалось, порядковые элементы имеют естественный порядок, или, другими словами, они имеют порядковую шкалу с рейтингом. Поэтому, если мы хотим сохранить ценность функций после ее кодирования, мы хотим убедиться, что она соответствует их ранжированию.

Визуализация с помощью дерева решений

Интуитивно понятный способ думать об этом - это то, как дерево решений устанавливает свои пороговые значения. Во время обучения дерево решений изучит оптимальные функции, которые нужно установить на каждом узле, а также оптимальный порог, который определит путь, по которому невидимые образцы будут проходить через каждый узел.

Если мы закодируем порядковый объект с помощью простого LabelEncoder, это может привести к тому, что функция скажет, что 1 представляет теплый, 2, что может быть переведено как горячий, и 0, представляющий кипение. В таком случае результатом будет дерево с излишне большим количеством разбиений и, следовательно, гораздо более сложной задачей для того, что должно быть проще моделировать.

Хотя на самом деле увидеть, почему это плохая идея, будет более интуитивно понятным, чем просто слова (на помощь приходят деревья решений :). Давайте использовать простой пример, чтобы проиллюстрировать вышеизложенное, состоящий из двух порядковых функций, содержащих диапазон с количеством часов, потраченных студентом на подготовку к экзамену, и средней оценкой всех предыдущих заданий, а также целевой переменной, указывающей, был ли экзамен сдан. или не. Я определил столбцы фрейма данных как pd.Categorical:

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

При этом для более общего подхода с fit / transform методами нам следует рассмотреть другие инструменты, которые я рассмотрю в следующем разделе.

Итак, фрейм данных, который мы будем использовать, выглядит следующим образом:

   Hours_of_dedication   Assignments_avg_grade  Result
0                20-25                       B    Pass
1                20-25                       C    Pass
2                 5-10                       F    Fail
3                 5-10                       C    Fail
4                40-45                       B    Pass
5                  0-5                       D    Fail
6                15-20                       C    Fail
7                20-25                       A    Pass
8                30-35                       B    Pass
9                 5-10                       B    Fail
10               10-15                       D    Fail
...

И, как уже упоминалось, мы можем получить присвоенные коды каждой категории в соответствии с их порядком в параметре categories, используя метод pd.Series.cat.codes:

X = df.apply(lambda x: x.cat.codes)
print(X)
    Hours_of_dedication  Assignments_avg_grade  Result
0                     4                      3       1
1                     4                      2       1
2                     1                      0       0
3                     1                      2       0
4                     7                      3       1
5                     0                      1       0
6                     3                      2       0
7                     4                      4       1
8                     6                      3       1
9                     1                      3       0
10                    2                      1       0
...

Теперь давайте подберем DecisionTreeClassifier и посмотрим, как дерево определило разбиения:

from sklearn import tree
dt = tree.DecisionTreeClassifier()
y = X.pop('Result')
dt.fit(X, y)

Модуль sklearn.tree предоставляет функцию для построения подобранного дерева решений. Давайте воспользуемся этим, чтобы увидеть, как выглядит дерево решений:

g = tree.plot_tree(dt, 
                   feature_names = X.columns,
                   class_names=['Failed', 'Pass'],
                   filled = True,
                   label='all',
                   rounded=True)

В том, что все?? Что ж… да! На самом деле я установил функции таким образом, что существует простая и очевидная связь между функцией Hours of dedication и тем, сдан экзамен или нет, что дает понять, что проблема должна быть очень легко решаемой. модель.

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

from sklearn.preprocessing import LabelEncoder
X_wrong = df.apply(LabelEncoder().fit_transform)
dt_wrong = tree.DecisionTreeClassifier()
dt_wrong.fit(X_wrong, y)
g = tree.plot_tree(dt_wrong, 
                   feature_names = X_wrong.columns,
                   class_names=['Fail', 'Pass'],
                   filled = True,
                   label='all',
                   rounded=True)

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

Это будет означать, что классификатор, вероятно, переоценивается, поскольку мы резко увеличиваем сложность. А обрезая дерево и настраивая необходимые параметры для предотвращения переобучения, мы тоже не решаем проблему, поскольку добавили слишком много шума из-за неправильного кодирования функций.

Как следует кодировать порядковые элементы?

В приведенном выше примере мы видели простой способ кодирования порядковых функций с помощью pd.Categorical. Но в большинстве случаев нас будет интересовать не только преобразование заданного набора функций, но и репликация примененных преобразований в невидимых данных.

Для этого у нас есть OrdinalEncoder от Категория Энкодеры. Этот класс позволяет нам:

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

Давайте посмотрим, как его использовать, на примере, содержащем несколько функций:

OrdinalEncoder получает параметр сопоставления, ожидая список словарей, каждый из которых содержит ключи col и mapping:

Затем мы можем создать новый экземпляр кодировщика с помощью:

import category_encoders as ce
encoder = ce.OrdinalEncoder(mapping = ordinal_cols_mapping, 
                             return_df = True)

Итак, преобразовав тот же фрейм данных, что и выше, мы получим:

df_train = encoder.fit_transform(df)
print(df_train)
    Outlook  Temperature  Humidity  Windy  PlayTennis
0         2            2         1      0           0
1         2            2         1      1           0
2         1            2         1      0           1
3         0            1         1      0           1
4         0            0         0      0           1
5         0            0         0      1           0
6         1            0         0      1           1
7         2            1         1      0           0
8         2            0         0      0           1
9         0            1         0      0           1
10        2            1         0      1           1
11        1            1         1      1           1
12        1            2         0      0           1
13        0            1         1      1           0

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

Найдите весь код вместе с пояснениями также как блокнот на jupyter. Надеюсь, вам понравилось, и вы нашли это полезным!