import numpy as np
import pandas as pd
pd.set_option("display.max.columns", 100)
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import StratifiedKFold, cross_val_score, train_test_split
Считаем данные и посмотрим на первые несколько строк. Видим, что у нас тут немало категориальных признаков.
df = pd.read_csv("../../data/bank.csv")
df.head()
df.info()
Всего 9 признаков со строковыми значениями.
df.columns[df.dtypes == "object"]
Попытаемся сначала просто проигнорировать категориальные признаки. Обучим случайный лес и посмотрим на ROC AUC на кросс-валидации и на отоженной выборке. Это будет наш бейзлайн.
df_no_cat, y = df.loc[:, df.dtypes != "object"].drop("y", axis=1), df["y"]
df_no_cat_part, df_no_cat_valid, y_train_part, y_valid = train_test_split(
df_no_cat, y, test_size=0.3, stratify=y, random_state=17
)
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=17)
forest = RandomForestClassifier(random_state=17)
np.mean(
cross_val_score(forest, df_no_cat_part, y_train_part, cv=skf, scoring="roc_auc")
)
forest.fit(df_no_cat_part, y_train_part)
roc_auc_score(y_valid, forest.predict_proba(df_no_cat_valid)[:, 1])
Сделаем то же самое, но попробуем закодировать категориальные признаки по-простому: с помощью LabelEncoder
.
from sklearn.preprocessing import LabelEncoder
label_encoder = LabelEncoder()
df_cat_label_enc = df.copy().drop("y", axis=1)
for col in df.columns[df.dtypes == "object"]:
df_cat_label_enc[col] = label_encoder.fit_transform(df_cat_label_enc[col])
df_cat_label_enc.shape
df_cat_label_enc_part, df_cat_label_enc_valid = train_test_split(
df_cat_label_enc, test_size=0.3, stratify=y, random_state=17
)
np.mean(
cross_val_score(
forest, df_cat_label_enc_part, y_train_part, cv=skf, scoring="roc_auc"
)
)
forest.fit(df_cat_label_enc_part, y_train_part)
roc_auc_score(y_valid, forest.predict_proba(df_cat_label_enc_valid)[:, 1])
Теперь сделаем то, что обычно по умолчанию и делают – бинаризацию категориальных признаков. Dummy-признаки, One-Hot Encoding... с небольшими различиями это об одном же - для каждого значения каждого категориального признака завести свой бинарный признак.
df_cat_dummies = pd.get_dummies(df, columns=df.columns[df.dtypes == "object"]).drop(
"y", axis=1
)
df_cat_dummies.shape
df_cat_dummies_part, df_cat_dummies_valid = train_test_split(
df_cat_dummies, test_size=0.3, stratify=y, random_state=17
)
np.mean(
cross_val_score(
forest, df_cat_dummies_part, y_train_part, cv=skf, scoring="roc_auc"
)
)
forest.fit(df_cat_dummies_part, y_train_part)
roc_auc_score(y_valid, forest.predict_proba(df_cat_dummies_valid)[:, 1])
Пока лес все еще лучше регрессии (хотя мы не тюнили гиперпараметры, но и не будем). Мы хотим идти дальше. Мощной техникой для работы с категориальными признаками будет учет попарных взаимодействий признаков (feature interactions). Построим попарные взаимодействия всех признаков. Вообще тут можно пойти дальше и строить взаимодействия трех и более признаков. Owen Zhang как-то строил даже 7-way interactions. Чего не сделаешь ради победы на Kaggle! :)
df_interact = df.copy()
cat_features = df.columns[df.dtypes == "object"]
for i, col1 in enumerate(cat_features):
for j, col2 in enumerate(cat_features[i + 1 :]):
df_interact[col1 + "_" + col2] = df_interact[col1] + "_" + df_interact[col2]
df_interact.shape
df_interact.head()
Получилось аж 824 бинарных признака – многовато для такой задачи, и тут случайный лес начинает не справляться, да и логистическая регрессия сработала хуже, чем в прошлый раз.
df_interact_cat_dummies = pd.get_dummies(
df_interact, columns=df_interact.columns[df_interact.dtypes == "object"]
).drop("y", axis=1)
df_interact_cat_dummies.shape
df_interact_cat_dummies_part, df_interact_cat_dummies_valid = train_test_split(
df_interact_cat_dummies, test_size=0.3, stratify=y, random_state=17
)
np.mean(
cross_val_score(
forest, df_interact_cat_dummies_part, y_train_part, cv=skf, scoring="roc_auc"
)
)
forest.fit(df_interact_cat_dummies_part, y_train_part)
roc_auc_score(y_valid, forest.predict_proba(df_interact_cat_dummies_valid)[:, 1])
Случайному лесу уже тяжеловато, когда признаков так много, а вот логистической регрессии – норм.
from sklearn.linear_model import LogisticRegression
logit = LogisticRegression(random_state=17)
np.mean(
cross_val_score(
logit, df_interact_cat_dummies_part, y_train_part, cv=skf, scoring="roc_auc"
)
)
logit.fit(df_interact_cat_dummies_part, y_train_part)
roc_auc_score(y_valid, logit.predict_proba(df_interact_cat_dummies_valid)[:, 1])
Теперь будем использовать технику кодирования категориальных признаков средним значением целевого признака. Это очень мощная техника, правда, надо умело ее использовать – легко переобучиться. Основная идея – для каждого значения категориального признака посчитать среднее значение целевого признака и заменить категориальный признак на посчитанные средние. Правда, считать средние надо на кросс-валидации, а то легко переобучиться. Но далее я адресую к видео топ-участников соревнований Kaggle, от них можно узнать про эту технику из первых уст.
Похожая техника используется и в CatBoost.
Для начала давайте таким образом закодируем исходные категориальные признаки.
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=1)
train_df, y = df.copy(), df["y"]
train_df_part, valid_df, y_train_part, y_valid = train_test_split(
train_df.drop("y", axis=1), y, test_size=0.3, stratify=y, random_state=17
)
def mean_target_enc(train_df, y_train, valid_df, skf):
import warnings
warnings.filterwarnings("ignore")
glob_mean = y_train.mean()
train_df = pd.concat([train_df, pd.Series(y_train, name="y")], axis=1)
new_train_df = train_df.copy()
cat_features = train_df.columns[train_df.dtypes == "object"].tolist()
for col in cat_features:
new_train_df[col + "_mean_target"] = [
glob_mean for _ in range(new_train_df.shape[0])
]
for train_idx, valid_idx in skf.split(train_df, y_train):
train_df_cv, valid_df_cv = (
train_df.iloc[train_idx, :],
train_df.iloc[valid_idx, :],
)
for col in cat_features:
means = valid_df_cv[col].map(train_df_cv.groupby(col)["y"].mean())
valid_df_cv[col + "_mean_target"] = means.fillna(glob_mean)
new_train_df.iloc[valid_idx] = valid_df_cv
new_train_df.drop(cat_features + ["y"], axis=1, inplace=True)
for col in cat_features:
means = valid_df[col].map(train_df.groupby(col)["y"].mean())
valid_df[col + "_mean_target"] = means.fillna(glob_mean)
valid_df.drop(train_df.columns[train_df.dtypes == "object"], axis=1, inplace=True)
return new_train_df, valid_df
train_mean_target_part, valid_mean_target = mean_target_enc(
train_df_part, y_train_part, valid_df, skf
)
np.mean(
cross_val_score(
forest, train_mean_target_part, y_train_part, cv=skf, scoring="roc_auc"
)
)
forest.fit(train_mean_target_part, y_train_part)
roc_auc_score(y_valid, forest.predict_proba(valid_mean_target)[:, 1])
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=1)
train_df, y = df_interact.drop("y", axis=1).copy(), df_interact["y"]
train_df_part, valid_df, y_train_part, y_valid = train_test_split(
train_df, y, test_size=0.3, stratify=y, random_state=17
)
train_mean_target_part, valid_mean_target = mean_target_enc(
train_df_part, y_train_part, valid_df, skf
)
np.mean(
cross_val_score(
forest, train_mean_target_part, y_train_part, cv=skf, scoring="roc_auc"
)
)
forest.fit(train_mean_target_part, y_train_part)
roc_auc_score(y_valid, forest.predict_proba(valid_mean_target)[:, 1])
Опять лучше справляется логистическая регрессия.
np.mean(
cross_val_score(
logit, train_mean_target_part, y_train_part, cv=skf, scoring="roc_auc"
)
)
logit.fit(train_mean_target_part, y_train_part)
roc_auc_score(y_valid, logit.predict_proba(valid_mean_target)[:, 1])
В библиотеке Catboost, помимо всего прочего, реализована как раз техника кодирования категориальных значений средним значением целевого признака. Результаты получаются хорошими именно когда в данных много важных категориальных признаков. Из минусов можно отметить меньшую (пока что) производительность в сравнении с Xgboost и LightGBM.
from catboost import CatBoostClassifier
ctb = CatBoostClassifier(random_seed=17)
train_df, y = df.drop("y", axis=1), df["y"]
train_df_part, valid_df, y_train_part, y_valid = train_test_split(
train_df, y, test_size=0.3, stratify=y, random_state=17
)
cat_features_idx = np.where(train_df_part.dtypes == "object")[0].tolist()
%%time
cv_scores = []
for train_idx, test_idx in skf.split(train_df_part, y_train_part):
cv_train_df, cv_valid_df = (
train_df_part.iloc[train_idx, :],
train_df_part.iloc[test_idx, :],
)
y_cv_train, y_cv_valid = y_train_part.iloc[train_idx], y_train_part.iloc[test_idx]
ctb.fit(cv_train_df, y_cv_train, cat_features=cat_features_idx)
cv_scores.append(roc_auc_score(y_cv_valid, ctb.predict_proba(cv_valid_df)[:, 1]))
np.mean(cv_scores)
%%time
ctb.fit(train_df_part, y_train_part, cat_features=cat_features_idx);
roc_auc_score(y_valid, ctb.predict_proba(valid_df)[:, 1])