машинное обучение
July 26, 2022

Использование выборок при валидации моделей

Рассмотрим, как правильно делить данные на выборки, чтобы потом не приходилось делать это опять, несмотря на использование различных способов предобработки для разных алгоритмов машинного обучения. Сначала скачаем и сделаем быструю обработку датасета о пассажирах "Титаника":

import pandas as pd
from sklearn.metrics import roc_auc_score
import numpy as np
np.random.seed(0)

df = pd.read_csv("https://s3.amazonaws.com/h2o-public-test-data/smalldata/gbm_test/titanic.csv")

df['sex'] = df['sex'].map({'male':1, 'female':0})
df[['age', 'body', 'fare']] = df[['age', 'body', 'fare']].fillna(df[['age', 'body', 'fare']].agg('median').to_dict())
df = df.fillna('unknown')

df = df.sample(frac=1).reset_index(drop=True)
df.head()

Теперь определим категориальные и числовые колонки:

id_cols = ['name', 'ticket']

cat_level = 30
target_col = 'survived'
cols_ser = df.drop([target_col]+id_cols, axis=1).nunique()

cat_cols = cols_ser[cols_ser<=cat_level].index
num_cols = cols_ser[cols_ser>cat_level].index
str_cols = df.drop([target_col]+id_cols, axis=1).select_dtypes(exclude=np.number).columns

cat_cols = cat_cols.union(str_cols).tolist()
num_cols = num_cols.difference(str_cols).tolist()

display(cat_cols)
display(num_cols)

Так как данные перемешаны разделим их с помощью np.split и будем работать с индексами выборок (о способах разделения писал ранее) :

tr_idx, val_idx, ts_idx = np.split(np.arange(df.shape[0]), 
                                   [round(0.8*df.shape[0]), round(0.9*df.shape[0])])

Ниже демонстрируются способы работы с выборками в алгоритмах, предобработка датасета в которых различается. CatBoost:

from catboost import CatBoostClassifier

model_cat = CatBoostClassifier(cat_features = cat_cols, silent=True)
model_cat.fit(df.drop(columns=id_cols+[target_col]).iloc[tr_idx], df[target_col].iloc[tr_idx])

auc_cat = roc_auc_score(df[target_col].iloc[val_idx], 
                model_cat.predict_proba(df.drop(columns=id_cols+[target_col]).iloc[val_idx])[:,1])
auc_cat

LightGBM (для категориальных колонок достаточно задать тип category):

from lightgbm import LGBMClassifier

model_lgb = LGBMClassifier()
model_lgb.fit(df.drop(columns=id_cols+[target_col]).\
              astype({it:'category' for it in cat_cols}).iloc[tr_idx], df[target_col].iloc[tr_idx])

auc_lgb = roc_auc_score(df[target_col].iloc[val_idx], 
                        model_lgb.predict_proba(df.drop(columns=id_cols+[target_col])\
                                    .astype({it:'category' for it in cat_cols}).iloc[val_idx])[:,1])

auc_lgb

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

from sklearn.preprocessing import OneHotEncoder
oh = OneHotEncoder(sparse=False, handle_unknown='ignore')

ar_tr = oh.fit_transform(df[cat_cols].iloc[tr_idx]) 
ar_val = oh.transform(df[cat_cols].iloc[val_idx])
ar_ts = oh.transform(df[cat_cols].iloc[ts_idx])

X_tr = pd.DataFrame(ar_tr, columns=oh.get_feature_names_out(cat_cols), index=df.iloc[tr_idx].index)
X_val = pd.DataFrame(ar_val, columns=oh.get_feature_names_out(cat_cols), index=df.iloc[val_idx].index)
X_ts = pd.DataFrame(ar_ts, columns=oh.get_feature_names_out(cat_cols), index=df.iloc[ts_idx].index)

X_y_tr = pd.concat([df[id_cols+[target_col]+num_cols].iloc[tr_idx], X_tr], axis=1)
X_y_val = pd.concat([df[id_cols+[target_col]+num_cols].iloc[val_idx], X_val], axis=1)
X_y_ts = pd.concat([df[id_cols+[target_col]+num_cols].iloc[ts_idx], X_ts], axis=1)

Теперь масштабирование:

from sklearn.preprocessing import StandardScaler

sc = StandardScaler()
X_y_tr[num_cols] = sc.fit_transform(X_y_tr[num_cols])
X_y_val[num_cols] = sc.transform(X_y_val[num_cols])
X_y_ts[num_cols] = sc.transform(X_y_ts[num_cols])

Ну и, наконец, моделирование:

from sklearn.linear_model import LogisticRegression

model_log = LogisticRegression()

model_log.fit(X_y_tr.drop(columns=id_cols+[target_col]), X_y_tr[target_col])

roc_auc_score(X_y_val[target_col],model_log.predict_proba(
                                            X_y_val.drop(columns=id_cols+[target_col]))[:,1])

Сравните этот способ с применением Pipeline-ов. Он позволяет работать с датасетом без особой предобработки почти так же, как CatBoost или LightGBM:

from sklearn.compose import make_column_transformer
from sklearn.pipeline import Pipeline


ct = make_column_transformer((OneHotEncoder(sparse=False, handle_unknown='ignore'), cat_cols), 
                             (StandardScaler(), num_cols), remainder='passthrough')

model_log_pipe = Pipeline(steps=[('prep', ct), ('clf', LogisticRegression())])

model_log_pipe.fit(df.drop(columns=id_cols+[target_col]).iloc[tr_idx], df[target_col].iloc[tr_idx])

roc_auc_score(df[target_col].iloc[val_idx], model_log_pipe.predict_proba(
                                                    df.drop(columns=id_cols+[target_col]).iloc[val_idx])[:,1])

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

tr_val_idx = np.concatenate([tr_idx, val_idx])
model_cat.fit(df.drop(columns=id_cols+[target_col]).iloc[tr_val_idx], df[target_col].iloc[tr_val_idx])

roc_auc_score(df[target_col].iloc[ts_idx], model_cat.predict_proba(
                                                    df.drop(columns=id_cols+[target_col]).iloc[ts_idx])[:,1])

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

Не пропустите ничего интересного и подписывайтесь на страницы канала в других социальных сетях:

Яндекс Дзен

Telegram