Забудьте о train_test_split: Pipeline, ColumnTransformer, FeatureUnion и FunctionTransformer незаменимы, даже если вы используете XGBoost или LGBM.

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

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

И я говорю не только о том старом каштане: train_test_split. Если вы готовы копнуть немного глубже, вы найдете сокровищницу полезных инструментов для более продвинутых методов подготовки данных, все из которых полностью совместимы с использованием других библиотек, таких как lightgbm, xgboost и catboost, для последующего моделирования.

В этой статье я пройдусь по четырем курсам scikit-learn, которые значительно ускорят мои рабочие процессы подготовки данных в моей повседневной работе в качестве Data Scientist.

1. Конвейер: бесшовное объединение этапов предварительной обработки

Класс Pipeline от Scikit-learn позволяет комбинировать различные препроцессоры или модели в единый вызываемый фрагмент кода:

Трубопроводы могут состоять из двух разных вещей:

  • Трансформатор: любой объект с методами fit() и transform(). Вы можете думать о преобразователе как об объекте, который используется для обработки ваших данных, и у вас обычно будет несколько преобразователей в рабочем процессе подготовки данных. Например, вы можете использовать один преобразователь для вменения пропущенных значений, а другой — для масштабирования функций или горячего кодирования ваших категориальных переменных. MinMaxScaler(), SimpleImputer() и OneHotEncoder() — все это примеры трансформеров.
  • Оценщик. На жаргоне scikit-learn «оценщик» обычно означает модель машинного обучения; то есть объект с методами fit() и predict(). LinearRegression() и RandomForestClassifier() являются примерами оценок.

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

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

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

Создание пайплайна с помощью scikit-learn удивительно просто.

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

import pandas as pd
from sklearn.datasets import load_diabetes
from sklearn.model_selection import train_test_split

# Load diabetes dataset into pandas DataFrames
X, y = load_diabetes(scaled=False, return_X_y=True, as_frame=True)

# Split into training and testing sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)
display(X_train.head())
display(y_train.head())

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

from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import MinMaxScaler
from sklearn import set_config

# Return pandas DataFrames instead of numpy arrays
set_config(transform_output="pandas")

# Build pipeline
pipe = Pipeline(steps=[
    ('impute_mean', SimpleImputer(strategy='mean')),
    ('rescale', MinMaxScaler())
])

После того, как мы определили наш Pipeline, мы «подгоняем» его к нашему набору данных для обучения и используем для преобразования наборов данных для обучения и тестирования:

# Fit the pipeline to the training data
pipe.fit(X_train)

# Transform data using the fitted pipeline
X_train_transformed = pipe.transform(X_train)
X_test_transformed = pipe.transform(X_test)

Это даст нам два предварительно обработанных набора данных (X_train_transformed и X_test_transformed), готовых для любых последующих шагов, таких как моделирование или выбор функций.

Преимущество использования Pipeline для обработки этих шагов предварительной обработки двоякое:

  1. Защита от утечки: поскольку препроцессор приспособлен к обучающему набору данных X_train, никакая информация о тестовом наборе не «утекает» при подстановке пропущенных значений или создании функций с горячим кодированием.
  2. Избегайте дублирования: если бы мы не использовали Pipeline для обработки этих шагов предварительной обработки, нам пришлось бы преобразовывать набор данных X_test несколько раз (каждый раз, когда мы хотели применить шаг предварительной обработки). В таком маленьком масштабе повторение может показаться не таким уж плохим. Но в сложных рабочих процессах ML вы можете легко увеличить число шагов предварительной обработки до 5, 10 или даже 20. Использование Pipeline делает это простым, потому что мы можем добавить столько шагов, сколько захотим, и все равно нужно преобразовать X_train и X_test только один раз:
preprocessor = Pipeline(steps=[
    ('imputer', SimpleImputer(strategy='mean')),
    ('scaler', MinMaxScaler()),
    ('step_3', ...),
    ('step_4', ...),
    ...,
    ('step_k', ...)
])

preprocessor.fit(X_train)

X_train_transformed = pipe.transform(X_train)
X_test_transformed = pipe.transform(X_test)

2. ColumnTransformer: применение отдельных преобразователей к различным подмножествам функций.

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

Вот где вступает ColumnTransformer. ColumnTransformer позволяет применять разные преобразователи к разным столбцам массива или pandas DataFrame.

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

# This code will only work if you've already run the code in the previous sections

from sklearn.pipeline import Pipeline, make_pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, FunctionTransformer, MinMaxScaler
from sklearn.impute import SimpleImputer

# Categorical columns transformer - (a) impute NAs with the mode, and (b) one-hot encode
categorical_features = ['sex']
categorical_transformer = Pipeline(steps=[
    ('impute_mode', SimpleImputer(strategy='mode')),
    ('ohe', OneHotEncoder(handle_unknown='ignore', sparse=False, drop='first')) # handle_unknown='ignore' ensures that any values not encountered in the training dataset are ignored (i.e. all ohe columns will be set to zero)
])

# Numerical columns transformer - (a) impute NAs with the mean, and (b) rescale
numerical_features = ['bp', 'bmi', 's1', 's2', 's3', 's4', 's5', 's6'] # All except 'age' and 'sex'
numerical_transformer = Pipeline(steps=[
    ('impute_mean', SimpleImputer(strategy='mean')),
    ('rescale', MinMaxScaler())
])

# Combine the individual transformers into a single ColumnTransformer
preprocessor = ColumnTransformer(
    
    # Chain together the individual transformers
    transformers = [
        ('categorical_transformer', categorical_transformer, categorical_features),
        ('numerical_transformer', numerical_transformer, numerical_features),
    ],
    
    # By default, columns which are not transformed by the ColumnTransformer 
    # will be dropped. By setting remainder='passthrough', we ensure that
    # these columns are retained, in their original form.
    remainder='passthrough',
    
    # Prefix feature names with the name of the transformer that generated them (optional)
    verbose_feature_names_out=True
)
# Get visual representation of the preprocessing/feature engineering pipeline
preprocessor

Чтобы применить ColumnTransformer к нашим данным, мы используем тот же код, что и для применения нашего первого Pipeline:

# Fit the preprocessor to the training data
preprocessor.fit(X_train)

# Transform data using the fitted preprocessor
X_train_transformed = preprocessor.transform(X_train)
X_test_transformed = preprocessor.transform(X_test)

3. FeatureUnion: параллельное применение нескольких трансформаторов.

Pipeline и ColumnTransformer — отличные инструменты, но у них есть существенное ограничение. Вы это заметили?

Они могут применять преобразователи только последовательно.

Другими словами, когда вы преобразуете функцию Column1 с помощью Pipeline/ColumnTransformer, scikit-learn сначала применит transformer_1 к Column1, затем применит transformer_2 к преобразованной версии Column1 и так далее. Это хорошо, когда мы хотим предварительно обработать наши данные последовательным образом (например, «сначала вставить пропущенные значения, а затем сразу закодировать»), но это не идеально в случаях, когда мы хотим применять различные этапы предварительной обработки параллельно (например, « создать два новых объекта из одного и того же базового столбца одновременно»). В этих случаях использования стандартных Pipeline или ColumnTransformer будет недостаточно, поскольку исходные «сырые» значения Column1 будут потеряны, как только будет применен первый преобразователь в последовательности.

Если мы хотим применить несколько преобразований к одним и тем же базовым функциям параллельно, нам нужно использовать другой инструмент: FeatureUnion.

Мы можем думать о FeatureUnion как об инструменте, который создает «копию» ваших базовых данных, параллельно применяет преобразователи к этим копиям, а затем сшивает результаты вместе. Каждому преобразователю передаются необработанные базовые данные, поэтому мы не сталкиваемся с проблемой последовательного преобразования.

Чтобы использовать FeatureUnion, нам просто нужно добавить несколько строк кода:

# This code will only work if you've already run the code in the previous sections

from sklearn.pipeline import FeatureUnion
from sklearn.decomposition import PCA, TruncatedSVD

# Define a feature_union object which will create reduced-dimensionality features
union = FeatureUnion(transformer_list=[
    ("pca", PCA(n_components=1)),
    ("svd", TruncatedSVD(n_components=2))
])

# Adapt the numerical transformer so that it includes the FeatureUnion
numerical_features = ['bp', 'bmi', 's1', 's2', 's3', 's4', 's5', 's6'] # All except 'age' and 'sex'
numerical_transformer = Pipeline(steps=[
    ('impute_mean', SimpleImputer(strategy='mean')),
    ('rescale', MinMaxScaler()),
    ('reduce_dimensionality', union)
])

# Categorical columns transformer - same as above
categorical_features = ['sex']
categorical_transformer = Pipeline(steps=[
    ('impute_mode', SimpleImputer(strategy='mode')),
    ('ohe', OneHotEncoder(handle_unknown='ignore', sparse=False, drop='first')) # handle_unknown='ignore' ensures that any values not encountered in the training dataset are ignored (i.e. all ohe columns will be set to zero)
])

# Build the ColumnTransformer
preprocessor = ColumnTransformer(
    transformers = [
        ('categorical_transformer', categorical_transformer, categorical_features),
        ('numerical_transformer', numerical_transformer, numerical_features),
    ],
    remainder='passthrough',
    verbose_feature_names_out=True
)
preprocessor

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

# Fit the preprocessor to the training data
preprocessor.fit(X_train)

# Transform data using the fitted preprocessor
X_train_transformed = preprocessor.transform(X_train)
X_test_transformed = preprocessor.transform(X_test)

4. FunctionTransformer: бесшовная интеграция разработки функций

Все преобразователи и инструменты, описанные выше, используют предварительно созданные классы в scikit-learn для применения стандартных преобразований к вашим данным (например, масштабирование, горячее кодирование, импутирование и т. д.).

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

Создать FunctionTransformer очень просто. Вы начинаете с определения своих функций в стандартном стиле Python, а затем создаете конвейер. Здесь я определяю две простые функции: одну, которая складывает вместе два столбца, и другую, которая вычитает два столбца.

from sklearn.preprocessing import FunctionTransformer

def add_features(X):
    X['featuretrain_test_split2'] = X['feature_1'] + X['feature_2']
    return X

def subtract_features(X):
    X['featurexgboost4'] = X['feature_3'] - X['feature_4']
    return X

# Put into a pipeline
feature_engineering = Pipeline(steps=[
    ('add_features', FunctionTransformer(add_features)),
    ('subtract_features', FunctionTransformer(subtract_features))
])

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

def add_subtract_features(X):
    X['featuretrain_test_split2'] = X['feature_1'] + X['feature_2'] # Add features
    X['featurexgboost4'] = X['feature_3'] - X['feature_4'] # Subtract features
    return X

# Put into a pipeline
feature_engineering = Pipeline(steps=[
    ('add_subtract_features', FunctionTransformer(add_subtract_features)),
])

Наконец, добавьте конвейер feature_engineering к конвейеру preprocessing, который мы определили ранее:

# Combine preprocessing and feature engineering in a single pipeline
pipe = Pipeline([
    ('preprocessing', preprocessor),
    ('feature_engineering', feature_engineering),
])

pipe

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

# Fit the preprocessor to the training data
pipe.fit(X_train)

# Transform data using the fitted preprocessor
X_train_transformed = pipe.transform(X_train)
X_test_transformed = pipe.transform(X_test)

Бонус: сохраните конвейеры для действительно воспроизводимых рабочих процессов.

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

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

import joblib

# Save pipeline
joblib.dump(pipe, "pipe.pkl")

# Assume that the below steps are applied in another notebook/script

# Load pipeline
pretrained_pipe = joblib.load("pipe.pkl")

# Apply pipeline to a new dataset, X_test_new
X_test_new_transformed = pretrained_pipe.transform(X_test_new)

Заключение

Резюме:

  • Pipeline обеспечивает быстрый способ последовательного применения различных преобразователей предварительной обработки к вашим данным.
  • Использование ColumnTransformer — это фантастический способ последовательного применения отдельных шагов предварительной обработки к различным подмножествам функций.
  • FeatureUnion позволяет параллельно применять различные преобразования предварительной обработки
  • FunctionTransformer предоставляет сверхпростой способ написания пользовательских функций разработки функций и интеграции их в ваши пайплайны.

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

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

Спасибо за прочтение!