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

от автора

Upsampling data
Upsampling data

Данная статья рассчитана для новичков в машинном обучении. Используются следующие интструменты:

  • 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

Gender

DOB

Lead_Creation_Date

City_Code

City_Category

Employer_Code

Employer_Category1

Employer_Category2

Monthly_Income

Customer_Existing_Primary_Bank_Code

Primary_Bank_Type

Contacted

Source

Source_Category

Existing_EMI

Loan_Amount

Loan_Period

Interest_Rate

EMI

Var1

Approved

0

APPC90493171225

Female

23/07/79

15/07/16

C10001

A

COM0044082

A

4.0

2000.0

B001

P

N

S122

G

0.0

NaN

NaN

NaN

NaN

0

0

1

APPD40611263344

Male

07/12/86

04/07/16

C10003

A

COM0000002

C

1.0

3500.0

B002

P

Y

S122

G

0.0

20000.0

2.0

13.25

953.0

10

0

2

APPE70289249423

Male

10/12/82

19/07/16

C10125

C

COM0005267

C

4.0

2250.0

B003

G

Y

S143

B

0.0

45000.0

4.0

NaN

NaN

0

0

3

APPF80273865537

Male

30/01/89

09/07/16

C10477

C

COM0004143

A

4.0

3500.0

B003

G

Y

S143

B

0.0

92000.0

5.0

NaN

NaN

7

0

4

APPG60994436641

Male

19/04/85

20/07/16

C10002

A

COM0001781

A

4.0

10000.0

B001

P

Y

S134

B

2500.0

50000.0

2.0

NaN

NaN

10

0

69708

APPU90955789628

Female

31/07/83

30/09/16

C10006

A

COM0000010

A

1.0

4900.0

B002

P

N

S122

G

0.0

NaN

NaN

NaN

NaN

10

0

69709

APPV80989824738

Female

27/01/71

30/09/16

C10116

C

COM0045789

A

4.0

7190.1

B002

P

N

S122

G

1450.0

NaN

NaN

NaN

NaN

7

0

69710

APPW50697209842

Female

01/02/92

30/09/16

C10022

B

COM0013284

C

4.0

1600.0

B030

P

Y

S122

G

0.0

24000.0

4.0

35.50

943.0

2

0

69711

APPY50870035036

Male

27/06/78

30/09/16

C10002

A

COM0000098

C

3.0

9893.0

B002

P

Y

S122

G

1366.0

80000.0

5.0

NaN

NaN

10

0

69712

APPZ60733046119

Male

31/12/89

30/09/16

C10003

A

COM0000056

A

1.0

4230.0

NaN

NaN

Y

S122

G

0.0

69000.0

4.0

13.99

1885.0

10

0

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[i] = df[i].loc[df[i].notnull()].str[1:].astype(int)     df[i] = df[i].fillna(df[i].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[i] = df[i].fillna(df[i].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[i].notnull().unique()))

Nan нет, а что же с распределением классов?

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

Создадим новый 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_magazines/blob/master/Banking_Marketing_Leads_Conversion_Data/Project.ipynb

ссылка на оригинал статьи https://habr.com/ru/post/568266/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *