От 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 согласован для всех оценщиков и использует трехэтапный процесс для подгонки (обучения) данных.
- Импортируйте оценщик, который нам нужен, из модуля, в котором он находится.
- Создайте экземпляр оценщика, возможно, изменив его значения по умолчанию.
- Подгоните оценщик к данным. При необходимости можно преобразовать данные в новое пространство.
Ниже мы импортируем 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, чтобы получить пожизненный доступ ко всем текущим и будущим курсам. Некоторые из курсов, которые он содержит:
- Exercise Python - всестороннее введение в Python (200+ страниц, 100+ упражнений)
- Анализ основных данных с помощью Python - наиболее полный курс, доступный для изучения панд. (800+ страниц и 300+ упражнений)
- Осваивайте машинное обучение с помощью Python - глубокое погружение в машинное обучение с постоянно обновляемым scikit-learn, демонстрирующим новейшие и лучшие инструменты. (300+ страниц)
Получите 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, чтобы получить пожизненный доступ ко всем текущим и будущим курсам. Некоторые из курсов, которые он содержит:
- Exercise Python - всестороннее введение в Python (200+ страниц, 100+ упражнений)
- Анализ основных данных с помощью Python - самый полный курс, доступный для изучения панд. (800+ страниц и 300+ упражнений)
- Осваивайте машинное обучение с помощью Python - глубокое погружение в машинное обучение с постоянно обновляемым scikit-learn, демонстрирующим новейшие и лучшие инструменты. (300+ страниц)