Привет, Хабр! Меня зовут Иван Сивков, я наставник на курсе «Специалист по Data Science» в Яндекс Практикуме. В этой статье покажу, как построить пайплайн в библиотеке scikit-learn на базе встроенных инструментов и сократить количество кода при преобразовании данных. Эта статья рассчитана на новичков, которые только начинают изучать Data Science, но уже знают основные понятия.
В тексте упоминается scikit-learn — одна из самых популярных Python-библиотек для классического машинного обучения. Кроме большого числа алгоритмов машинного обучения, с помощью scikit-learn можно строить пайплайны. Это цепочки функций, через которые можно проводить данные. Пайплайны похожи на конвейеры, где каждое звено выступает в роли преобразователя (трансформера) или модели-предсказателя (обычно это последнее звено).
Важный этап обработки данных — их преобразование. Без пайплайна данные должны проходить через отдельные преобразователи: энкодеры (преобразуют категориальные признаки в числовые векторы), импьютеры (заполняют пропуски в данных) и скейлеры (приводят признаки к одному масштабу). Каждый инструмент по отдельности нужно натренировать на обучающей выборке, преобразовать её и отдельно сделать преобразование тестовой выборки. В результате получается много повторяющегося кода.
Пайплайн собирает все инструменты в один конвейер без повторяющегося кода. Достаточно обучить этот конвейер на обучающей выборке и использовать его для всех нужных преобразований одной командой. Он принимает на вход признаки, преобразует их и выдаёт результат.
Ниже соберём пайплайн из отдельных инструментов, обучим его на обучающей выборке данных и сделаем предсказание.
Подготовка
Первым шагом установим минимально нужную версию библиотеки — 1.1. Если выполняете код в Jupiter Hub Яндекс Практикума, то базовая версия sklearn — 0.24. Нужно установить более позднюю, чтобы избежать проблем с совместимостью.
!pip install scikit-learn==1.1
Затем импортируем pandas для загрузки данных, а также все инструменты sklearn, которые понадобятся в этом примере: скейлеры, энкодеры и импьютеры. В списке есть вспомогательные инструменты для разделения выборок, рассчитывания метрик и других операций.
import pandas as pdfrom sklearn.compose import ColumnTransformer
from sklearn.impute import KNNImputer, SimpleImputer
from sklearn.linear_model import Ridge
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.pipeline import Pipeline
from sklearn.metrics import mean_squared_error
Убедимся, что версия библиотеки не ниже требуемой. Показываю для примера — в реальной разработке этот шаг не нужен.
import sklearn
sklearn.__version__
-
Отключим предупреждения о будущем изменении названия аргумента sparse объекта OneHotEncoder на sparse_output, чтобы не мешало отображению вывода. Впрочем это предупреждение будет выводиться только, если используете версию scikit-learn 1.2 и выше. Также ограничим вывод датафреймов восемью столбцами.
В таком виде они будут влезать на один экран, и работать с ними будет удобнее.
import warnings
warnings.simplefilter(action='ignore', category=FutureWarning)
pd.set_option('display.max_columns', 8)
Для этого примера возьмём популярный бесплатный датасет с информацией о стоимости недвижимости в Калифорнии, который часто упоминают в обучающих статьях и книгах по Data Science. Скачаем его с сайта компании Hugging Face — разработчика библиотеки Transformers (содержит нейронные сети и другие инструменты для работы с текстовыми данными).
data = pd.read_csv('https://huggingface.co/datasets/leostelon/california-housing/raw/main/housing.csv')
data.info()
display(data.describe())
Следующим шагом выбираем целевой признак
medium_house_value
— это медианная цена за дом для округа. Численными признаками будут медианное значение возраста домов, общее количество комнат и спален в них, население, число домов, медианный доход на семью. А категориальный признак — положение относительно океана.
А затем разделяем датасет на рабочую и тестовую выборки:
target = data['median_house_value']
features = data.drop(['median_house_value'], axis=1)
features_train, features_test, target_train, target_test = train_test_split(features, target, test_size=0.2, random_state=44)
На этом подготовительная часть закончена — перейдём к созданию пайплайна.
Пример пайплайна
Для наглядности создадим два отдельных пайплайна — для численных и категориальных признаков, а затем соберем их в один.
Начнём с численных. Задача пайплайна
pipe_num
на этом этапе — заполнить пропуски и, если они есть, провести масштабирование признаков.
Для этого используем инструмент SimpleImputer
, заполним с его помощью пропуски медианным значением признака.
StandardScaler
стандартизирует данные — вычитает среднее и делит на среднее квадратичное обучающей выборки.
В последней строке задаём объект класса Pipeline. При этом передаём на вход список, каждый элемент в котором — кортеж из двух значений. Это трансформер и его название, которое мы задаём самостоятельно, чтобы в случае необходимости обратиться к нему.
simple_imputer = SimpleImputer(strategy='median')
std_scaler = StandardScaler()
pipe_num = Pipeline([('imputer', simple_imputer), ('scaler', std_scaler)])
Следующим шагом через
pipe_num
вызываемfit_transform
— происходит то же самое, как если бы мы отдельно применялиSimpleImputer
иStandardScaler
. Здесь используются все признаки, кроме категориального.
res_num = pipe_num.fit_transform(features_train.drop(['ocean_proximity'], axis=1))
Трансформатор выше возвращает данные в виде NumPy-массива, столбцы в котором не названы. Чтобы вернуть названия столбцов для удобства работы с данными, можно использовать метод get_feature_names_out
объекта пайплайна.
В данном случае названия можно взять и напрямую из датафрейма, так как этот пайплайн сохраняет исходный набор признаков. Но если после преобразования набор столбцов меняется, например с применением энкодера, такой способ не подойдёт. Использование get_feature_names_out
— более универсальный подход, поэтому будем использовать его для всех пайплайнов.
В конце для проверки выводим результат преобразования численных признаков:
res_df_num = pd.DataFrame(res_num,
columns=pipe_num['scaler'].get_feature_names_out(features.drop(['ocean_proximity'], axis=1).columns))
res_df_num
Это служебная строка, в которой мы проверяем, что в данных нет пропусков. В итоговый пайплайн она не попадёт и нужна исключительно для демонстрации.
res_df_num.info()
Теперь создадим пайплайн для категориальных признаков. С помощью
SimpleImputer
заменим пропуски на значение‘unknown’
, аOneHotEncoder
используем для кодирования категориальных признаков в числовые значения, понятные моделям. Аргументhandle_unknown=’ignore’
нужен для исключения ошибок в случаях, если при тестировании встречаются категории, которых не было в обучающей выборке. В конце создаём пайплайн из импьютера и энкодера и проверяем, что он сработал.
Хотя в этом датасете в категориальном признаке нет пропусков, они могут возникнуть, если при использовании модели на практике появятся новые данные. На этот случай нужно решение, которое позволит коду не упасть, — для этого и нужен импьютер.
s_imputer = SimpleImputer(strategy='constant', fill_value='unknown')
ohe_encoder = OneHotEncoder(handle_unknown='ignore', sparse=False)
pipe_cat = Pipeline([('imputer', s_imputer), ('encoder', ohe_encoder)])
res_cat = pipe_cat.fit_transform(features_train[['ocean_proximity']])
res_df_cat = pd.DataFrame(res_cat, columns=pipe_cat.get_feature_names_out())
res_df_cat
Теперь объединяем признаки в один пайплайн.
Здесь есть нюанс: поставить категориальные и численные признаки просто друг за другом не удастся, их надо обрабатывать по-разному. В scikit-learn есть инструмент ColumnTransformer
, который работает как компоновщик пайплайнов. С его помощью можно распараллелить пайплайн: для одной части столбцов может применяться один пайплайн, для второй — другой.
При задании объекта Column Transformer передаётся список кортежей. В каждом кортеже кроме названия и трансформера, как при создании объекта Pipeline, дополнительно указывается, для каких столбцов он применяется.
Стоит отметить, что кроме элементарных трансформеров (SimpleImputer, StandardScaler и др.) Column Transformer может принимать в качестве преобразователя и объекты Pipeline.
С помощью инструмента list comprehension
создаём списки численных и категориальных столбцов. В нашем примере датасет небольшой, и можно было бы задать списки вручную. Но на практике столбцов может быть намного больше, поэтому лучше автоматизировать процесс. В коде ниже проверяется тип столбца: если он object
, то считается как численный, если нет — как категориальный.
col_transformer = ColumnTransformer([('num_preproc', pipe_num, [x for x in features.columns if features[x].dtype!='object']),
('cat_preproc', pipe_cat, [x for x in features.columns if features[x].dtype=='object'])])
В этой строке возвращаем результат выполнения пайплайна:
res = col_transformer.fit_transform(features_train)
Преобразуем результат в датафрейм. При этом уберём дополнительную информацию в названиях столбцов — автоматически метод
get_feature_names_out
объектаColumnTransformer
добавляет название трансформера, с помощью которого были получены столбцы. Из-за этого на страницу влезает меньше столбцов, и пользоваться выводом становится неудобно.
res_df = pd.DataFrame(res, columns = [x.split('__')[-1] for x in col_transformer.get_feature_names_out()])
res_df
Собираем готовый пайплайн, компонуя его части с помощью Column Transformer. Он состоит из предобработки и модели
Ridge
из библиотеки scikit-learn — это линейная регрессия с регуляризацией. Это одна из простых моделей, которая предсказывает целевой признак на основе суммы других признаков, умноженных на коэффициенты.
model = Ridge()
final_pipe = Pipeline([('preproc', col_transformer),
('model', model)])
Обучаем финальный пайплайн на обучающей выборке и делаем предсказание.
final_pipe.fit(features_train, target_train)
preds = final_pipe.predict(features_test)
mean_squared_error(target_test, preds, squared=False)
Если объединить пайплайн целиком и исключить из него демонстрационные фрагменты кода, то он будет выглядеть так:
simple_imputer = SimpleImputer(strategy='median')
std_scaler = StandardScaler()
pipe_num = Pipeline([('imputer', simple_imputer), ('scaler', std_scaler)])
s_imputer = SimpleImputer(strategy='constant', fill_value='unknown')
ohe_encoder = OneHotEncoder(handle_unknown='ignore', sparse=False)
pipe_cat = Pipeline([('imputer', s_imputer), ('encoder', ohe_encoder)])
col_transformer = ColumnTransformer([('num_preproc', pipe_num, [x for x in features.columns if features[x].dtype!='object']),
('cat_preproc', pipe_cat, [x for x in features.columns if features[x].dtype=='object'])])
final_pipe = Pipeline([('preproc', col_transformer),
('model', model)])
final_pipe.fit(features_train, target_train)
preds = final_pipe.predict(features_test)
В итоге весь код для предобработки, обучения и предсказания занимает буквально полстраницы.
В следующих статьях изучим тонкости использования пайплайнов вместе с инструментами кросс-валидации scikit-learn и покажем, как делать более сложные кастомизированные конвейеры и использовать их в своих пет-проектах.
Научиться работать с датасетами, пайплайнами, Jupyter Notebook и многими другими инструментами, не упомянутыми в этой статье, можно на курсе «Специалист по Data Science» от Практикума. В зависимости от вашего расписания и привычного темпа работы, можно освоить программу за 5, 8 или 16 месяцев.
saege5b
О...
Учебник с костылингом и депрекатингом, для новичков :)
Ну, для "здесь и сейчас" сойдёт, но в будущем будут проблемы; - это я для тех, кто будет пробовать и будет получать массовые ошибки.
I_a_sivkov Автор
Спасибо за конструктивную критику и что обратили внимание на этот момент. В момент верстки вкралась относится. Depricate warning к scikit-learn, а не pandas относится. С версии 1.4 планируется убрать название аргумента sparse и оставить sparse_output (с версии 1.2 вводится).
Соглашусь, что эта статья именно для новичков. Цель познакомить читателя с инструментом Pipeline библиотеки sklearn, а не дать готовое решение "на все времена".