От Pandas до Scikit-Learn - новый увлекательный рабочий процесс

Новая интеграция Scikit-Learn с Pandas

Scikit-Learn сделает одно из самых больших обновлений за последние годы, выпустив гигантскую версию 0.20. Для многих специалистов по обработке данных типичный рабочий процесс состоит из использования Pandas для исследовательского анализа данных перед переходом к scikit-learn для машинного обучения. Этот новый выпуск сделает процесс более простым, многофункциональным, надежным и стандартизированным.

Дорожка сертификации Python Pandas

С 15 февраля 2021 года я предлагаю 8+ онлайн-курсов, охватывающих всю библиотеку pandas. В конце каждого курса вам нужно будет сдать сложные сертификационные экзамены, чтобы подтвердить свой опыт анализа данных с помощью Python и pandas. Узнайте больше о сертификационных курсах.

Резюме и цели этой статьи

  • Эта статья предназначена для тех, кто использует Scikit-Learn в качестве библиотеки машинного обучения, но полагается на Pandas в качестве инструмента исследования и подготовки данных.
  • Предполагается, что вы знакомы как с Scikit-Learn, так и с Pandas.
  • Мы исследуем новую оценку ColumnTransformer, которая позволяет нам применять отдельные преобразования к разным подмножествам ваших данных параллельно, прежде чем объединять результаты вместе.
  • Основной проблемой для пользователей (и, на мой взгляд, худшей частью Scikit-Learn) была подготовка pandas DataFrame со строковыми значениями в его столбцах. Этот процесс должен стать более стандартизированным.
  • Оценщик OneHotEncoder получил хорошее обновление для кодирования столбцов со строковыми значениями.
  • Чтобы помочь с одним горячим кодированием, мы используем новую оценку SimpleImputer для заполнения пропущенных значений константами.
  • Мы создадим настраиваемый оценщик, который выполняет все «базовые» преобразования в DataFrame, вместо того, чтобы полагаться на встроенные инструменты Scikit-Learn. Это также преобразует данные с помощью пары различных функций, которых нет в Scikit-Learn.
  • Наконец, мы исследуем группирование числовых столбцов с помощью нового оценщика KBinsDiscretizer.

Примечание перед тем, как мы начнем

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

Продолжаем…

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

Это привело к появлению множества руководств, в которых строковые столбцы обрабатываются по-своему. Некоторые решения включали обращение к функции Pandas get_dummies. Некоторые использовали Scikit-Learn’sLabelBinarizer, который выполняет быстрое кодирование, но был разработан для меток (целевой переменной), а не для ввода. Другие создали свои собственные оценщики. Даже целые пакеты, такие как sklearn-pandas, были созданы для поддержки этого проблемного места. Из-за отсутствия стандартизации те, кто хотел создавать модели машинного обучения со строковыми столбцами, причиняли боль.

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

Обновление до версии 0.20

conda update scikit-learn

или пункт:

pip install -U scikit-learn

Представляем ColumnTransformer и обновленный OneHotEncoder

С обновлением до версии 0.20 многие рабочие процессы от Pandas до Scikit-Learn должны стать похожими. ColumnTransformer estimator применяет преобразование к определенному подмножеству столбцов вашего Pandas DataFrame (или массива).

Оценщик OneHotEncoder не нов, но был обновлен для кодирования строковых столбцов. Раньше он кодировал только столбцы, содержащие числовые категориальные данные.

Давайте посмотрим, как эти новые дополнения работают для обработки строковых столбцов в Pandas DataFrame.

Набор данных Kaggle Housing

Одним из первых конкурсов машинного обучения Kaggle является Цены на жилье: продвинутые методы регрессии. Цель состоит в том, чтобы спрогнозировать цены на жилье с учетом около 80 характеристик. Существует сочетание непрерывных и категориальных столбцов. Вы можете скачать данные с сайта или использовать их инструмент командной строки (что очень приятно).

Проверить данные

Давайте прочитаем в нашем DataFrame и выведем первые несколько строк.

>>> import pandas as pd
>>> import numpy as np
>>> train = pd.read_csv(‘data/housing/train.csv’)
>>> train.head()

>>> train.shape
(1460, 81)

Удалите целевую переменную из обучающего набора

Целевая переменная - SalePrice, которую мы удаляем и присваиваем как массив ее собственной переменной. Мы будем использовать его позже, когда будем заниматься машинным обучением.

>>> y = train.pop('SalePrice').values

Кодирование однострочного столбца

Для начала давайте закодируем столбец с одной строкой HouseStyle, который содержит значения для внешнего вида дома. Давайте выведем уникальное количество каждого строкового значения.

>>> vc = train['HouseStyle'].value_counts()
>>> vc
1Story    726
2Story    445
1.5Fin    154
SLvl       65
SFoyer     37
1.5Unf     14
2.5Unf     11
2.5Fin      8
Name: HouseStyle, dtype: int64

В этом столбце 8 уникальных значений.

Scikit-Learn Gotcha - нужны 2D-данные

Большинство оценщиков Scikit-Learn требуют, чтобы данные были строго двумерными. Если мы выберем столбец выше как train['HouseStyle'], технически создается серия Pandas, которая представляет собой одно измерение данных. Мы можем заставить Pandas создать DataFrame с одним столбцом, передав список с одним элементом в скобки следующим образом:

>>> hs_train = train[['HouseStyle']].copy()
>>> hs_train.ndim
2

Осваивайте машинное обучение с помощью Python

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

Импорт, создание экземпляра, подгонка - трехэтапный процесс для каждого оценщика.

Scikit-Learn API согласован для всех оценщиков и использует трехэтапный процесс для подгонки (обучения) данных.

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

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

>>> from sklearn.preprocessing import OneHotEncoder
>>> ohe = OneHotEncoder(sparse=False)
>>> hs_train_transformed = ohe.fit_transform(hs_train)
>>> hs_train_transformed
array([[0., 0., 0., ..., 1., 0., 0.],
       [0., 0., 1., ..., 0., 0., 0.],
       [0., 0., 0., ..., 1., 0., 0.],
       ...,
       [0., 0., 0., ..., 1., 0., 0.],
       [0., 0., 1., ..., 0., 0., 0.],
       [0., 0., 1., ..., 0., 0., 0.]])

Как и ожидалось, он закодировал каждое уникальное значение как отдельный двоичный столбец.

>>> hs_train_transformed.shape
(1460, 8)

Если вам нравится эта статья, подумайте о покупке All Access Pass! который включает в себя все мои текущие и будущие материалы по одной низкой цене.

У нас есть массив NumPy. Где названия столбцов?

Обратите внимание, что наш вывод - это массив NumPy, а не DataFrame Pandas. Scikit-Learn изначально не создавался для прямой интеграции с Pandas. Все объекты Pandas преобразуются в массивы NumPy внутренне, а массивы NumPy всегда возвращаются после преобразования.

Мы все еще можем получить имя столбца из объекта OneHotEncoder с помощью его метода get_feature_names.

>>> feature_names = ohe.get_feature_names()
>>> feature_names
array(['x0_1.5Fin', 'x0_1.5Unf', 'x0_1Story', 'x0_2.5Fin', 
       'x0_2.5Unf', 'x0_2Story', 'x0_SFoyer', 'x0_SLvl'],  dtype=object)

Проверка правильности нашей первой строки данных

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

>>> row0 = hs_train_transformed[0]
>>> row0
array([0., 0., 0., 0., 0., 1., 0., 0.])

Это кодирует 6-е значение в массиве как 1. Давайте воспользуемся логическим индексированием, чтобы раскрыть имя функции.

>>> feature_names[row0 == 1]
array(['x0_2Story'], dtype=object)

Теперь давайте проверим, что первое значение в нашем исходном столбце DataFrame совпадает.

>>> hs_train.values[0]
array(['2Story'], dtype=object)

Используйте inverse_transform, чтобы автоматизировать это

Как и большинство объектов-трансформеров, существует inverse_transform метод, который вернет вам исходные данные. Здесь мы должны заключить row0 в список, чтобы сделать его двумерным массивом.

>>> ohe.inverse_transform([row0])
array([['2Story']], dtype=object)

Мы можем проверить все значения, инвертируя весь преобразованный массив.

>>> hs_inv = ohe.inverse_transform(hs_train_transformed)
>>> hs_inv
array([['2Story'],
       ['1Story'],
       ['2Story'],
       ...,
       ['2Story'],
       ['1Story'],
       ['1Story']], dtype=object)
>>> np.array_equal(hs_inv, hs_train.values)
True

Применение преобразования к тестовой выборке

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

>>> test = pd.read_csv('data/housing/test.csv')
>>> hs_test = test[['HouseStyle']].copy()
>>> hs_test_transformed = ohe.transform(hs_test)
>>> hs_test_transformed
array([[0., 0., 1., ..., 0., 0., 0.],
       [0., 0., 1., ..., 0., 0., 0.],
       [0., 0., 0., ..., 1., 0., 0.],
       ...,
       [0., 0., 1., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 1., 0.],
       [0., 0., 0., ..., 1., 0., 0.]])

У нас снова должно получиться 8 столбцов, и мы это делаем.

>>> hs_test_transformed.shape
(1459, 8)

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

Область неисправности №1 - Категории, уникальные для набора тестов

Что произойдет, если у нас есть дом с домашним стилем, который уникален только для тестовой группы? Скажите что-нибудь вроде 3Story. Давайте изменим первое значение стилей домов и посмотрим, какое значение по умолчанию используется в Scikit-Learn.

>>> hs_test = test[['HouseStyle']].copy()
>>> hs_test.iloc[0, 0] = '3Story'
>>> hs_test.head(3)
HouseStyle
0     3Story
1     1Story
2     2Story
>>> ohe.transform(hs_test)
ValueError: Found unknown categories ['3Story'] in column 0 during transform

Ошибка: неизвестная категория

По умолчанию наш кодировщик выдаст ошибку. Вероятно, это то, что мы хотим, поскольку нам нужно знать, есть ли в тестовом наборе уникальные строки. Если у вас действительно есть эта проблема, то может быть что-то более глубокое, что требует исследования. На данный момент мы проигнорируем проблему и закодируем эту строку как все 0, установив для параметра handle_unknown значение «игнорировать» при создании экземпляра.

>>> ohe = OneHotEncoder(sparse=False, handle_unknown='ignore')
>>> ohe.fit(hs_train)
>>> hs_test_transformed = ohe.transform(hs_test)
>>> hs_test_transformed
array([[0., 0., 0., ..., 0., 0., 0.],
       [0., 0., 1., ..., 0., 0., 0.],
       [0., 0., 0., ..., 1., 0., 0.],
       ...,
       [0., 0., 1., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 1., 0.],
       [0., 0., 0., ..., 1., 0., 0.]])

Убедимся, что в первой строке все нули.

>>> hs_test_transformed[0]
array([0., 0., 0., 0., 0., 0., 0., 0.])

Область неисправности # 2 - Отсутствующие значения в тестовом наборе

Если в вашем тестовом наборе отсутствуют значения (NaN или None), они будут игнорироваться, пока для handle_unknown установлено значение «игнорировать». Давайте добавим некоторые недостающие значения в первые пару элементов нашего тестового набора.

>>> hs_test = test[['HouseStyle']].copy()
>>> hs_test.iloc[0, 0] = np.nan
>>> hs_test.iloc[1, 0] = None
>>> hs_test.head(4)
HouseStyle
0        NaN
1       None
2     2Story
3     2Story
>>> hs_test_transformed = ohe.transform(hs_test)
>>> hs_test_transformed[:4]
array([[0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 1., 0., 0.],
       [0., 0., 0., 0., 0., 1., 0., 0.]])

Область проблемы № 3 - Отсутствующие значения в обучающем наборе

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

>>> hs_train = hs_train.copy()
>>> hs_train.iloc[0, 0] = np.nan
>>> ohe = OneHotEncoder(sparse=False, handle_unknown='ignore')
>>> ohe.fit_transform(hs_train)
TypeError: '<' not supported between instances of 'str' and 'float'

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

Необходимо вменять отсутствующие значения

На данный момент мы должны вменять недостающие значения. Старый Imputer из модуля предварительной обработки устарел. Вместо него был сформирован новый модуль impute с новым оценщиком SimpleImputer и новой стратегией «константа». По умолчанию при использовании этой стратегии пропущенные значения заполняются строкой «missing_value». Мы можем выбрать, что установить, с помощью параметра fill_value.

>>> hs_train = train[['HouseStyle']].copy()
>>> hs_train.iloc[0, 0] = np.nan
>>> from sklearn.impute import SimpleImputer
>>> si = SimpleImputer(strategy='constant', fill_value='MISSING')
>>> hs_train_imputed = si.fit_transform(hs_train)
>>> hs_train_imputed
array([['MISSING'],
       ['1Story'],
       ['2Story'],
       ...,
       ['2Story'],
       ['1Story'],
       ['1Story']], dtype=object)

Отсюда мы можем кодировать, как делали раньше.

>>> hs_train_transformed = ohe.fit_transform(hs_train_imputed)
>>> hs_train_transformed
array([[0., 0., 0., ..., 1., 0., 0.],
       [0., 0., 1., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.],
       ...,
       [0., 0., 0., ..., 0., 0., 0.],
       [0., 0., 1., ..., 0., 0., 0.],
       [0., 0., 1., ..., 0., 0., 0.]])

Обратите внимание, что теперь у нас есть дополнительный столбец и имя дополнительной функции.

>>> hs_train_transformed.shape
(1460, 9)
>>> ohe.get_feature_names()
array(['x0_1.5Fin', 'x0_1.5Unf', 'x0_1Story', 'x0_2.5Fin',
       'x0_2.5Unf', 'x0_2Story', 'x0_MISSING', 'x0_SFoyer', 
       'x0_SLvl'], dtype=object)

Подробнее о fit_transform

Для всех оценщиков метод fit_transform сначала вызовет метод fit, а затем метод transform. Метод fit находит ключевые свойства, которые будут использоваться во время преобразования. Например, с SimpleImputer, если бы стратегия была «средней», тогда она находила бы среднее значение для каждого столбца во время fit метода. Это среднее значение будет сохранено для каждого столбца. Когда вызывается transform, он использует это сохраненное среднее значение каждого столбца для заполнения отсутствующих значений и возвращает преобразованный массив.

OneHotEncoder работает аналогично. В методе fit он находит все уникальные значения для каждого столбца и снова сохраняет их. Когда вызывается transform, он использует эти сохраненные уникальные значения для создания двоичного массива.

Примените оба преобразования к набору тестов

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

>>> hs_test = test[['HouseStyle']].copy()
>>> hs_test.iloc[0, 0] = 'unique value to test set'
>>> hs_test.iloc[1, 0] = np.nan
>>> hs_test_imputed = si.transform(hs_test)
>>> hs_test_transformed = ohe.transform(hs_test_imputed)
>>> hs_test_transformed.shape
(1459, 8)
>>> ohe.get_feature_names()
array(['x0_1.5Fin', 'x0_1.5Unf', 'x0_1Story', 'x0_2.5Fin', 
       'x0_2.5Unf', 'x0_2Story', 'x0_SFoyer', 'x0_SLvl'], 
       dtype=object)

Вместо этого используйте Pipeline

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

>>> from sklearn.pipeline import Pipeline

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

>>> si_step = ('si', SimpleImputer(strategy='constant',
                fill_value='MISSING'))
>>> ohe_step = ('ohe', OneHotEncoder(sparse=False,
                handle_unknown='ignore'))
>>> steps = [si_step, ohe_step]
>>> pipe = Pipeline(steps)
>>> hs_train = train[['HouseStyle']].copy()
>>> hs_train.iloc[0, 0] = np.nan
>>> hs_transformed = pipe.fit_transform(hs_train)
>>> hs_transformed.shape
(1460, 9)

Набор тестов легко трансформируется на каждом этапе конвейера, просто передав его методу transform.

>>> hs_test = test[['HouseStyle']].copy()
>>> hs_test_transformed = pipe.transform(hs_test)
>>> hs_test_transformed.shape
(1459, 9)

Почему только метод преобразования для набора тестов?

При преобразовании набора тестов важно просто вызывать метод transform, а не fit_transform. Когда мы запустили fit_transform на обучающем наборе, Scikit-Learn обнаружил всю необходимую информацию, необходимую для преобразования любого другого набора данных, содержащего те же имена столбцов.

Преобразование нескольких строковых столбцов

Кодирование нескольких строковых столбцов не проблема. Выберите нужные столбцы, а затем снова передайте новый DataFrame через тот же конвейер.

>>> string_cols = ['RoofMatl', 'HouseStyle']
>>> string_train = train[string_cols]
>>> string_train.head(3)
RoofMatl HouseStyle
0  CompShg     2Story
1  CompShg     1Story
2  CompShg     2Story
>>> string_train_transformed = pipe.fit_transform(string_train)
>>> string_train_transformed.shape
(1460, 16)

Получите отдельные части конвейера

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

>>> ohe = pipe.named_steps['ohe']
>>> ohe.get_feature_names()
array(['x0_ClyTile', 'x0_CompShg', 'x0_Membran', 'x0_Metal', 
       'x0_Roll', 'x0_Tar&Grv', 'x0_WdShake', 'x0_WdShngl',
       'x1_1.5Fin', 'x1_1.5Unf', 'x1_1Story', 'x1_2.5Fin', 
       'x1_2.5Unf', 'x1_2Story', 'x1_SFoyer', 'x1_SLvl'], 
        dtype=object)

Используйте новый ColumnTransformer для выбора столбцов

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

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

ColumnTransformer принимает список кортежей из трех элементов. Первое значение в кортеже - это имя, которое помечает его, второе - это созданный экземпляр оценщика, а третье - это список столбцов, которые вы хотите применить преобразование к. Кортеж будет выглядеть так:

('name', SomeTransformer(parameters), columns)

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

Вы также можете использовать массивы NumPy с ColumnTransformer, но это руководство сосредоточено на интеграции Pandas, поэтому мы будем придерживаться только использования DataFrames.

Магистр Python, Data Science и машинного обучения

Погрузитесь в мой комплексный путь к овладению наукой о данных и машинным обучением с помощью Python. Купите All Access Pass, чтобы получить пожизненный доступ ко всем текущим и будущим курсам. Некоторые из курсов, которые он содержит:

Получите All Access Pass прямо сейчас!

Передайте Pipeline ColumnTransformer

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

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

>>> from sklearn.compose import ColumnTransformer
>>> cat_si_step = ('si', SimpleImputer(strategy='constant',
                   fill_value='MISSING'))
>>> cat_ohe_step = ('ohe', OneHotEncoder(sparse=False,
                    handle_unknown='ignore'))
>>> cat_steps = [cat_si_step, cat_ohe_step]
>>> cat_pipe = Pipeline(cat_steps)
>>> cat_cols = ['RoofMatl', 'HouseStyle']
>>> cat_transformers = [('cat', cat_pipe, cat_cols)]
>>> ct = ColumnTransformer(transformers=cat_transformers)

Передайте весь DataFrame в ColumnTransformer

Экземпляр ColumnTransformer выбирает столбцы, которые мы хотим использовать, поэтому мы просто передаем весь DataFrame методу fit_transform. Для нас будут выбраны нужные столбцы.

>>> X_cat_transformed = ct.fit_transform(train)
>>> X_cat_transformed.shape
(1460, 16)

Теперь мы можем таким же образом преобразовать наш тестовый набор.

>>> X_cat_transformed_test = ct.transform(test)
>>> X_cat_transformed_test.shape
(1459, 16)

Получение названий функций

Нам нужно немного покопаться, чтобы получить названия функций. Все преобразователи хранятся в атрибуте словаря named_transformers_. Затем мы используем имена, первый элемент из трех элементов кортежа, чтобы выбрать конкретный преобразователь. Ниже мы выбираем наш трансформатор (здесь только один - конвейер с именем «cat»).

>>> pl = ct.named_transformers_['cat']

Затем из этого конвейера мы выбираем объект one-hot encoder и, наконец, получаем имена функций.

>>> ohe = pl.named_steps['ohe']
>>> ohe.get_feature_names()
array(['x0_ClyTile', 'x0_CompShg', 'x0_Membran', 'x0_Metal', 
       'x0_Roll','x0_Tar&Grv', 'x0_WdShake', 'x0_WdShngl', 
       'x1_1.5Fin', 'x1_1.5Unf', 'x1_1Story', 'x1_2.5Fin', 
       'x1_2.5Unf', 'x1_2Story', 'x1_SFoyer', 'x1_SLvl'], 
        dtype=object)

Преобразование числовых столбцов

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

Использование всех числовых столбцов

Вместо того, чтобы выбирать вручную только один или два столбца, как мы делали выше со строковыми столбцами, мы можем выбрать все числовые столбцы. Мы делаем это, сначала определяя тип данных каждого столбца с атрибутом dtypes, а затем проверяя, соответствует ли kind каждого dtype 'O'. Атрибут dtypes возвращает серию объектов NumPy dtype. Каждый из них имеет атрибут kind, который представляет собой отдельный символ. Мы можем использовать это, чтобы найти числовые или строковые столбцы. Pandas хранит все свои строковые столбцы как object, которые имеют вид, равный O. См. Документы NumPy для получения дополнительной информации об атрибуте kind.

>>> train.dtypes.head()
Id               int64
MSSubClass       int64
MSZoning        object
LotFrontage    float64
LotArea          int64
dtype: object

Получите типы, односимвольную строку, представляющую dtype.

>>> kinds = np.array([dt.kind for dt in train.dtypes])
>>> kinds[:5]
array(['i', 'i', 'O', 'f', 'i'], dtype='<U1')

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

>>> all_columns = train.columns.values
>>> is_num = kinds != 'O'
>>> num_cols = all_columns[is_num]
>>> num_cols[:5]
array(['Id', 'MSSubClass', 'LotFrontage', 'LotArea', 'OverallQual'],
      dtype=object)
>>> cat_cols = all_columns[~is_num]
>>> cat_cols[:5]
array(['MSZoning', 'Street', 'Alley', 'LotShape', 'LandContour'],
      dtype=object)

Получив числовые имена столбцов, мы снова можем использовать ColumnTransformer.

>>> from sklearn.preprocessing import StandardScaler
>>> num_si_step = ('si', SimpleImputer(strategy='median'))
>>> num_ss_step = ('ss', StandardScaler())
>>> num_steps = [num_si_step, num_ss_step]
>>> num_pipe = Pipeline(num_steps)
>>> num_transformers = [('num', num_pipe, num_cols)]
>>> ct = ColumnTransformer(transformers=num_transformers)
>>> X_num_transformed = ct.fit_transform(train)
>>> X_num_transformed.shape
(1460, 37)

Комбинирование категориальных и числовых преобразований столбцов

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

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

>>> transformers = [('cat', cat_pipe, cat_cols),
                    ('num', num_pipe, num_cols)]
>>> ct = ColumnTransformer(transformers=transformers)
>>> X = ct.fit_transform(train)
>>> X.shape
(1460, 305)

Машинное обучение

Весь смысл этого упражнения - настроить наши данные так, чтобы мы могли выполнять машинное обучение. Мы можем создать один финальный конвейер и добавить модель машинного обучения в качестве окончательной оценки. Первым шагом в конвейере будет вся трансформация, которую мы только что сделали выше. Мы присвоили y еще в начале руководства как SalePrice. Здесь мы просто будем использовать метод fit вместо fit_transform, поскольку наш последний шаг - это модель машинного обучения и не выполняет никаких преобразований.

>>> from sklearn.linear_model import Ridge
>>> ml_pipe = Pipeline([('transform', ct), ('ridge', Ridge())])
>>> ml_pipe.fit(train, y)

Мы можем оценить нашу модель с помощью метода score, который возвращает значение R-квадрата:

>>> ml_pipe.score(train, y)
0.92205

Перекрестная проверка

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

>>> from sklearn.model_selection import KFold, cross_val_score
>>> kf = KFold(n_splits=5, shuffle=True, random_state=123)
>>> cross_val_score(ml_pipe, train, y, cv=kf).mean()
0.813

Выбор параметров при поиске по сетке

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

>>> from sklearn.model_selection import GridSearchCV
>>> param_grid = {
    'transform__num__si__strategy': ['mean', 'median'],
    'ridge__alpha': [.001, 0.1, 1.0, 5, 10, 50, 100, 1000],
    }
>>> gs = GridSearchCV(ml_pipe, param_grid, cv=kf)
>>> gs.fit(train, y)
>>> gs.best_params_
{'ridge__alpha': 10, 'transform__num__si__strategy': 'median'}
>>> gs.best_score_
0.819

Получение всех результатов поиска по сетке в Pandas DataFrame

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

>>> pd.DataFrame(gs.cv_results_)

Создание нестандартного трансформатора, который делает все необходимое

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

Низкочастотные струны

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

Написание собственного класса оценщика

Scikit-Learn предоставляет некоторую помощь в рамках своей документации по написанию собственного класса оценщика. Класс BaseEstimator в модуле base предоставляет вам методы get_params и set_params. Метод set_params необходим при поиске по сетке. Вы можете написать свой собственный или унаследовать от BaseEstimator. Также есть TransformerMixin, но он просто пишет за вас fit_transform метод. Мы делаем это в одной строке кода ниже, поэтому мы не наследуем от него.

Следующий класс BasicTransformer выполняет следующие действия:

  • Заполняет отсутствующие значения средним или медианным значением для числовых столбцов.
  • Стандартизирует все числовые столбцы
  • Использует одну горячую кодировку для строковых столбцов
  • Не заполняет пропущенные значения для категориальных столбцов. Вместо этого он кодирует их как 0
  • Игнорирует уникальные значения в строковых столбцах в тестовом наборе
  • Позволяет выбрать порог количества вхождений значения в строковом столбце. Строки ниже этого порога будут закодированы как все нули.
  • Он работает только с DataFrames и является экспериментальным и не тестировался, поэтому он не работает для некоторых наборов данных.
  • Он называется «базовым», потому что это, вероятно, самые базовые преобразования, которые обычно выполняются со многими наборами данных.
from sklearn.base import BaseEstimator
class BasicTransformer(BaseEstimator):
    
    def __init__(self, cat_threshold=None, num_strategy='median',
                 return_df=False):
        # store parameters as public attributes
        self.cat_threshold = cat_threshold
        
        if num_strategy not in ['mean', 'median']:
            raise ValueError('num_strategy must be either "mean" or 
                              "median"')
        self.num_strategy = num_strategy
        self.return_df = return_df
        
    def fit(self, X, y=None):
        # Assumes X is a DataFrame
        self._columns = X.columns.values
        
        # Split data into categorical and numeric
        self._dtypes = X.dtypes.values
        self._kinds = np.array([dt.kind for dt in X.dtypes])
        self._column_dtypes = {}
        is_cat = self._kinds == 'O'
        self._column_dtypes['cat'] = self._columns[is_cat]
        self._column_dtypes['num'] = self._columns[~is_cat]
        self._feature_names = self._column_dtypes['num']
        
        # Create a dictionary mapping categorical column to unique 
        # values above threshold
        self._cat_cols = {}
        for col in self._column_dtypes['cat']:
            vc = X[col].value_counts()
            if self.cat_threshold is not None:
                vc = vc[vc > self.cat_threshold]
            vals = vc.index.values
            self._cat_cols[col] = vals
            self._feature_names = np.append(self._feature_names, col 
                                            + '_' + vals)
            
        # get total number of new categorical columns    
        self._total_cat_cols = sum([len(v) for col, v in 
                                    self._cat_cols.items()])
        
        # get mean or median
        num_cols = self._column_dtypes['num']
        self._num_fill = X[num_cols].agg(self.num_strategy)
        return self
        
    def transform(self, X):
        # check that we have a DataFrame with same column names as 
        # the one we fit
        if set(self._columns) != set(X.columns):
            raise ValueError('Passed DataFrame has different columns 
                              than fit DataFrame')
        elif len(self._columns) != len(X.columns):
            raise ValueError('Passed DataFrame has different number 
                              of columns than fit DataFrame')
            
        # fill missing values
        num_cols = self._column_dtypes['num']
        X_num = X[num_cols].fillna(self._num_fill)
        
        # Standardize numerics
        std = X_num.std()
        X_num = (X_num - X_num.mean()) / std
        zero_std = np.where(std == 0)[0]
        
        # If there is 0 standard deviation, then all values are the 
        # same. Set them to 0.
        if len(zero_std) > 0:
            X_num.iloc[:, zero_std] = 0
        X_num = X_num.values
        
        # create separate array for new encoded categoricals
        X_cat = np.empty((len(X), self._total_cat_cols), 
                         dtype='int')
        i = 0
        for col in self._column_dtypes['cat']:
            vals = self._cat_cols[col]
            for val in vals:
                X_cat[:, i] = X[col] == val
                i += 1
                
        # concatenate transformed numeric and categorical arrays
        data = np.column_stack((X_num, X_cat))
        
        # return either a DataFrame or an array
        if self.return_df:
            return pd.DataFrame(data=data, 
                                columns=self._feature_names)
        else:
            return data
    
    def fit_transform(self, X, y=None):
        return self.fit(X).transform(X)
    
    def get_feature_names():
        return self._feature_names

Используя наш BasicTransformer

Наш оценщик BasicTransformer можно использовать так же, как и любой другой оценщик scikit-learn. Мы можем создать его экземпляр, а затем преобразовать наши данные.

>>> bt = BasicTransformer(cat_threshold=3, return_df=True)
>>> train_transformed = bt.fit_transform(train)
>>> train_transformed.head(3)

Использование нашего трансформатора в трубопроводе

Наш трансформатор может быть частью трубопровода.

>>> basic_pipe = Pipeline([('bt', bt), ('ridge', Ridge())])
>>> basic_pipe.fit(train, y)
>>> basic_pipe.score(train, y)
0.904

Мы также можем выполнить перекрестную проверку с его помощью и получить такой же балл, как и с нашим конвейером преобразования столбцов scikit-learn, приведенным выше.

>>> cross_val_score(basic_pipe, train, y, cv=kf).mean()
0.816

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

>>> param_grid = {
        'bt__cat_threshold': [0, 1, 2, 3, 5],
        'ridge__alpha': [.1, 1, 10, 100]
    }
>>> gs = GridSearchCV(p, param_grid, cv=kf)
>>> gs.fit(train, y)
>>> gs.best_params_
{'bt__cat_threshold': 0, 'ridge__alpha': 10}
>>> gs.best_score_
0.830

Группирование и кодирование числовых столбцов с помощью нового KBinsDiscretizer

Есть несколько столбцов, которые содержат годы. Имеет смысл объединить значения в эти столбцы и рассматривать их как категории. Scikit-Learn представила новый оценщик KBinsDiscretizer, который делает именно это. Он не только объединяет значения, но и кодирует их. Раньше вы могли делать это вручную с помощью функций Pandas cut или qcut.

Давайте посмотрим, как это работает, только с YearBuilt столбцом.

>>> from sklearn.preprocessing import KBinsDiscretizer
>>> kbd = KBinsDiscretizer(encode='onehot-dense')
>>> year_built_transformed = kbd.fit_transform(train[['YearBuilt']])
>>> year_built_transformed
array([[0., 0., 0., 0., 1.],
       [0., 0., 1., 0., 0.],
       [0., 0., 0., 1., 0.],
       ...,
       [1., 0., 0., 0., 0.],
       [0., 1., 0., 0., 0.],
       [0., 0., 1., 0., 0.]])

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

>>> year_built_transformed.sum(axis=0)
array([292., 274., 307., 266., 321.])

Это «квантильная» стратегия. Вы можете выбрать «равномерное», чтобы края бункера располагались на одинаковом расстоянии, или «kmeans», который использует кластеризацию K-средних для поиска краев бункера.

>>> kbd.bin_edges_
array([array([1872. , 1947.8, 1965. , 1984. , 2003. , 2010. ])],
      dtype=object)

Обработка всех столбцов года отдельно с помощью ColumnTransformer

Теперь у нас есть еще одно подмножество столбцов, которые требуют отдельной обработки, и мы можем сделать это с помощью ColumnTransformer. Следующий код добавляет еще один шаг к нашему предыдущему преобразованию. Мы также отбрасываем столбец Id, который просто определял каждую строку.

>>> year_cols = ['YearBuilt', 'YearRemodAdd', 'GarageYrBlt', 
                 'YrSold']
>>> not_year = ~np.isin(num_cols, year_cols + ['Id'])
>>> num_cols2 = num_cols[not_year]
>>> year_si_step = ('si', SimpleImputer(strategy='median'))
>>> year_kbd_step = ('kbd', KBinsDiscretizer(n_bins=5, 
                     encode='onehot-dense'))
>>> year_steps = [year_si_step, year_kbd_step]
>>> year_pipe = Pipeline(year_steps)
>>> transformers = [('cat', cat_pipe, cat_cols),
                    ('num', num_pipe, num_cols2),
                    ('year', year_pipe, year_cols)]
>>> ct = ColumnTransformer(transformers=transformers)
>>> X = ct.fit_transform(train)
>>> X.shape
(1460, 320)

Мы проводим перекрестную проверку и оценку и видим, что вся эта работа не принесла нам улучшений.

>>> ml_pipe = Pipeline([('transform', ct), ('ridge', Ridge())])
>>> cross_val_score(ml_pipe, train, y, cv=kf).mean()
0.813

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

Больше вкусностей в Scikit-Learn 0.20

В предстоящем выпуске появятся и другие новые функции. Дополнительную информацию можно найти в разделе документации Что нового. Есть масса изменений.

Заключение

В этой статье представлен новый рабочий процесс, который будет доступен пользователям Scikit-Learn, которые полагаются на Pandas для первоначального исследования и подготовки данных. Гораздо более плавный и многофункциональный процесс взятия Pandas DataFrame и его преобразования, чтобы он был готов для машинного обучения, теперь выполняется с помощью новых и улучшенных оценокColumnTransformer, SimpleImputer, OneHotEncoder и KBinsDiscretizer.

Я очень рад видеть это новое обновление, и я собираюсь немедленно интегрировать эти новые рабочие процессы в свои проекты и учебные материалы.

Магистр Python, Data Science и машинного обучения

Погрузитесь в мой комплексный путь к овладению наукой о данных и машинным обучением с помощью Python. Купите All Access Pass, чтобы получить пожизненный доступ ко всем текущим и будущим курсам. Некоторые из курсов, которые он содержит:

Получите All Access Pass прямо сейчас!