Меня зовут Арсений Казанцев, и я буду ревьюером твоего проекта. Если не против, то давай общаться на «ты» :) Но мы легко можем перейти на «вы», если тебе так будет комфортнее.
Моя основная цель — помочь тебе стать крутым специалистом, поэтому давай разберем твой проект и попробуем сделать его еще лучше. Ниже ты найдешь мои комментарии - пожалуйста, не перемещай, не изменяй и не удаляй их. Я буду использовать цветовую разметку:
Давай работать над проектом в диалоге: если ты что-то меняешь в проекте или отвечаешь на мои комменатри — пиши об этом. Если нужно удалить часть кода или целую ячейку, то лучше просто закомментируй. Мне будет легче отследить изменения, если ты выделишь свои комментарии:
Отток клиентов¶
Из «Бета-Банка» стали уходить клиенты. Каждый месяц. Немного, но заметно. Банковские маркетологи посчитали: сохранять текущих клиентов дешевле, чем привлекать новых.
Нужно спрогнозировать, уйдёт клиент из банка в ближайшее время или нет. Вам предоставлены исторические данные о поведении клиентов и расторжении договоров с банком.
Постройте модель с предельно большим значением F1-меры. Чтобы сдать проект успешно, нужно довести метрику до 0.59. Проверьте F1-меру на тестовой выборке самостоятельно.
Дополнительно измеряйте AUC-ROC, сравнивайте её значение с F1-мерой.
Источник данных: https://www.kaggle.com/barelydedicated/bank-customer-churn-modeling
Подготовка данных¶
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (f1_score, mean_squared_error,
roc_auc_score, roc_curve, precision_score, recall_score, accuracy_score)
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.utils import shuffle
from sklearn.preprocessing import StandardScaler
Импорты могут быть сгруппированы в следующем порядке:
1.импорты из стандартной библиотеки
2.импорты сторонних библиотек
3.импорты модулей текущего проекта
Можно вставить пустую строку между каждой группой импортов.
В каждой группе желательно отсортировать импорты в алфавитном порядке.
Ccылка для ознакомления: https://pythonworld.ru/osnovy/pep-8-rukovodstvo-po-napisaniyu-koda-na-python.html
data = pd.read_csv('/datasets/Churn.csv')
data.head()
| RowNumber | CustomerId | Surname | CreditScore | Geography | Gender | Age | Tenure | Balance | NumOfProducts | HasCrCard | IsActiveMember | EstimatedSalary | Exited | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 15634602 | Hargrave | 619 | France | Female | 42 | 2.0 | 0.00 | 1 | 1 | 1 | 101348.88 | 1 |
| 1 | 2 | 15647311 | Hill | 608 | Spain | Female | 41 | 1.0 | 83807.86 | 1 | 0 | 1 | 112542.58 | 0 |
| 2 | 3 | 15619304 | Onio | 502 | France | Female | 42 | 8.0 | 159660.80 | 3 | 1 | 0 | 113931.57 | 1 |
| 3 | 4 | 15701354 | Boni | 699 | France | Female | 39 | 1.0 | 0.00 | 2 | 0 | 0 | 93826.63 | 0 |
| 4 | 5 | 15737888 | Mitchell | 850 | Spain | Female | 43 | 2.0 | 125510.82 | 1 | 1 | 1 | 79084.10 | 0 |
data.info()
print(f'\n Количесвто явных дубликатов: {data.duplicated().sum()}')
<class 'pandas.core.frame.DataFrame'> RangeIndex: 10000 entries, 0 to 9999 Data columns (total 14 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 RowNumber 10000 non-null int64 1 CustomerId 10000 non-null int64 2 Surname 10000 non-null object 3 CreditScore 10000 non-null int64 4 Geography 10000 non-null object 5 Gender 10000 non-null object 6 Age 10000 non-null int64 7 Tenure 9091 non-null float64 8 Balance 10000 non-null float64 9 NumOfProducts 10000 non-null int64 10 HasCrCard 10000 non-null int64 11 IsActiveMember 10000 non-null int64 12 EstimatedSalary 10000 non-null float64 13 Exited 10000 non-null int64 dtypes: float64(3), int64(8), object(3) memory usage: 1.1+ MB Количесвто явных дубликатов: 0
data.hist(figsize=(15,15), bins = 50)
plt.show()
Промежуточное :
1) Открыли датасет. Пробежались по колонкам.
2) Основные проблемы: имена колонок имеют разный регистр и отсутствует "змеинный_регистр", имеются колонки которые
нам не пригодяться в исследовании, несоответствие данных их типу, имеются пропуски в данных.
3) В колонках "Gender" и "Geography" у нас находяться категориальные данные. Необходимо будет провести кодирование.
4) В колонке 'NumOfProducts' есть выбросы, это значения 3 и 4. Будем избавляться от этих значений, предварительно посмотрев сколько человек отказалось от услуг из этих групп.
Немного подробнее о проблемах.
Колонки "RowNumber", "CustomerId", "Surname" не несут полезной информации с точки зрения нашего исследования. В связи с этим мы их просто удалим.
В столбце "Tenure" есть пропуски, пока это мы оставляем как есть и в дальнейшем будем их обрабатывать. Значения здесь целочисленные, но из-за пропусков у нас тип 'float64'. Далее по коду мы это все испраим.
#приводим названия столбцов в соответствие
data.columns = data.columns.str.lower()
data.rename(columns = {'creditscore':'credit_score','numofproducts':'num_of_products',
'hascrcard':'has_cr_card', 'isactivemember':'is_active_member',
'estimatedsalary':'estimated_salary'}, inplace = True)
data.columns
Index(['rownumber', 'customerid', 'surname', 'credit_score', 'geography',
'gender', 'age', 'tenure', 'balance', 'num_of_products', 'has_cr_card',
'is_active_member', 'estimated_salary', 'exited'],
dtype='object')
print('Отношение положительных к отрицательным в выборке ', data.query('(num_of_products > 2) & (exited == 1)')
['exited'].count()/data.query('(num_of_products > 2)')['exited'].count())
print('Отношение положительных к отрицательным во всем ДФ ', data.query('(exited == 1)')['exited'].count()
/data.query('(exited == 0)')['exited'].count())
Отношение положительных к отрицательным в выборке 0.8588957055214724 Отношение положительных к отрицательным во всем ДФ 0.25580811252040686
Избавляться от значений в колонке "num_of_products" не будем. Т.к. здесь очень интересная зависимость.
Во всем датасете преобладают отрицательные значения, а в нашей выборке положительные
#удаляем столбцы
data.drop(['rownumber','customerid','surname'], axis = 1, inplace = True)
data.head()
| credit_score | geography | gender | age | tenure | balance | num_of_products | has_cr_card | is_active_member | estimated_salary | exited | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 619 | France | Female | 42 | 2.0 | 0.00 | 1 | 1 | 1 | 101348.88 | 1 |
| 1 | 608 | Spain | Female | 41 | 1.0 | 83807.86 | 1 | 0 | 1 | 112542.58 | 0 |
| 2 | 502 | France | Female | 42 | 8.0 | 159660.80 | 3 | 1 | 0 | 113931.57 | 1 |
| 3 | 699 | France | Female | 39 | 1.0 | 0.00 | 2 | 0 | 0 | 93826.63 | 0 |
| 4 | 850 | Spain | Female | 43 | 2.0 | 125510.82 | 1 | 1 | 1 | 79084.10 | 0 |
Кодирование¶
В двух колонках, 'gender' и 'geography' содержаться категориальные переменные. Т.к. мы будем строить модели нам необходимо привести эти значения в соответствующий вид.
# data['gender'] = pd.get_dummies(data['gender'], drop_first = True) # провели кодирование с учетом "Дамми-ловушки"
# print(f'Gender: Female = 0 || male = 1')
data = pd.get_dummies(data, drop_first=True) # провели кодирование по ДФ для колонки "geography"
data.columns = data.columns.str.lower()
data.head()
| credit_score | age | tenure | balance | num_of_products | has_cr_card | is_active_member | estimated_salary | exited | geography_germany | geography_spain | gender_male | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 619 | 42 | 2.0 | 0.00 | 1 | 1 | 1 | 101348.88 | 1 | 0 | 0 | 0 |
| 1 | 608 | 41 | 1.0 | 83807.86 | 1 | 0 | 1 | 112542.58 | 0 | 0 | 1 | 0 |
| 2 | 502 | 42 | 8.0 | 159660.80 | 3 | 1 | 0 | 113931.57 | 1 | 0 | 0 | 0 |
| 3 | 699 | 39 | 1.0 | 0.00 | 2 | 0 | 0 | 93826.63 | 0 | 0 | 0 | 0 |
| 4 | 850 | 43 | 2.0 | 125510.82 | 1 | 1 | 1 | 79084.10 | 0 | 0 | 1 | 0 |
У нас в теории: Преобразуйте колонку Gender техникой OHE. Вызовите функцию pd.get_dummies() и напечатайте на экране первые пять записей изменённой таблицы.
Будем разбираться)
У тебя могут возникнуть ряд вопросов почему и зачем делать именно так, поэтому давай попробую все объяснить.
1) Почему признаки нужно кодировать после разделения данных: так мы избежим утечки данных, c которой ты можешь ознакомиться более подродно по этой ссылке.
2) Почему OHE из sklearn лучше, чем get_dummies(): get_dummies подходит для анализа данных, а для машинного обучения более предпочтителен OHE из sklearn, т.к. он позоволяет избежать ряд ошибок при обучении моделей, в том числе может работать с неизвестными ранее уровнями категорий, которых не было изначально (например, если появится еще одна страна Italy). Еще можно почитать статью с примером о разнице OneHotEncoder() vs get_dummies.
Создание датасетов без пропусков¶
Для заполнения пропусков в датасете посмотрим значения колонки "Tenure", распределение, среднее и медиана.
Создадим два датасета. В одном мы удалим пропуски, а во втором заполним. Забегая вперед, заполнять будем медианным значением. Т.к. среднее стремиться к медиане и такие значения не должны внести больших ошибок. Но в дальнейшем мы будем строить модели и проверять какой набор данных окажется более удачным (с точки зрения предсказании).
print(data['tenure'].unique())
print(data['tenure'].median())
print(data['tenure'].mean())
data['tenure'].hist(bins = 21)
plt.title('Распределение клиентов по годам', color ='brown')
plt.xlabel('Количество лет работы с клиентом')
plt.ylabel('Количество клиентов')
[ 2. 1. 8. 7. 4. 6. 3. 10. 5. 9. 0. nan] 5.0 4.997690023099769
Text(0, 0.5, 'Количество клиентов')
print('Кол-во данных без пропусков:',100/data['age'].count()*data['tenure'].dropna().count(),'%')
df = data.dropna()
df = df.reset_index(drop =True)
df['tenure'] = df['tenure'].astype('int')
df_mean = data.fillna(data['tenure'].median())
df_mean['tenure'] = df_mean['tenure'].astype('int')
df_mean.head()
Кол-во данных без пропусков: 90.91 %
| credit_score | age | tenure | balance | num_of_products | has_cr_card | is_active_member | estimated_salary | exited | geography_germany | geography_spain | gender_male | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 619 | 42 | 2 | 0.00 | 1 | 1 | 1 | 101348.88 | 1 | 0 | 0 | 0 |
| 1 | 608 | 41 | 1 | 83807.86 | 1 | 0 | 1 | 112542.58 | 0 | 0 | 1 | 0 |
| 2 | 502 | 42 | 8 | 159660.80 | 3 | 1 | 0 | 113931.57 | 1 | 0 | 0 | 0 |
| 3 | 699 | 39 | 1 | 0.00 | 2 | 0 | 0 | 93826.63 | 0 | 0 | 0 | 0 |
| 4 | 850 | 43 | 2 | 125510.82 | 1 | 1 | 1 | 79084.10 | 0 | 0 | 1 | 0 |
Вывод:
Просмотрев данные было несколько проблемных моментов для решения нашей задачи:
1) Пропущенные данные (создали два датасета)
2) Категориальные переменные (провели кодирование)
3) Наличие колонок которые не несут для нас полезной информации (удалили)
Исследование задачи¶
Разделим датасеты на выборки¶
feature = df.drop('exited', axis =1) # признаки из датасета с удаленными строками
target = df['exited'] # целевой признак из датасета с удаленными строками
feature_mean = df_mean.drop('exited', axis =1) # признаки из датасета с восстановленными данными
target_mean = df_mean['exited'] # целевой признак из датасета с восстановленными данными
rs = 12345 # переменная для random_state
def split_data(feature, target, rs):
(feature_train,
feature_valid,
target_train,
target_valid) = train_test_split(feature, target, test_size =0.4, random_state = rs)
(feature_valid,
feature_test,
target_valid,
target_test) = train_test_split(feature_valid, target_valid, test_size =0.5, random_state = rs)
return feature_train, feature_valid, feature_test, target_train, target_valid, target_test
(feature_train,
feature_valid,
feature_test,
target_train,
target_valid,
target_test) = split_data(feature,target,rs)
print('Размеры выборок из датасета с удаленными данными')
print(feature_train.shape, 'train')
print(feature_valid.shape, 'valid')
print(feature_test.shape, 'test \n')
(mean_feature_train,
mean_feature_valid,
mean_feature_test,
mean_target_train,
mean_target_valid,
mean_target_test) = split_data(feature_mean, target_mean, rs)
print('Размеры выборок из датасета с восстановленными данными')
print(mean_feature_train.shape, 'train')
print(mean_feature_valid.shape, 'valid')
print(mean_feature_test.shape, 'test')
Размеры выборок из датасета с удаленными данными (5454, 11) train (1818, 11) valid (1819, 11) test Размеры выборок из датасета с восстановленными данными (6000, 11) train (2000, 11) valid (2000, 11) test
Масштабирование признаков¶
pd.options.mode.chained_assignment = None
numeric = ['credit_score', 'age', 'tenure', 'balance', 'num_of_products', 'estimated_salary']
scaler = StandardScaler()
scaler.fit(feature_train[numeric])
feature_train[numeric] = scaler.transform(feature_train[numeric])
feature_valid[numeric] = scaler.transform(feature_valid[numeric])
feature_test[numeric] = scaler.transform(feature_test[numeric])
scaler.fit(mean_feature_train[numeric])
mean_feature_train[numeric] = scaler.transform(mean_feature_train[numeric])
mean_feature_valid[numeric] = scaler.transform(mean_feature_valid[numeric])
mean_feature_test[numeric] = scaler.transform(mean_feature_test[numeric])
P.S. Я попросил твоего куратора передать тебе мое сообщение о масштабировании. Не передали?
Далее прошелся по коду исправил неточности которые возникли, выводы поправил.
print('Процентное содержание отрицательных и положительных признаков train:',
list(target_train.value_counts(normalize =True)*100))
print('Процентное содержание отрицательных и положительных признаков valid:',
list(target_valid.value_counts(normalize =True)*100))
print('Процентное содержание отрицательных и положительных признаков test:',
list(target_test.value_counts(normalize =True)*100))
Процентное содержание отрицательных и положительных признаков train: [79.35460212687936, 20.645397873120647] Процентное содержание отрицательных и положительных признаков valid: [79.26292629262926, 20.737073707370737] Процентное содержание отрицательных и положительных признаков test: [80.70368334249588, 19.29631665750412]
Промежуточные выводы: Дисбаланс данных по положительным и отрицательным значениям присутствует. Вне зависимости от выборки он составляет ~ 80% отрицательных к 20% положительных.
Построим первую модель и посмотрим метрики.(линейная регрессия)¶
f1 = 0
f1_mean = 0
best_iter = 0
best_iter_mean = 0
for iter in np.arange(1000,5000,100):
model = LogisticRegression(solver='liblinear', random_state=rs, max_iter= iter)
model.fit(feature_train, target_train)
mean_model = LogisticRegression(solver='liblinear', random_state=rs, max_iter= iter)
mean_model.fit(mean_feature_train, mean_target_train)
predicted_valid = model.predict(feature_valid)
mean_predicted_valid = mean_model.predict(mean_feature_valid)
probabilities_valid = model.predict_proba(feature_valid)
probabilities_one_valid = probabilities_valid[:, 1]
mean_probabilities_valid = model.predict_proba(mean_feature_valid)
mean_probabilities_one_valid = mean_probabilities_valid[:, 1]
f1_iter = f1_score(target_valid, predicted_valid)
roc_auc = roc_auc_score(target_valid, probabilities_one_valid)
f1_iter_mean = f1_score(mean_target_valid, mean_predicted_valid)
roc_auc_mean = roc_auc_score(mean_target_valid, mean_probabilities_one_valid)
if f1 < f1_iter:
f1 = f1_iter
roc_auc_best = roc_auc
best_iter = iter
if f1_mean < f1_iter_mean:
f1_mean = f1_iter_mean
roc_auc_best_mean = roc_auc_mean
best_iter_mean = iter
print('Датасет с отброшенными строками')
print(best_iter)
print("F1 :", f1)
print('AUC-ROC:', roc_auc_best, '\n')
print(best_iter_mean)
print('Датасет с заполнеными данными по медиане')
print("F1 :", f1_mean)
print('AUC-ROC:', roc_auc_best_mean)
Датасет с отброшенными строками 1000 F1 : 0.30400000000000005 AUC-ROC: 0.7736191158144302 1000 Датасет с заполнеными данными по медиане F1 : 0.33108108108108103 AUC-ROC: 0.7623700240141787
Дерево решений¶
best_result = 0
mean_best_result = 0
roc_auc = 0
depth_find = 0
for depth in range(1, 23):
model = DecisionTreeClassifier(random_state=rs, max_depth = depth)
model.fit(feature_train, target_train)
predictions_valid = model.predict(feature_valid)
result = f1_score(target_valid, predictions_valid)
probabilities_valid = model.predict_proba(feature_valid)
probabilities_one_valid = probabilities_valid[:, 1]
if result > best_result:
depth_find = depth
best_result = result
roc_auc = roc_auc_score(target_valid, probabilities_one_valid)
print('F1-мера:',best_result)
print('AUC-ROC:',roc_auc)
print('Глубина дерева решений:',depth_find)
for depth in range(1, 23):
model = DecisionTreeClassifier(random_state=rs, max_depth = depth)
model.fit(mean_feature_train, mean_target_train)
mean_predictions_valid = model.predict(mean_feature_valid)
mean_result = f1_score(mean_target_valid, mean_predictions_valid)
mean_probabilities_valid = model.predict_proba(mean_feature_valid)
mean_probabilities_one_valid = mean_probabilities_valid[:, 1]
if mean_result > mean_best_result:
depth_find = depth
mean_best_result = mean_result
roc_auc = roc_auc_score(target_valid, probabilities_one_valid)
print('F1-мера:',best_result)
print('AUC-ROC:',roc_auc)
print('Глубина дерева решений:',depth_find)
F1-мера: 0.5764331210191083 AUC-ROC: 0.8346049843812412 Глубина дерева решений: 7 F1-мера: 0.5764331210191083 AUC-ROC: 0.6797786314764468 Глубина дерева решений: 6
Исправь, пожалуйста, это для всех моделей.
Выводы:
Линейная регрессия
1) Мера F1 стремиться к 0.3, это говорит о том что модель плохо предсказывает положительные исходы.
2) Мера AUC-ROC свидельствует о том что наша модель пресказывает лучше случайной, но не значительно.
3) Как бы не меняли геперпараметр "max_iter" значение мер остается прежним, дисбаланс в данных видимо всему причина.
Дерево решений:
1) Мера F1 стремиться к 0.57, это говорит о том что модель плохо предсказывает положительные исходы, что уже лучше линейной. Но этого недостаточно для качественной модели.
2) Мера AUC-ROC свидельствует о том что наша модель пресказывает лучше случайной, но качество этих ответов низкое.
Общий вывод:
Основная проблема исследуемых данных это дисбаланс в классах данных. Отрицательные ответы составляют ~80% соответственно положительных 20%. Для построения качественной модели необходимо масштабировать данные для избавления от выше указанной проблемыили использовать стандарнтный инструмент class_weight
Модель котурую построили выше при дисбалансе в данных, ни как нельзя принять за качественную модель.
Избавляемся от дисбаланса¶
Увеличение выборки¶
Для исправления ситуации с дисбалансом в данных есть 3 варианта.
1) Использование class_weight, встроенная в модели возможность ( определение веса значений)
2) Использовать Upsampling, увеличиваем количество положительных исходов.
3) Использовать Downsampling, уменьшаем кол-во отрицательных ответов.
Class_weight мы будем использовать непосредственно при построении модели, а методы по увеличению и уменьшению выборок требуют отдельного внимания. Пройдемся порядку и создадим дополнительные датасеты с учетом этих методов.
def upsample(feature, target, repeat, rs):
feature_zeros = feature[target == 0]
feature_ones = feature[target == 1]
target_zeros = target[target == 0]
target_ones = target[target == 1]
feature_upsampled = pd.concat([feature_zeros] + [feature_ones] * repeat)
target_upsampled = pd.concat([target_zeros] + [target_ones] * repeat)
feature_upsampled, target_upsampled = shuffle(
feature_upsampled, target_upsampled, random_state=rs)
return feature_upsampled, target_upsampled
Но хочу для увеличения выборки порекомендовать использовать SMOTE.
up_feature_train, up_target_train = upsample(feature_train, target_train, 3, rs)
up_mean_feature_train, up_mean_target_train = upsample(mean_feature_train, mean_target_train, 3, rs)
print(up_feature_train.shape)
print(up_target_train.shape)
print()
print(up_mean_feature_train.shape)
print(up_mean_target_train.shape)
print()
print('Процентное содержание отрицательных и положительных признаков train:',
list(up_target_train.value_counts(normalize =True)*100))
print('Процентное содержание отрицательных и положительных признаков train:',
list(up_mean_target_train.value_counts(normalize =True)*100))
(7706, 11) (7706,) (8392, 11) (8392,) Процентное содержание отрицательных и положительных признаков train: [56.16402803010641, 43.83597196989359] Процентное содержание отрицательных и положительных признаков train: [57.24499523355576, 42.75500476644423]
Уменьшение выборки¶
def downsample(features, target, fraction, rs):
features_zeros = features[target == 0]
features_ones = features[target == 1]
target_zeros = target[target == 0]
target_ones = target[target == 1]
features_downsampled = pd.concat(
[features_zeros.sample(frac=fraction, random_state=rs)] + [features_ones])
target_downsampled = pd.concat(
[target_zeros.sample(frac=fraction, random_state=rs)] + [target_ones])
features_downsampled, target_downsampled = shuffle(
features_downsampled, target_downsampled, random_state=rs)
return features_downsampled, target_downsampled
down_feature_train, down_target_train = downsample(feature_train, target_train, 0.25, rs)
down_mean_feature_train, down_mean_target_train = downsample(mean_feature_train, mean_target_train, 0.25, rs)
print(down_feature_train.shape)
print(down_target_train.shape)
print()
print(down_mean_feature_train.shape)
print(down_mean_target_train.shape)
print()
print('Процентное содержание отрицательных и положительных признаков train:',
list(down_target_train.value_counts(normalize =True)*100))
print('Процентное содержание отрицательных и положительных признаков train:',
list(down_mean_target_train.value_counts(normalize =True)*100))
(2208, 11) (2208,) (2397, 11) (2397,) Процентное содержание отрицательных и положительных признаков train: [50.9963768115942, 49.0036231884058] Процентное содержание отрицательных и положительных признаков train: [50.104297037964116, 49.89570296203588]
Построение моделей¶
Будем строить модели в теле функций для сокращения кода и его удобства использования.
Логистическая регрессия¶
Строим логистическую регрессию. Но в этот раз дополнительно используем атрибут class_weight, а также наши промасштобированные выборки
def log_reg(feature_train, target_train,feature_valid, target_valid, balance, rs):
f1 = 0
f1_mean = 0
best_iter = 0
best_iter_mean = 0
for iter in np.arange(1000,5000,100):
model = LogisticRegression(solver='liblinear', random_state=rs, class_weight= balance, max_iter= iter)
model.fit(feature_train, target_train)
predicted_valid = model.predict(feature_valid)
probabilities_valid = model.predict_proba(feature_valid)
probabilities_one_valid = probabilities_valid[:, 1]
f1_iter = f1_score(target_valid, predicted_valid)
roc_auc = roc_auc_score(target_valid, probabilities_one_valid)
if f1 < f1_iter:
f1 = f1_iter
roc_auc_best = roc_auc
best_iter = iter
return (print("F1 :", f1),
print('AUC-ROC:', roc_auc_best),
print(best_iter))
print("Меры модели набора данных с отброшенными значениями")
log_reg(feature_train, target_train, feature_valid, target_valid, 'balanced', rs)
print("Меры модели набора данных с восстановленными")
log_reg(mean_feature_train, mean_target_train, mean_feature_valid, mean_target_valid, 'balanced', rs)
print('\n')
print("Меры модели набора данных с отброшенными значениями, выборка с upsample")
log_reg(up_feature_train, up_target_train,feature_valid, target_valid, None, rs)
print("Меры модели набора данных с восстановленными, выборка с upsample")
log_reg(up_mean_feature_train, up_mean_target_train,feature_valid, target_valid, None, rs)
print('\n')
print("Меры модели набора данных с отброшенными значениями, выборка с downsample")
log_reg(down_feature_train, down_target_train, feature_valid, target_valid, None, rs)
print("Меры модели набора данных с восстановленными, выборка с downsample")
log_reg(down_mean_feature_train, down_mean_target_train, mean_feature_valid, mean_target_valid, None,rs)
Меры модели набора данных с отброшенными значениями F1 : 0.509731232622799 AUC-ROC: 0.7777884132187896 1000 Меры модели набора данных с восстановленными F1 : 0.4888507718696398 AUC-ROC: 0.7637310291013133 1000 Меры модели набора данных с отброшенными значениями, выборка с upsample F1 : 0.5055679287305122 AUC-ROC: 0.7773576778578094 1000 Меры модели набора данных с восстановленными, выборка с upsample F1 : 0.5028184892897406 AUC-ROC: 0.7785431204752078 1000 Меры модели набора данных с отброшенными значениями, выборка с downsample F1 : 0.5044883303411131 AUC-ROC: 0.7770355467117773 1000 Меры модели набора данных с восстановленными, выборка с downsample F1 : 0.48406546080964685 AUC-ROC: 0.7622883637089505 1000
(None, None, None)
Вывод: Лучший результат для этой модели нам дает набор данных с отброшенными значениями. F1 = 0.51, AUC-ROC = 0.78
Случайный лес¶
def random_forest_classifier(feature_train, target_train, feature_valid, target_valid, balance, rs):
best_model = None
best_est = 0
best_depth = 0
f1_best = 0
roc_auc = 0
for est in range(10, 150, 10):
for depth in range (1, 20):
model = RandomForestClassifier(random_state = rs, n_estimators = est, max_depth = depth, class_weight= balance)
model.fit(feature_train, target_train)
predictions_valid = model.predict(feature_valid)
f1 = f1_score(target_valid, predictions_valid)
probabilities_valid = model.predict_proba(feature_valid)
probabilities_one_valid = probabilities_valid[:, 1]
if f1_best < f1:
best_model = model
best_est = est
best_depth = depth
f1_best = f1
roc_auc = roc_auc_score(target_valid, probabilities_one_valid)
return print(" F1-мера:", f1_best,
"\n ROC", roc_auc,
"\n Количество деревьев:", best_est,
"\n Максимальная глубина:", best_depth)
print("Меры модели набора данных с отброшенными значениями ")
random_forest_classifier(feature_train, target_train, feature_valid, target_valid, 'balanced', rs)
print('\n')
print("Меры модели набора данных с восстановленными")
random_forest_classifier(mean_feature_train, mean_target_train, mean_feature_valid, mean_target_valid, 'balanced', rs)
Меры модели набора данных с отброшенными значениями F1-мера: 0.6497584541062802 ROC 0.8690573706367336 Количество деревьев: 10 Максимальная глубина: 9 Меры модели набора данных с восстановленными F1-мера: 0.6337543053960963 ROC 0.8563398641414477 Количество деревьев: 130 Максимальная глубина: 9
print("Меры модели набора данных с отброшенными значениями, выборка с upsample")
random_forest_classifier(up_feature_train, up_target_train,feature_valid, target_valid, None, rs)
print('\n')
print("Меры набора данных с восстановленными данными, выборка с upsample")
random_forest_classifier(up_mean_feature_train, up_mean_target_train, mean_feature_valid, mean_target_valid, None, rs)
Меры модели набора данных с отброшенными значениями, выборка с upsample F1-мера: 0.6478873239436619 ROC 0.8717715556357304 Количество деревьев: 60 Максимальная глубина: 9 Меры набора данных с восстановленными данными, выборка с upsample F1-мера: 0.6381502890173409 ROC 0.8524179011486884 Количество деревьев: 100 Максимальная глубина: 6
print("Меры модели набора данных с отброшенными значениями, выборка с downsample")
random_forest_classifier(down_feature_train, down_target_train, feature_valid, target_valid, None, rs)
print('\n')
print("Меры модели набора данных с восстановленными, выборка с downsample")
random_forest_classifier(down_mean_feature_train, down_mean_target_train, mean_feature_valid, mean_target_valid, None, rs)
Меры модели набора данных с отброшенными значениями, выборка с downsample F1-мера: 0.6072874493927125 ROC 0.8653234104668692 Количество деревьев: 40 Максимальная глубина: 8 Меры модели набора данных с восстановленными, выборка с downsample F1-мера: 0.6092184368737475 ROC 0.8354975834598564 Количество деревьев: 10 Максимальная глубина: 4
Вывод: Лучший результат для этой модели нам дает набор данных с удаленными пропущенными. F1 = 0.65, AUC-ROC = 0.87 Параметры случайного леса: кол-во деревьев 10, глубина 9. class_weight = 'balanced'
Набор с отброшенными данными и увелечнием положительных ответов дал чуть более слабую модель.
Дерево решений¶
def tree_classifier(feature_train, target_train, feature_valid, target_valid, balance, rs):
best_result = 0
roc_auc = 0
depth_find = 0
roc_auc_best = 0
for depth in range(1, 25):
model = DecisionTreeClassifier(random_state=rs, max_depth = depth, class_weight = balance)
model.fit(feature_train, target_train)
predictions_valid = model.predict(feature_valid)
result = f1_score(target_valid, predictions_valid)
probabilities_valid = model.predict_proba(feature_valid)
probabilities_one_valid = probabilities_valid[:, 1]
roc_auc = roc_auc_score(target_valid, probabilities_one_valid)
fpr, tpr, thresholds = roc_curve(target_valid, probabilities_one_valid)
if result > best_result:
depth_find = depth
best_result = result
roc_auc_best = roc_auc
return print('F1-мера:',best_result,'AUC-ROC:', roc_auc_best,'Глубина дерева', depth_find)
print("Меры модели набора данных с отброшенными значениями ")
tree_classifier(feature_train, target_train, feature_valid, target_valid, 'balanced', rs)
print('\n')
print("Меры набора данных с восстановленными данными")
tree_classifier(mean_feature_train, mean_target_train, mean_feature_valid, mean_target_valid, 'balanced', rs)
Меры модели набора данных с отброшенными значениями F1-мера: 0.5735449735449736 AUC-ROC: 0.8396523192522141 Глубина дерева 5 Меры набора данных с восстановленными данными F1-мера: 0.5963791267305644 AUC-ROC: 0.8310244134068074 Глубина дерева 5
print("Меры модели набора данных с отброшенными значениями, выборка с upsample")
tree_classifier(up_feature_train, up_target_train,feature_valid, target_valid, None, rs)
print('\n')
print("Меры модели набора данных с восстановленными, выборка с upsample")
tree_classifier(up_mean_feature_train, up_mean_target_train,mean_feature_valid, mean_target_valid, None, rs)
Меры модели набора данных с отброшенными значениями, выборка с upsample F1-мера: 0.5929526123936817 AUC-ROC: 0.8400158672598789 Глубина дерева 5 Меры модели набора данных с восстановленными, выборка с upsample F1-мера: 0.586374695863747 AUC-ROC: 0.8229097986317362 Глубина дерева 5
print("Меры модели набора данных с отброшенными значениями, выборка с downsample")
tree_classifier(down_feature_train, down_target_train, feature_valid, target_valid, None, rs)
print('\n')
print("Меры модели набора данных с восстановленными, выборка с downsample")
tree_classifier(down_mean_feature_train, down_mean_target_train, mean_feature_valid, mean_target_valid, None, rs)
Меры модели набора данных с отброшенными значениями, выборка с downsample F1-мера: 0.5636704119850188 AUC-ROC: 0.8343500405885245 Глубина дерева 6 Меры модели набора данных с восстановленными, выборка с downsample F1-мера: 0.5943204868154158 AUC-ROC: 0.8241029464247908 Глубина дерева 5
Вывод: Лучший результат для этой модели нам дает набор данных с восстановленными данными. F1 = 0.6, AUC-ROC = 0.83
Построение лучшей модели и тестовая проверка¶
Из выше сделанных выводов, наша лучшая модель это случайный лес(классификация), с параметрами n_estimators = 10, max_depth = 9, class_weight= 'balanced'. Датасет же используется с удаленными пропущенными значениями.
Построим модель уже без функции, с заданными параметрами. Проверим на тестовой выборке.
model = RandomForestClassifier(random_state = rs, n_estimators = 10, max_depth = 9, class_weight= 'balanced')
model.fit(feature_train, target_train)
predictions_valid = model.predict(feature_valid)
predictions_test = model.predict(feature_test)
f1 = f1_score(target_valid, predictions_valid)
probabilities_valid = model.predict_proba(feature_valid)
probabilities_one_valid = probabilities_valid[:, 1]
roc_auc = roc_auc_score(target_valid, probabilities_one_valid)
f1_test = f1_score(target_test, predictions_test)
probabilities_valid_second = model.predict_proba(feature_test)
probabilities_one_valid_second= probabilities_valid_second[:, 1]
roc_auc_test = roc_auc_score(target_test, probabilities_one_valid_second)
fpr, tpr, thresholds = roc_curve(target_valid, probabilities_one_valid)
plt.plot(fpr, tpr, 'b', label = 'AUC = %0.2f' % roc_auc)
plt.title('AUC-ROC', color ='red')
plt.legend(loc = 'lower right')
plt.plot([0, 1], [0, 1],'r--')
plt.xlim([0, 1])
plt.ylim([0, 1])
plt.ylabel('True Positive Rate')
plt.xlabel('False Positive Rate')
plt.show()
target_pred_constant = pd.Series(target_test * 0)
print('Качество пресказаний константной модели:',accuracy_score(target_test, target_pred_constant))
print('Качество пресказаний нашей модели:',accuracy_score(target_test, predictions_test))
print('\n Валидационная выборка выборка')
print('f1-мера',f1, 'ROC-AUC', roc_auc)
print('\n Тестовая выборка')
print('f1-мера',f1_test,'ROC-AUC', roc_auc_test)
Качество пресказаний константной модели: 0.8070368334249588 Качество пресказаний нашей модели: 0.8306761957119296 Валидационная выборка выборка f1-мера 0.6497584541062802 ROC-AUC 0.8690573706367336 Тестовая выборка f1-мера 0.5936675461741425 ROC-AUC 0.8450291886940389
Итоговый вывод:
1) Лучшая модель случайный лес(классификации). Параметры его: кол-во деревьев 10, максимальная глубина 9,
class_weight= 'balanced'.
2) Лучше всего себя показали данные с отброшенными значениями. Количество положительных ответов увеличено в 3 раза.
3) Показатели метрик для тестовой выборки следующие: f1-мера = 0.59, ROC-AUC = 0.85.
4)Показатели метрик для валидационной выборки следующие: f1-мера = 0.65, ROC-AUC = 0.87.
5) Проверка на адекватнось дала положительный результат Accuracy константной модели 0.8, а нашей модели
тот же показатель равен 0.83.
В целом у нас получилась очень хорошая модель. Мера F1 говорит нам о том что положительные ответы модель предсказывает хорошо.
- 1) Корректно выполнить кодирование признаков.
- 2) Правильно посчитать ROC-AUC.
- 3) Исследовать не менее 2 моделей без учета дисбаланса.
- 4) Добавить перебор гиперпараметров.
- 5) Скорректировать разделы и их названия.
- 6) Выполнить ап(даун)семплинг только для трейна.
- 7) Для тестирования выбрать одну модель.
- 8) Скорректировать выводы.
Осталось совсем немного, буду ждать твою работу на повторное ревью 🙂
- 1) Избежать дамми-ловушки.
- 2) Добавить масштабирование численных признаков.
Желаю успехов и удачи!
Чек-лист готовности проекта¶
Поставьте 'x' в выполненных пунктах. Далее нажмите Shift+Enter.
- Jupyter Notebook открыт
- Весь код выполняется без ошибок
- Ячейки с кодом расположены в порядке исполнения
- Выполнен шаг 1: данные подготовлены
- Выполнен шаг 2: задача исследована
- Исследован баланс классов
- Изучены модели без учёта дисбаланса
- Написаны выводы по результатам исследования
- Выполнен шаг 3: учтён дисбаланс
- Применено несколько способов борьбы с дисбалансом
- Написаны выводы по результатам исследования
- Выполнен шаг 4: проведено тестирование
- Удалось достичь F1-меры не менее 0.59
- Исследована метрика AUC-ROC