Pull to refresh

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

Reading time6 min
Views12K
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

Полный код можно скачать здесь.

Tags:
Hubs:
0
Comments15

Articles

Change theme settings