Забудьте о 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
для обработки этих шагов предварительной обработки двоякое:
- Защита от утечки: поскольку препроцессор приспособлен к обучающему набору данных
X_train
, никакая информация о тестовом наборе не «утекает» при подстановке пропущенных значений или создании функций с горячим кодированием. - Избегайте дублирования: если бы мы не использовали
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_split
2'] = X['feature_1'] + X['feature_2'] return X def subtract_features(X): X['featurexgboost
4'] = 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_split
2'] = X['feature_1'] + X['feature_2'] # Add features X['featurexgboost
4'] = 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 долларов в месяц. Это не добавляет вам дополнительных затрат по сравнению с регистрацией через общую страницу регистрации и помогает поддерживать мое письмо, поскольку я получаю небольшую комиссию.
Спасибо за прочтение!