Как победить несбалансированность датасета: метод upsampling data

Kate

Administrator
Команда форума
Данная статья рассчитана для новичков в машинном обучении. Используются следующие интструменты:
  • Python
  • Random forest classifier
  • Google Colab
  • Upsampling data
Каждый дата саентист хоть раз сталкивался с проблемой несбалансированности данных для классификации: какой-то класс превосходит другие. Существует далеко не один способ борьбы с этой проблемой. Наибольшую известность имеет преобразование гиперпараметров, например:
Однако в данной статье мы рассмотрим метод, не связанный с гиперпараметрами модели: upsampling data. Мы преувеличим количество наименьших классов ещё до обучения модели: продублируем n (отношение количества преобладающего класс к интересующему) раз наименьший класс.
В качестве данных выбраны данные соревнования на kaggle: https://www.kaggle.com/arashnic/banking-loan-prediction. В качестве алгоритма обучения возьмём случайные лес. Начнём!
Импортируем необходимые библиотеки:
import seaborn as sns
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from numpy import nan
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import confusion_matrix
from sklearn.metrics import precision_recall_fscore_support
from sklearn.metrics import precision_recall_curve
from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score, classification_report, confusion_matrix
from sklearn.metrics import roc_auc_score
from sklearn.metrics import roc_curve, auc
from sklearn import metrics
import copy
from tune_sklearn import TuneSearchCV
import scipy
from ray import tune
Загружаем данные (в качестве среды разработки мной использовался Google Colab, а данные располагались на Google Drive):
from google.colab import drive
drive.mount('/content/drive')
train = pd.read_csv('/content/drive/MyDrive/портфолио/Project "Help to increase customer acquisition"/train.csv')
test = pd.read_csv('/content/drive/MyDrive/портфолио/Project "Help to increase customer acquisition"/test.csv')
Посмотрим на исходные данные train (их мы будем использовать для тренировки и теста, в данных test отсутствует таргетированный столбец)
train
GenderDOBLead_Creation_DateCity_CodeCity_CategoryEmployer_CodeEmployer_Category1Employer_Category2Monthly_IncomeCustomer_Existing_Primary_Bank_CodePrimary_Bank_TypeContactedSourceSource_CategoryExisting_EMILoan_AmountLoan_PeriodInterest_RateEMIVar1Approved
0APPC90493171225Female23/07/7915/07/16C10001ACOM0044082A4.02000.0B001PNS122G0.0NaNNaNNaNNaN00
1APPD40611263344Male07/12/8604/07/16C10003ACOM0000002C1.03500.0B002PYS122G0.020000.02.013.25953.0100
2APPE70289249423Male10/12/8219/07/16C10125CCOM0005267C4.02250.0B003GYS143B0.045000.04.0NaNNaN00
3APPF80273865537Male30/01/8909/07/16C10477CCOM0004143A4.03500.0B003GYS143B0.092000.05.0NaNNaN70
4APPG60994436641Male19/04/8520/07/16C10002ACOM0001781A4.010000.0B001PYS134B2500.050000.02.0NaNNaN100
.....................................................................
69708APPU90955789628Female31/07/8330/09/16C10006ACOM0000010A1.04900.0B002PNS122G0.0NaNNaNNaNNaN100
69709APPV80989824738Female27/01/7130/09/16C10116CCOM0045789A4.07190.1B002PNS122G1450.0NaNNaNNaNNaN70
69710APPW50697209842Female01/02/9230/09/16C10022BCOM0013284C4.01600.0B030PYS122G0.024000.04.035.50943.020
69711APPY50870035036Male27/06/7830/09/16C10002ACOM0000098C3.09893.0B002PYS122G1366.080000.05.0NaNNaN100
69712APPZ60733046119Male31/12/8930/09/16C10003ACOM0000056A1.04230.0NaNNaNYS122G0.069000.04.013.991885.0100
69713 rows × 22 columns
Как можно заметить, данные необходимо предобработать перед обучением:
  1. создать новые признаки на основе старых: возраст вместо даты рождения, день в году вместо даты заявки на заём (только июль-сентябрь 2016)
  2. обработать nan: заменить nan на моды (категориальные признаки) и медианы (численные признаки)
  3. преобразовать категориальные признаки в числовые (случайный лес обучается на integer, float, boolean)
Для удобства обработки обоих наборов данных создадим функцию предобработки:
def data_preprocessing(df):
# преобразуем значения поля Gender: Female - 0, Male - 1
df.loc[(df['Gender'] == 'Female'), 'Gender'] = 0
df.loc[(df['Gender'] != 0), 'Gender'] = 1
# добавим признак возраст
df['DOB_year'] = nan
df.loc[df['DOB'].notnull(), 'DOB_year'] = 121 - df['DOB'].loc[df['DOB'].notnull()].str[-2:].astype(int)
df['DOB_year'] = df['DOB_year'].fillna(df['DOB_year'].median())
# добавим признак дней с начала года от даты заёма (в данных июль-сентябрь 2016)
df['Lead_Creation_Date'] = df['Lead_Creation_Date'].str.replace(r'(..\/..\/)(..)', r'\1 20\2')
df['Lead_Creation_Date'] = pd.to_datetime(df['Lead_Creation_Date'], format="%d/%m/ %Y")
df['Lead_Creation_Date_day'] = (df['Lead_Creation_Date']-pd.to_datetime('1/1/2016')).astype('timedelta64[h]')/24

#удаляем первый символ данных столбцов (они одинаковы), преобразуем в int,
#заменяем nan на моду (признак был категориальным)
first_drop_cols = ['City_Code', 'Source', 'Customer_Existing_Primary_Bank_Code']
for i in first_drop_cols:
df = df.loc[df.notnull()].str[1:].astype(int)
df = df.fillna(df.mode()[0])
# удаляем первые 3 символа и далее аналогично верхнему
df['Employer_Code'] = df['Employer_Code'].loc[df['Employer_Code'].notnull()].str[3:].astype(int)
df['Employer_Code'] = df['Employer_Code'].fillna(df['Employer_Code'].mode()[0])
# заполняем nan медианой
amount_cols = ['Employer_Category2', 'Monthly_Income', 'Existing_EMI', 'Loan_Amount',
'Loan_Period', 'Interest_Rate', 'EMI', 'Var1']
df[amount_cols] = df[amount_cols].fillna(df[amount_cols].median())
# заполняем nan модой и кодируем столбцы (переводим в численные)
str_cols = ['City_Category', 'Employer_Category1', 'Primary_Bank_Type', 'Contacted', 'Source_Category']
str_dict = dict(enumerate(str_cols))
for i in str_cols:
df = df.fillna(df.mode()[0])
le = LabelEncoder()
df[str_cols] = df[str_cols].apply(le.fit_transform)
return df
train = data_preprocessing(train)
test = data_preprocessing(test)
# преобразуем в float
not_float_cols = ['ID', 'DOB', 'Lead_Creation_Date']
train[train.columns.difference(not_float_cols)] = train[train.columns.difference(not_float_cols)].astype(float)
#проверим нет ли NaN в столбцах
for i in train.columns.difference(unused_cols):
print('{} {}'.format(i, train.notnull().unique()))
ad0039dd9f6b53e2f7486c694a5a57b9.png

Nan нет, а что же с распределением классов?
# посмотрим на количество строк с разными классами: датасет очень несбалансирован в сторону 0 (в около 68 раз)
train['Approved'].value_counts()
13f65993d96c8ac8ac8a48cd319e0714.png
# отношение количества строк с 0 к 1 Approved
rat = len(train.loc[train['Approved']==0])//len(train.loc[train['Approved']==1])
rat
045b9c6b53a0d41cbcb455906bceee30.png

Создадим новый train датасет методом upsampling:
  1. возьмём все данные с классом 1
  2. продублируем его rat раз
  3. присоединим к данным класса 0 продублированный класс 1 и перемещаем
df_1 = train.loc[train['Approved']==1]
df_1 = df_1.loc[df_1.index.repeat(rat)]
train_n = pd.concat([train.loc[train['Approved']==0], df_1]).sample(frac=1)
Посмотрим на новое распределение классов:
train_n['Approved'].value_counts()
Распределение классов в новом датасете
Распределение классов в новом датасете
Приступаем к обучению. Будем использовать случайный лес, а также его тюнинг (подбор наиболее качественных гиперпараметров)
# делим на тренировочную и тестовую
X = train_n[train_n.columns.difference(['Approved'])]
y = train_n['Approved']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)

# задаём параметры, из диапазона значений которых надо выбрать лучшее
# https://github.com/ray-project/tune-sklearn
param_dists = {
'criterion': tune.choice(['gini', 'entropy']),
'max_depth': tune.choice([i for i in range(2, 17)]),
'max_features': tune.choice(['log2', 'sqrt']),
'min_samples_leaf': tune.choice([i for i in range(2, 33)]),
'min_samples_split': tune.choice([i for i in range(2, 17)]),
'random_state': tune.choice([23])
}

hyperopt_tune_search = TuneSearchCV(RandomForestClassifier(),
param_distributions=param_dists,
n_trials=2,
early_stopping=True,
max_iters=10,
search_optimization="hyperopt"
)

hts = hyperopt_tune_search.fit(X_train, y_train)
y_pred = hts.predict(X_test)
print(confusion_matrix(y_test, y_pred))
print(precision_recall_fscore_support(y_test, y_pred))
print(roc_auc_score(y_test, y_pred, average='weighted'))
Значения метрик
Значения метрик
Значения метрик f1 получились достаточно высокие (90%+), что может говорить качественности модели классификации.
Таким образом, работать с несбалансированными данными можно не только через гиперпараметры, но и методом upsampling. Он позволяет метрикам наименьшего класса от 0.0 достичь значений более 0.8
Полный код можно скачать здесь: https://github.com/sivovaalex/for_m...Marketing_Leads_Conversion_Data/Project.ipynb


Источник статьи: https://habr.com/ru/post/568266/
 
Сверху