Данная статья рассчитана для новичков в машинном обучении. Используются следующие интструменты:
В качестве данных выбраны данные соревнования на 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
69713 rows × 22 columns
Как можно заметить, данные необходимо предобработать перед обучением:
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()))
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:
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/
- Python
- Random forest classifier
- Google Colab
- Upsampling data
- class_weight, но его можно использовать при незначительной несбалансированности: соотношении данных разных классов, например, 4:3 (подробнее можно прочесть тут: https://datascience.stackexchange.c...-class-weights-work-in-randomforestclassifier)
- warm_start, который позволяет батчами (частями датасета) обучать данные (подробнее можно прочесть тут: https://stackoverflow.com/questions/42757892/how-to-use-warm-start/42763502)
В качестве данных выбраны данные соревнования на 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 |
Как можно заметить, данные необходимо предобработать перед обучением:
- создать новые признаки на основе старых: возраст вместо даты рождения, день в году вместо даты заявки на заём (только июль-сентябрь 2016)
- обработать nan: заменить nan на моды (категориальные признаки) и медианы (численные признаки)
- преобразовать категориальные признаки в числовые (случайный лес обучается на 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()))
Nan нет, а что же с распределением классов?
# посмотрим на количество строк с разными классами: датасет очень несбалансирован в сторону 0 (в около 68 раз)
train['Approved'].value_counts()
rat = len(train.loc[train['Approved']==0])//len(train.loc[train['Approved']==1])
rat
Создадим новый train датасет методом upsampling:
- возьмём все данные с классом 1
- продублируем его rat раз
- присоединим к данным класса 0 продублированный класс 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/