Отток клиентов

Из «Бета-Банка» стали уходить клиенты. Каждый месяц. Немного, но заметно. Банковские маркетологи посчитали: сохранять текущих клиентов дешевле, чем привлекать новых.

Нужно спрогнозировать, уйдёт клиент из банка в ближайшее время или нет. Вам предоставлены исторические данные о поведении клиентов и расторжении договоров с банком.

Постройте модель с предельно большим значением F1-меры. Чтобы сдать проект успешно, нужно довести метрику до 0.59. Проверьте F1-меру на тестовой выборке самостоятельно.

Дополнительно измеряйте AUC-ROC, сравнивайте её значение с F1-мерой.

Источник данных: https://www.kaggle.com/barelydedicated/bank-customer-churn-modeling

(... загрузка данных и библиотек ...)

1. Подготовка данных

In [6]:
# просмотр первых трех строк новой таблицы
df.head(3)
Out[6]:
CreditScore Geography Gender Age Tenure Balance NumOfProducts HasCrCard IsActiveMember EstimatedSalary 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
In [7]:
# общие характеристики и проверка на наличие выбивающихся значений
df.describe()
Out[7]:
CreditScore Age Tenure Balance NumOfProducts HasCrCard IsActiveMember EstimatedSalary Exited
count 10000.000000 10000.000000 9091.000000 10000.000000 10000.000000 10000.00000 10000.000000 10000.000000 10000.000000
mean 650.528800 38.921800 4.997690 76485.889288 1.530200 0.70550 0.515100 100090.239881 0.203700
std 96.653299 10.487806 2.894723 62397.405202 0.581654 0.45584 0.499797 57510.492818 0.402769
min 350.000000 18.000000 0.000000 0.000000 1.000000 0.00000 0.000000 11.580000 0.000000
25% 584.000000 32.000000 2.000000 0.000000 1.000000 0.00000 0.000000 51002.110000 0.000000
50% 652.000000 37.000000 5.000000 97198.540000 1.000000 1.00000 1.000000 100193.915000 0.000000
75% 718.000000 44.000000 7.000000 127644.240000 2.000000 1.00000 1.000000 149388.247500 0.000000
max 850.000000 92.000000 10.000000 250898.090000 4.000000 1.00000 1.000000 199992.480000 1.000000
  • Первое, на что обращаешь внимание, это 25% клиентов с нулевым балансом на счете. Это достаточно много. Если эти люди все еще клиенты банка, то они не приносят денег банку. Потенциально, эти люди могут покинуть банк, если еще не ушли.
  • Крупных выбивающихся значений не обнаружено (есть клиент возрастом 92 года)
  • Есть странное минимальное значение предполагаемой зарплаты 11.58 при средней и близкой к ней медианной чуть больше 100 000
  • Интересно, что для кредитного рейтинга, количества недвижимости и предполагаемой зарплаты медиана и среднее очень близки по значению 5, что указывает на симметричное распределение данных в этих колонках
In [8]:
# оценка корреляций признаков
df.corr()
Out[8]:
CreditScore Age Tenure Balance NumOfProducts HasCrCard IsActiveMember EstimatedSalary Exited
CreditScore 1.000000 -0.003965 -0.000062 0.006268 0.012238 -0.005458 0.025651 -0.001384 -0.027094
Age -0.003965 1.000000 -0.013134 0.028308 -0.030680 -0.011721 0.085472 -0.007201 0.285323
Tenure -0.000062 -0.013134 1.000000 -0.007911 0.011979 0.027232 -0.032178 0.010520 -0.016761
Balance 0.006268 0.028308 -0.007911 1.000000 -0.304180 -0.014858 -0.010084 0.012797 0.118533
NumOfProducts 0.012238 -0.030680 0.011979 -0.304180 1.000000 0.003183 0.009612 0.014204 -0.047820
HasCrCard -0.005458 -0.011721 0.027232 -0.014858 0.003183 1.000000 -0.011866 -0.009933 -0.007138
IsActiveMember 0.025651 0.085472 -0.032178 -0.010084 0.009612 -0.011866 1.000000 -0.011421 -0.156128
EstimatedSalary -0.001384 -0.007201 0.010520 0.012797 0.014204 -0.009933 -0.011421 1.000000 0.012097
Exited -0.027094 0.285323 -0.016761 0.118533 -0.047820 -0.007138 -0.156128 0.012097 1.000000

Из матрицы корреляций видно, что возраст и баланс на счету коррелируют с тем, что покинет клиент банк или нет. То есть чем старше клиент и у него больше денег на счету тем больше шансов, что он покинет банк. И наоборот, чем более активен клиент, тем менее вероятно он покинет банк (эти параметры антикоррелируют).

In [9]:
# визуализация корреляций
sns.pairplot(data=df, hue = 'Exited')
Out[9]:
<seaborn.axisgrid.PairGrid at 0x7f749c8f5cf8>
  • Визуализация нам подтвердила, что более возрастные клиенты охотнее покидают банк. Из графика виден сдвиг распределений с максимумум близким к 47 годам, для тех кто покинул банк.
  • Интересно, что клиенты с нулевым балансом продолжают оставаться в банке, а клиенты с деньгами как раз уходят - это тревожный знак для банка
  • Клиенты у кого 2 и более продукта охотнее остаются в банке, это указывает на то , что они довольны предоставляемыми банком услугами. Это же относится к активности клиентов (активный клиент остается, у неактивного больше шансов покинуть банк)
  • Кредитный рейтинг, наличие недвижимости и кредитной карты, предполагаемая зарплата не сильно влияют на решение клиента уйти или остаться.

Посмотрим данные столбика с пустыми значениями "Наличие недвижимости":

  • В этом столбике около 9% пустых значений.
  • Данные в этом столбике, как было отмечено ранее, симметрично распределены.
  • Сам этот признак влияет на то, покинет ли клиент банк при наличии недвижимости больше 5 (то есть больше чем стреднее и медиана) и судя по графику влияет незначительно, то есть сам по себе этот признак не является критически важным
In [10]:
# просмотр уникальных значений этого признака
df['Tenure'].value_counts()
Out[10]:
1.0     952
2.0     950
8.0     933
3.0     928
5.0     927
7.0     925
4.0     885
9.0     882
6.0     881
10.0    446
0.0     382
Name: Tenure, dtype: int64
In [11]:
# построим график
sns.countplot( x= 'Tenure', hue = 'Exited', data = df)
Out[11]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f749c91d780>

Количество недвижимости(за исключением 0 и 10) распределено равномерно

Изучим чуть подробнее влияние наличия недвижимости

In [12]:
#создадим дополнительную колонку "наличие значений в колонке недвижимости" (потом ее уберем)
df['has_tenure'] = df['Tenure'].notnull()
In [13]:
sns.countplot( x= 'has_tenure', hue = 'Exited', data = df)
Out[13]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f7494ce1390>

Из гистограммы видно, что соотношение клиент ушел (1) или остался в банке (0) примерно одинаковое 1 к 4 и для пустых и непустых значений в колонке "tenure". Поэтому эти пустые значения не являются критичными для данной задачи.

Рассмотрим нулевые значение в колонке "Баланс", которые обратили на себя внимание Их достаточно большое количество (25%). Посмотрим на гистограмму этой колонки

In [14]:
df.hist('Balance', bins=50 )
Out[14]:
array([[<matplotlib.axes._subplots.AxesSubplot object at 0x7f7494c4a6d8>]],
      dtype=object)
  • Из гистограммы видно, что "Баланс" состоит из суперпозиции двух распределений: 25% просто нулевые значения и остальной баланс клиентов похож на нормальное распределение. И похоже эти нули реальные, а не какая-то аномалия в данных - клиенты просто вывели свои деньги со счетов, оставаясь при это все еще клиентами банка.

Посмотрим искажают ли как-то пустые значения колонки "tenure" значения колонки "Баланс" (вдруг все нулевые или вообще без нулей)

In [15]:
plt.hist(df[df["has_tenure"]==True]["Balance"], color = 'limegreen', alpha=0.5, 
         edgecolor="black",  bins = 20) 
plt.hist(df[df["has_tenure"]==False]["Balance"], color = 'indianred', alpha=0.5, 
         edgecolor="black", bins = 20)
red_patch = mpatches.Patch(color='limegreen', label='True')
green_patch = mpatches.Patch(color='indianred', label='False')
plt.legend(handles=[red_patch, green_patch])

plt.show()

Посмотрим как влияет уход клиента на "Баланс"

In [16]:
plt.hist(df[df["Exited"]==1]["Balance"], color = 'limegreen', alpha=0.5, 
         edgecolor="black",  bins = 50) 
plt.hist(df[df["Exited"]==0]["Balance"], color = 'indianred', alpha=0.5, 
         edgecolor="black", bins = 50)
red_patch = mpatches.Patch(color='limegreen', label='1')
green_patch = mpatches.Patch(color='indianred', label='0')
plt.legend(handles=[red_patch, green_patch])

plt.show()

Влияние пустых значений колонки "tenure" на кредитный рейтинг

In [17]:
plt.hist(df[df["has_tenure"]==True]["CreditScore"], color = 'limegreen', alpha=0.5, 
         edgecolor="black",  bins = 20) 
plt.hist(df[df["has_tenure"]==False]["CreditScore"], color = 'indianred', alpha=0.5, 
         edgecolor="black", bins = 20)
red_patch = mpatches.Patch(color='limegreen', label='True')
green_patch = mpatches.Patch(color='indianred', label='False')
plt.legend(handles=[red_patch, green_patch])

plt.show()

Распределение пустых значений колонки "tenure" достаточно симметрично, за исключением самых высоких значений кредитного рейтинга

Влияние ухода клиента на кредитный рейтинг

In [18]:
plt.hist(df[df["Exited"]==1]["CreditScore"], color = 'limegreen', alpha=0.5, 
         edgecolor="black",  bins = 20) 
plt.hist(df[df["Exited"]==0]["CreditScore"], color = 'indianred', alpha=0.5, 
         edgecolor="black", bins = 20)
red_patch = mpatches.Patch(color='limegreen', label='1')
green_patch = mpatches.Patch(color='indianred', label='0')
plt.legend(handles=[red_patch, green_patch])

plt.show()

Клиенты с низким кредитным рейтингом (меньше 420) покинули банк

In [19]:
# удалим созданную для анализа колонку "has_tenure"
df = df.drop('has_tenure', axis=1)

Cтратегии обработки пустых значений колонки 'Tenure':

  • Первый подход: убрать те строки в таблице, для которых "Tenure" содержит пустые значения. Получится удаление около 9% данных
  • второй вариант - удалить всю колонку. Судя по матрице корреляций и по графикам распределения для ушедших и оставшихся клиентов - этот признак не является ключевым как, например "Баланс" или "Возраст".
  • третий: попробовать заполнить:
    • случайным образом от 2 до 9 с равной вероятностью случайным образом заполнить целыми числами от 2 до 9, для 0 и 10 заполнять с вероятностью в два раза меньше
    • попробовать построить самую простую ML модель, опираясь на соседние колонки, но в таком случае эти данные не будут независимыми
    • так как данные в этой колонке распределены симметрично (медиана очень близка значению среднего), то можно заменить пустые значения медианой (5). В этом случае ни среднее, ни значение медианы не изменится, но будет перекос в количестве значений 5 в таблице.
  • Любое заполнение создаст дополнительный "статистический шум", что отразится на качестве модели. Поэтому ниже будем спользовать первые два подхода: удаление всей колонки и удаление 9% пустых строк

Обратимся к нашей таблице теперь как к таблице с признаками для построения моделей машинного обучения

In [20]:
# просмотр признаков
df.head()
Out[20]:
CreditScore Geography Gender Age Tenure Balance NumOfProducts HasCrCard IsActiveMember EstimatedSalary 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

Посмотрим на значения колонки с целевым признаком "Exited"

In [21]:
df['Exited'].value_counts()
Out[21]:
0    7963
1    2037
Name: Exited, dtype: int64

В этом столбце видно, что клиентов, которые ушли (1) гораздо меньше чем тех, кто остался (0). Примерно 1 к 4. То есть наблюдается дисбаланс классов. Учтем его позже

В обновленной таблице две колонки с категориальными признаками: "Gender" and "Geography".

In [22]:
# уникальные значения колонки "Пол"
df['Gender'].value_counts()
Out[22]:
Male      5457
Female    4543
Name: Gender, dtype: int64

Два пола: мужской и женский

In [23]:
# уникальные значения колонки "Geography"
df['Geography'].value_counts()
Out[23]:
France     5014
Germany    2509
Spain      2477
Name: Geography, dtype: int64

Три страны: Фанция, Германия и Испания

Преобразуем категориальные признаки в численные с момощью техники прямого кодирования, или отображения (One-Hot Encoding) и удалим по одному столбцу из вновь созданных с помощью этой техники (drop_first=True), чтобы ихбежать дамми-ловушку

In [24]:
df_ohe = pd.get_dummies(df, drop_first=True)

1 вариант. Уберем пустые строки из таблицы

In [25]:
# удаление строк из таблицы
df_no_null = df_ohe.dropna()
In [26]:
# перенумерация
df_no_null = df_no_null.reset_index(drop = True)
In [27]:
df_no_null.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 9091 entries, 0 to 9090
Data columns (total 12 columns):
CreditScore          9091 non-null int64
Age                  9091 non-null int64
Tenure               9091 non-null float64
Balance              9091 non-null float64
NumOfProducts        9091 non-null int64
HasCrCard            9091 non-null int64
IsActiveMember       9091 non-null int64
EstimatedSalary      9091 non-null float64
Exited               9091 non-null int64
Geography_Germany    9091 non-null uint8
Geography_Spain      9091 non-null uint8
Gender_Male          9091 non-null uint8
dtypes: float64(3), int64(6), uint8(3)
memory usage: 666.0 KB
In [28]:
df_no_null.tail(3)
Out[28]:
CreditScore Age Tenure Balance NumOfProducts HasCrCard IsActiveMember EstimatedSalary Exited Geography_Germany Geography_Spain Gender_Male
9088 516 35 10.0 57369.61 1 1 1 101699.77 0 0 0 1
9089 709 36 7.0 0.00 1 0 1 42085.58 1 0 0 0
9090 772 42 3.0 75075.31 2 1 0 92888.52 1 1 0 1
In [29]:
#Делаем отдельные датасеты для признаков features и целевого признака target
target = df_no_null['Exited']
features = df_no_null.drop('Exited', axis=1)
In [30]:
# 20% данных оставляем для тестовой выборки
features_train_val, features_test, target_train_val, target_test = train_test_split(
    features, target, test_size=0.20, random_state=12345)
In [31]:
# проверка размера тестовой выборки после разбиения данных
features_test.shape
Out[31]:
(1819, 11)
In [32]:
# создадим две выборки обучающую и валидационную
features_train, features_val, target_train, target_val = train_test_split(
    features_train_val, target_train_val, test_size=0.25, random_state=12345)
In [33]:
# проверка размера валидационной выборки после разбиения данных
features_val.shape
Out[33]:
(1818, 11)
In [34]:
# проверка размера обучающей выборки после разбиения данных
features_train.shape
Out[34]:
(5454, 11)

Проведем стандартизацию данных, воспользуясь методом масштабирования. На первом шаге создадим список с названиями колонок, которые требуют масштабирования.

In [35]:
# список названий колонок с признаками, которые требуют стандартизации
numeric = ['CreditScore', 'Age', 'Balance', 'EstimatedSalary']
In [36]:
# создадим объект структуры для стандартизации данных
scaler = StandardScaler()
In [37]:
# настроим его на обучающих данных, т. е. вычислим среднее и дисперсию
scaler.fit(features_train[numeric])  
# преобразуем обучающую выборку функцией transform()
features_train[numeric] = scaler.transform(features_train[numeric])
In [38]:
# преобразуем валидационную выборку функцией transform()
features_val[numeric] = scaler.transform(features_val[numeric])

# преобразуем тестовую выборку функцией transform()
features_test[numeric] = scaler.transform(features_test[numeric])
In [39]:
# просмотр как выглядит обучающая выборка после всех преобразований
features_train.head()
Out[39]:
CreditScore Age Tenure Balance NumOfProducts HasCrCard IsActiveMember EstimatedSalary Geography_Germany Geography_Spain Gender_Male
3353 -0.203819 0.471273 6.0 0.786022 2 0 0 -0.357205 0 1 0
6176 -0.357513 -0.384930 1.0 -1.230577 2 1 1 -1.671048 0 0 0
4020 0.175290 -0.289797 3.0 -1.230577 2 1 0 -1.119181 0 0 1
535 0.349476 1.708010 5.0 1.379462 1 0 0 -1.569064 1 0 0
1661 0.902771 -0.289797 9.0 -1.230577 1 0 1 1.543790 0 0 1

2. Исследование задачи

Мы выяснили, что целевой признак несбалансирован. Дисбаланс примерно 1 к 4. На первом этапе обучим модели без учета диссбаланса классов. Возьмем две модели машинного обучения: Случайный лес и Логистическую регрессию

Случайный лес

In [40]:
# Построим объект класса RandomForestClassifier, используя параметры по умолчанию
model_rf = RandomForestClassifier(random_state=12345) 
In [41]:
# запускаем обучение на обучающей выборке
model_rf.fit(features_train, target_train)

# предсказываем значения целевого признака валидационных данных
predicted_val_rf = model_rf.predict(features_val)
In [42]:
# Посчитаем вероятность классов
probabilities_val_rf = model_rf.predict_proba(features_val)
# Значения вероятностей класса «1» 
probabilities_one_val_rf = probabilities_val_rf[:, 1]

#Выясним, как сильно наша модель отличается от случайной, посчитаем площадь под ROC-кривой — AUC-ROC 
auc_roc_rf = roc_auc_score(target_val, probabilities_one_val_rf)
In [43]:
# делаем отчет по основным метрикам модели
print('Отчет по метрикам\n {}'.format(classification_report(target_val, predicted_val_rf)))
#Площадь по ROC-кривой
print('Площадь под кривой ошибок (ROC-кривой): {:.3f}'.format(auc_roc_rf))
Отчет по метрикам
               precision    recall  f1-score   support

           0       0.86      0.96      0.91      1452
           1       0.71      0.41      0.52       366

    accuracy                           0.85      1818
   macro avg       0.79      0.68      0.71      1818
weighted avg       0.83      0.85      0.83      1818

Площадь под кривой ошибок (ROC-кривой): 0.795

Логистическая регрессия

In [44]:
# создаем объекта класса LogisticRegression c гиперпараметрами по умолчанию
model_lr = LogisticRegression(random_state=12345, solver = 'liblinear')
In [45]:
# запускаем обучение на обучающей выборке
model_lr.fit(features_train, target_train)

# предсказываем значения целевого признака валидационных данных
predicted_val_lr = model_lr.predict(features_val)
In [46]:
# Посчитаем вероятность классов
probabilities_val_lr = model_lr.predict_proba(features_val)
# Значения вероятностей класса «1» 
probabilities_one_val_lr = probabilities_val_lr[:, 1]

#Выясним, как сильно наша модель отличается от случайной, посчитаем площадь под ROC-кривой — AUC-ROC 
auc_roc_lr = roc_auc_score(target_val, probabilities_one_val_lr)
In [47]:
# делаем отчет по основным метрикам модели
print('Отчет по метрикам\n {}'.format(classification_report(target_val, predicted_val_lr)))
#Площадь по ROC-кривой
print('Площадь под кривой ошибок (ROC-кривой): {:.3f}'.format(auc_roc_lr))
Отчет по метрикам
               precision    recall  f1-score   support

           0       0.83      0.97      0.89      1452
           1       0.61      0.20      0.30       366

    accuracy                           0.81      1818
   macro avg       0.72      0.58      0.59      1818
weighted avg       0.78      0.81      0.77      1818

Площадь под кривой ошибок (ROC-кривой): 0.773

Промежуточные выводы

  • Из двух моделей с гиперпараметрами по умолчанию наибольшое значение F1-меры показывает "Случайный лес" (0.52), но значение мектрики не доходит до требуемых 0.59.
  • Логистическая регрессия показывает достаточно высокое значение метрики точности (0.81) и AUC-ROC (0.773), но плохо предсказывает целевой признак со значением класса 1 (F1 мера равна 0.3 для класса 1)

3. Борьба с дисбалансом

Как было отмечено выше, наблюдается дисбаланс классов целевого признака. Мы можем его убрать с помощью нескольких подходов:

  • Взвешивание классов: придадим объектам редкого класса (1) больший вес
  • Увеличение выборки (upsampling)
  • Уменьшение выборки (downsamling)

Balanced. Взвешивание классов

Случайный лес

In [48]:
# Построим объект класса RandomForestClassifier с учетом взвешивания class_weight='balanced'
model_rf_b = RandomForestClassifier(random_state=12345, class_weight='balanced') 
In [49]:
# запускаем обучение на обучающей выборке
model_rf_b.fit(features_train, target_train)
# предсказываем значения целевого признака валидационных данных
predicted_val_rf_b = model_rf_b.predict(features_val)
In [50]:
# Посчитаем вероятность классов
probabilities_val_rf_b = model_rf_b.predict_proba(features_val)
# Значения вероятностей класса «1» 
probabilities_one_val_rf_b = probabilities_val_rf_b[:, 1]

#Выясним, как сильно наша модель отличается от случайной, посчитаем площадь под ROC-кривой — AUC-ROC 
auc_roc_rf_b = roc_auc_score(target_val, probabilities_one_val_rf_b)
In [51]:
# делаем отчет по основным метрикам модели
print('Отчет по метрикам\n {}'.format(classification_report(target_val, predicted_val_rf_b)))
#Площадь по ROC-кривой
print('Площадь под кривой ошибок (ROC-кривой): {:.3f}'.format(auc_roc_rf_b))
Отчет по метрикам
               precision    recall  f1-score   support

           0       0.86      0.97      0.91      1452
           1       0.75      0.37      0.50       366

    accuracy                           0.85      1818
   macro avg       0.80      0.67      0.70      1818
weighted avg       0.84      0.85      0.83      1818

Площадь под кривой ошибок (ROC-кривой): 0.800
  • "Взвешивание" немного ухудшило F1 метрику (0.5 против 0.52 для класса "1")
  • Метрика точности осталась без изменений: 0.85
  • метрика AUC-ROC (площадь под кривой ошибок) незначительно улучшилась с 0.795 до 0.8

Логистическая регрессия

In [52]:
# создаем объекта класса LogisticRegression c гиперпараметрами по умолчанию и с учетом взвешивания классов
model_lr_b = LogisticRegression(random_state=12345, solver = 'liblinear', class_weight='balanced')
In [53]:
# запускаем обучение на обучающей выборке
model_lr_b.fit(features_train, target_train)

# предсказываем значения целевого признака валидационных данных
predicted_val_lr_b = model_lr_b.predict(features_val)
In [54]:
# Посчитаем вероятность классов
probabilities_val_lr_b = model_lr_b.predict_proba(features_val)
# Значения вероятностей класса «1» 
probabilities_one_val_lr_b = probabilities_val_lr_b[:, 1]

#Выясним, как сильно наша модель отличается от случайной, посчитаем площадь под ROC-кривой — AUC-ROC 
auc_roc_lr_b = roc_auc_score(target_val, probabilities_one_val_lr_b)
In [55]:
# делаем отчет по основным метрикам модели
print('Отчет по метрикам\n {}'.format(classification_report(target_val, predicted_val_lr_b)))
#Площадь по ROC-кривой
print('Площадь под кривой ошибок (ROC-кривой): {:.3f}'.format(auc_roc_lr_b))
Отчет по метрикам
               precision    recall  f1-score   support

           0       0.90      0.73      0.80      1452
           1       0.39      0.69      0.50       366

    accuracy                           0.72      1818
   macro avg       0.64      0.71      0.65      1818
weighted avg       0.80      0.72      0.74      1818

Площадь под кривой ошибок (ROC-кривой): 0.775
  • "Взвешивание" значительно улучшило значение F1-меры для логистической регрессиии с 0.3 до 0.5
  • При этом пострадала метрика точности - уменьшилась с 0.81 до 0.72
  • Метрика AUC-ROC немного увеличилась с 0.773 до 0.775

Промежуточные выводы:

  • Использование "balanced" немного ухудшило F1-меру для модели "Случайного леса"
  • Взвешивание классов значительно улучшило значение F1-меры для логистической регрессии (с 0.3 без взвешивания классов, до 0.5 со взвешиванием). При этом площадь под кривой ошибок поменялась незначительно(0.773 против 0.775)

Upsampling

Создадим функцию upsample, которая будет преобразовывать нашу обучающую выборку:

  • На первом этапе разделим выборку с признаками, на те у которых целевой признак равен 0 и те, для которых целевой признак равен единице;
  • Потом скопируем несколько раз признаки, для которых целевой признак 1.
  • Создадим новую объединенную выборку и перемешаем в ней данные.

8 нулей и 2 единицы: соотношение классов 1 к 4. Будем подавать коээфициент 4 на вход нашей функции чтобы количество нулей и 1 стало одинаковым в новой выборке

In [58]:
# тестируем функцию
features_tested_ups, target_tested_ups = upsample(features_tested, target_tested, 4)
In [59]:
# проверяем как отработала функция
target_tested_ups.value_counts()
Out[59]:
1    8
0    8
Name: Exited, dtype: int64

Получилось сбалансировать классы: 8 нулей и 8 едениц.

Применим функцию ко всей нашей обучающей выборке. Как мы видели выше диссбаланс классов примерно 1 к 4, поэтому коэффициент repeat будет равен 4 в нашем случае

In [60]:
features_upsampled, target_upsampled = upsample(features_train, target_train, 4)
In [61]:
features_upsampled.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 8811 entries, 1832 to 4253
Data columns (total 11 columns):
CreditScore          8811 non-null float64
Age                  8811 non-null float64
Tenure               8811 non-null float64
Balance              8811 non-null float64
NumOfProducts        8811 non-null int64
HasCrCard            8811 non-null int64
IsActiveMember       8811 non-null int64
EstimatedSalary      8811 non-null float64
Geography_Germany    8811 non-null uint8
Geography_Spain      8811 non-null uint8
Gender_Male          8811 non-null uint8
dtypes: float64(5), int64(3), uint8(3)
memory usage: 645.3 KB
In [62]:
# соотношение классов до применения функции upsampled
target_train.value_counts()
Out[62]:
0    4335
1    1119
Name: Exited, dtype: int64
In [63]:
# соотношение классов после применения функции
target_upsampled.value_counts()
Out[63]:
1    4476
0    4335
Name: Exited, dtype: int64

Классы стали сбалансированными.

Случайный лес

Проведем подбор параметров для "Случайного леса". Так как гиперпараметров для этого алгоритма достаточно много, то сфокусируемся на трех: n_estimamators - количество деревьев, max_features - число признаков, по которым ищется разбиение и min_samples_leaf

In [64]:
# создадим словарь с гипперпараметрами и их значениями
grid = {'n_estimators': range(10, 210, 10), 'max_features': [2, 4, 8], 'min_samples_leaf': [2,3,4,5]} 

# пустой список, в который будут записываться значения метрики F1
f1_rf = []

# пустой список, в который будет записоваться значение метрики AUC-ROC
auc_roc_rf = []


# создаем объекта класса RandomForestClassifier
rfc = RandomForestClassifier(random_state=12345) 

# Организуем цикл по сетке параметров. 
for g in ParameterGrid(grid):
    
    #распаковывваем словарь и передаем его элемент в функцию .set_params 
    rfc.set_params(**g)  
    # запускаем обучение на обучающей выборке
    rfc.fit(features_upsampled, target_upsampled)
    
    
    # предсказываем значения целевого признака валидационных данных
    predictions_val = rfc.predict(features_val)
    
    # F1-мера - среднее гармоническое полноты и точности. Важна для правильного прогнозирования класса 1
    f1_score_rf = f1_score(target_val, predictions_val)
    
    # Посчитаем вероятность классов
    probabilities_val_rfc = rfc.predict_proba(features_val)
    # Значения вероятностей класса «1» 
    probabilities_one_val_rfc = probabilities_val_rfc[:, 1]

    
    # AUC-ROC площадь под кривой ошибок, показывает насколько модель далека/близка от/к случайной (случайная 0.5)
    auc_roc_score_rf = roc_auc_score(target_val, probabilities_one_val_rfc)
    
    # записываем значение метрик в соответствующие списки
    f1_rf.append(f1_score_rf)
    auc_roc_rf.append(auc_roc_score_rf)
    #print (f1_score_rf, auc_roc_score_rf)
    
# Лучшие значения гиперпараметров для валидационной выборки 
best_idx = np.argmax(f1_rf) # выбираем с максимальным значением F1-меры
print(f1_rf[best_idx], auc_roc_rf[best_idx], ParameterGrid(grid)[best_idx])
0.61340206185567 0.8399456562645833 {'n_estimators': 30, 'min_samples_leaf': 5, 'max_features': 4}
  • Лучшая модель "Случайного леса" обладает параметрами: n_estimators = 30, max_features = 4, min_samples_leaf = 5. При таких параметрах F1-мера равна 0.613 на валидационной выборке (при требуемых 0.59). AUC-ROC при этом равна 0.84, что выше, чем без применения подхода "upsampling" (0.795)

Логистическая регрессия

Выбререм несколько гиперпараметров логистической регрессии для насройки модели: параметр регуляризации C, dual - оптимизация прямая или двойственная, max_iter - максимальное количество итераций. Выберем алгоритм использования в задаче оптимизации solver = 'liblinear' - рекомендованный slkearn библиотекой для небольших датасетов.

In [65]:
# создадим словарь с гипперпараметрами и их значениями
grid = {'C': [1.0, 2.5, 5, 10, 100, 1000], 'dual': [True,False], 'max_iter': [100,110,120,130,140]} # , np.logspace(-1, 4, 10)'max_features': [2, 4, 8]
# пустой список, в который будут записываться значения метрики F1
f1_lr = []

# пустой список, в который будет записоваться значение метрики AUC-ROC
auc_roc_lr = []


# создаем объекта класса LogisticRegression

lr = LogisticRegression(random_state=12345, solver = 'liblinear') #solver = 'liblinear'
# Организуем цикл по сетке параметров. 
for g in ParameterGrid(grid):
    
    #распаковывваем словарь и передаем его элемент в функцию .set_params 
    lr.set_params(**g)  
    # запускаем обучение на обучающей выборке
    lr.fit(features_upsampled, target_upsampled)
    
    
    # предсказываем значения целевого признака валидационных данных
    predictions_val = lr.predict(features_val)
    
    # F1-мера - среднее гармоническое полноты и точности. Важна для правильного прогнозирования класса 1
    f1_score_lr = f1_score(target_val, predictions_val)
 
    # Посчитаем вероятность классов
    probabilities_val_lr = lr.predict_proba(features_val)
    # Значения вероятностей класса «1» 
    probabilities_one_val_lr = probabilities_val_lr[:, 1]

    # AUC-ROC площадь под кривой ошибок, показывает насколько модель далека/близка от/к случайной (случайная 0.5)
    auc_roc_score_lr = roc_auc_score(target_val, probabilities_one_val_lr)
    
    # записываем значение метрик в соответствующие списки
    f1_lr.append(f1_score_lr)
    auc_roc_lr.append(auc_roc_score_lr)
    #print (f1_score_lr, auc_roc_score_lr)
    
# Лучшие значения гиперпараметров для валидационной выборки 
best_idx = np.argmax(f1_lr) # выбираем с максимальным значением F1-меры
print(f1_lr[best_idx], auc_roc_lr[best_idx], ParameterGrid(grid)[best_idx])
0.5016501650165016 0.7769216005058033 {'max_iter': 120, 'dual': True, 'C': 2.5}

Техника "upsampling" незначительно помогла с улучшением значения F1-меры для модели логистической регрессии: F1 стала 0.502 против 0.5 при применениии техники "взешивания" классов, при этом значение AUC-ROC стало немного лучше: 0.777 против 0.773

Downsampling. Уменьшение выборки

Попробуем третий вариант решения проблемы несбалансированных классов: данные с признаками, для котрых целевой признак 1 (меньший класс) оставим без изменения, а данные для котрых целевой признак 0 сократим согласно пропорции дисбаланса. Создадим функцию Downsample

In [67]:
features_downsampled, target_downsampled = downsample(features_train, target_train, 0.25)
In [68]:
target_downsampled.value_counts()
Out[68]:
1    1119
0    1084
Name: Exited, dtype: int64

Классы примерно сбалансированы теперь.

Случайный лес

In [69]:
# создадим словарь с гипперпараметрами и их значениями
grid = {'n_estimators': range(10, 210, 10), 'max_features': [2, 4, 8], 'min_samples_leaf': [2,3,4,5]} 

# пустой список, в который будут записываться значения метрики F1
f1_rf = []

# пустой список, в который будет записоваться значение метрики AUC-ROC
auc_roc_rf = []


# создаем объекта класса RandomForestClassifier
rfc = RandomForestClassifier(random_state=12345) 

# Организуем цикл по сетке параметров. 
for g in ParameterGrid(grid):
    
    #распаковывваем словарь и передаем его элемент в функцию .set_params 
    rfc.set_params(**g)  
    # запускаем обучение на обучающей выборке
    rfc.fit(features_downsampled, target_downsampled)
    
    
    # предсказываем значения целевого признака валидационных данных
    predictions_val = rfc.predict(features_val)
    
    # F1-мера - среднее гармоническое полноты и точности. Важна для правильного прогнозирования класса 1
    f1_score_rf = f1_score(target_val, predictions_val)

    # Посчитаем вероятность классов
    probabilities_val_rfc = rfc.predict_proba(features_val)
    # Значения вероятностей класса «1» 
    probabilities_one_val_rfc = probabilities_val_rfc[:, 1]

    # AUC-ROC площадь под кривой ошибок, показывает насколько модель далека/близка от/к случайной (случайная 0.5)
    auc_roc_score_rf = roc_auc_score(target_val, probabilities_one_val_rfc)
    
    # записываем значение метрик в соответствующие списки
    f1_rf.append(f1_score_rf)
    auc_roc_rf.append(auc_roc_score_rf)
    #print (f1_score_rf, auc_roc_score_rf)
    
# Лучшие значения гиперпараметров для валидационной выборки 
best_idx = np.argmax(f1_rf) # выбираем с максимальным значением F1-меры
print(f1_rf[best_idx], auc_roc_rf[best_idx], ParameterGrid(grid)[best_idx])
0.6015200868621065 0.8499111833687094 {'n_estimators': 180, 'min_samples_leaf': 3, 'max_features': 4}

Лучшая модель "Случайного леса" обладает параметрами: n_estimators = 180, max_features = 4, min_samples_leaf = 3. При таких параметрах F1-мера равна 0.602 на валидационной выборке (при требуемых 0.59) и меньше, чем при применеии техники upsampling. AUC-ROC при этом равна 0.85, что несколько больше значения, чем с применением подхода "upsampling"

Логистическая регрессия

In [70]:
# создадим словарь с гипперпараметрами и их значениями
grid = {'C': [1.0, 2.5, 5, 10, 100, 1000], 'dual': [True,False], 'max_iter': [100,110,120,130,140]} # , np.logspace(-1, 4, 10)'max_features': [2, 4, 8]
# пустой список, в который будут записываться значения метрики F1
f1_lr = []

# пустой список, в который будет записоваться значение метрики AUC-ROC
auc_roc_lr = []


# создаем объекта класса LogisticRegression

lr = LogisticRegression(random_state=12345, solver = 'liblinear') #solver = 'liblinear'
# Организуем цикл по сетке параметров. 
for g in ParameterGrid(grid):
    
    #распаковывваем словарь и передаем его элемент в функцию .set_params 
    lr.set_params(**g)  
    # запускаем обучение на обучающей выборке
    lr.fit(features_downsampled, target_downsampled)
    
    
    # предсказываем значения целевого признака валидационных данных
    predictions_val = lr.predict(features_val)
    
    # F1-мера - среднее гармоническое полноты и точности. Важна для правильного прогнозирования класса 1
    f1_score_lr = f1_score(target_val, predictions_val)
 

    # Посчитаем вероятность классов
    probabilities_val_lr = lr.predict_proba(features_val)
    # Значения вероятностей класса «1» 
    probabilities_one_val_lr = probabilities_val_lr[:, 1]

    # AUC-ROC площадь под кривой ошибок, показывает насколько модель далека/близка от/к случайной (случайная 0.5)
    auc_roc_score_lr = roc_auc_score(target_val, probabilities_one_val_lr)
    
    # записываем значение метрик в соответствующие списки
    f1_lr.append(f1_score_lr)
    auc_roc_lr.append(auc_roc_score_lr)
    #print (f1_score_lr, auc_roc_score_lr)
    
# Лучшие значения гиперпараметров для валидационной выборки 
best_idx = np.argmax(f1_lr) # выбираем с максимальным значением F1-меры
print(f1_lr[best_idx], auc_roc_lr[best_idx], ParameterGrid(grid)[best_idx])
0.5034280117531832 0.7765113880985715 {'max_iter': 100, 'dual': True, 'C': 5}

Техника "downsampling" незначительно помогла с улучшением значения F1-меры для модели логистической регрессии: F1 стала 0.503 против 0.502 при применениии техники "взешивания" классов, при этом значение AUC-ROC стало немного лучше: 0.777 против 0.775

Выводы:

  • Максимальное значение F1-меры получилось для модели "Случайного леса" с применением техники "upsampling" с параметрами: n_estimators = 190, max_features = 4. При таких параметрах F1-мера равна 0.613 на валидационной выборке (при требуемых 0.59). AUC-ROC при этом равна 0.745

4. Тестирование модели

Проведем тестирование этой лучшей модели

In [71]:
rfc_best = RandomForestClassifier(random_state=12345, n_estimators = 30, max_features = 4, min_samples_leaf = 5) 
In [72]:
# запускаем обучение на обучающей выборке
rfc_best.fit(features_upsampled, target_upsampled)
    
Out[72]:
RandomForestClassifier(bootstrap=True, class_weight=None, criterion='gini',
                       max_depth=None, max_features=4, max_leaf_nodes=None,
                       min_impurity_decrease=0.0, min_impurity_split=None,
                       min_samples_leaf=5, min_samples_split=2,
                       min_weight_fraction_leaf=0.0, n_estimators=30,
                       n_jobs=None, oob_score=False, random_state=12345,
                       verbose=0, warm_start=False)
In [73]:
#  проверка работы модели на тестовой выборке
predictions_test_rfc_best = rfc_best.predict(features_test)
In [74]:
# Посчитаем вероятность классов
probabilities_test_rfc_best = rfc_best.predict_proba(features_test)
# Значения вероятностей класса «1» 
probabilities_one_val_rfc_best = probabilities_test_rfc_best[:, 1]

#Выясним, как сильно наша модель отличается от случайной, посчитаем площадь под ROC-кривой — AUC-ROC 
auc_roc_rfc_best = roc_auc_score(target_test, probabilities_one_val_rfc_best)
In [75]:
# делаем отчет по основным метрикам модели
print('Отчет по метрикам\n {}'.format(classification_report(target_test, predictions_test_rfc_best)))
print('AUC-ROC площадь под кривой ошибок: {:.3f}'.format(auc_roc_rfc_best))
Отчет по метрикам
               precision    recall  f1-score   support

           0       0.91      0.87      0.89      1450
           1       0.57      0.68      0.62       369

    accuracy                           0.83      1819
   macro avg       0.74      0.77      0.75      1819
weighted avg       0.84      0.83      0.83      1819

AUC-ROC площадь под кривой ошибок: 0.858
  • На тестовой выборке значение F1-меры равно 0.62;
  • При этом AUC-ROC площадь под кривой ошибок: 0.840

II вариант. Уберем колонку с пустыми значениями

In [76]:
df_no_tenure = df_ohe.drop('Tenure', axis=1)
df_no_tenure.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 10000 entries, 0 to 9999
Data columns (total 11 columns):
CreditScore          10000 non-null int64
Age                  10000 non-null int64
Balance              10000 non-null float64
NumOfProducts        10000 non-null int64
HasCrCard            10000 non-null int64
IsActiveMember       10000 non-null int64
EstimatedSalary      10000 non-null float64
Exited               10000 non-null int64
Geography_Germany    10000 non-null uint8
Geography_Spain      10000 non-null uint8
Gender_Male          10000 non-null uint8
dtypes: float64(2), int64(6), uint8(3)
memory usage: 654.4 KB
In [77]:
df_no_tenure.head(3)
Out[77]:
CreditScore Age Balance NumOfProducts HasCrCard IsActiveMember EstimatedSalary Exited Geography_Germany Geography_Spain Gender_Male
0 619 42 0.00 1 1 1 101348.88 1 0 0 0
1 608 41 83807.86 1 0 1 112542.58 0 0 1 0
2 502 42 159660.80 3 1 0 113931.57 1 0 0 0
In [78]:
#Делаем отдельные датасеты для признаков features и целевого признака target
target_no_tenure = df_no_tenure['Exited']
features_no_tenure = df_no_tenure.drop('Exited', axis=1)
In [79]:
# 20% данных оставляем для тестовой выборки
features_no_tenure_train_val, features_no_tenure_test, target_no_tenure_train_val, target_no_tenure_test = train_test_split(
    features_no_tenure, target_no_tenure, test_size=0.20, random_state=12345)
In [80]:
# проверка размера тестовой выборки после разбиения данных
features_no_tenure_test.shape
Out[80]:
(2000, 10)
In [81]:
# создадим две выборки обучающую и валидационную
features_no_tenure_train, features_no_tenure_val, target_no_tenure_train, target_no_tenure_val = train_test_split(
    features_no_tenure_train_val, target_no_tenure_train_val, test_size=0.25, random_state=12345)
In [82]:
# проверка размера валидационной выборки после разбиения данных
features_no_tenure_val.shape
Out[82]:
(2000, 10)
In [83]:
# проверка размера обучающей выборки после разбиения данных
features_no_tenure_train.shape
Out[83]:
(6000, 10)

Проведем стандартизацию данных, воспользуясь методом масштабирования. Объект структуры для стандартизации данных мы уже создавали выше scaler = StandardScaler(). Поэтому будем его испотльзовать, для настройки новых обучающих данных

In [84]:
# настроим его на обучающих данных, т. е. вычислим среднее и дисперсию
scaler.fit(features_no_tenure_train[numeric])  
# преобразуем обучающую выборку функцией transform()
features_no_tenure_train[numeric] = scaler.transform(features_no_tenure_train[numeric])
In [85]:
# преобразуем валидационную выборку функцией transform()
features_no_tenure_val[numeric] = scaler.transform(features_no_tenure_val[numeric])

# преобразуем тестовую выборку функцией transform()
features_no_tenure_test[numeric] = scaler.transform(features_no_tenure_test[numeric])

Так как присутствует дисбаланс классов, и выше мы увидели, что техника upsample для "Случайного леса" работает лучше всего в борьбе с этим дисбалансом, то применим ее далее

In [86]:
features_no_tenure_upsampled, target_no_tenure_upsampled = upsample(features_no_tenure_train, target_no_tenure_train, 4)
In [87]:
# проверяем как отработала функция, ихбавились от дисбаланса или нет
target_no_tenure_upsampled.value_counts()
Out[87]:
1    4876
0    4781
Name: Exited, dtype: int64
In [88]:
features_no_tenure_upsampled.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 9657 entries, 471 to 2444
Data columns (total 10 columns):
CreditScore          9657 non-null float64
Age                  9657 non-null float64
Balance              9657 non-null float64
NumOfProducts        9657 non-null int64
HasCrCard            9657 non-null int64
IsActiveMember       9657 non-null int64
EstimatedSalary      9657 non-null float64
Geography_Germany    9657 non-null uint8
Geography_Spain      9657 non-null uint8
Gender_Male          9657 non-null uint8
dtypes: float64(4), int64(3), uint8(3)
memory usage: 631.9 KB
In [89]:
# создадим словарь с гипперпараметрами и их значениями
grid = {'n_estimators': range(10, 210, 10), 'max_features': [2, 4, 8], 'min_samples_leaf': [2,3,4,5]} 

# пустой список, в который будут записываться значения метрики F1
f1_rf = []

# пустой список, в который будет записоваться значение метрики AUC-ROC
auc_roc_rf = []


# создаем объекта класса RandomForestClassifier
rfc = RandomForestClassifier(random_state=12345) 

# Организуем цикл по сетке параметров. 
for g in ParameterGrid(grid):
    
    #распаковывваем словарь и передаем его элемент в функцию .set_params 
    rfc.set_params(**g)  
    # запускаем обучение на обучающей выборке
    rfc.fit(features_no_tenure_upsampled, target_no_tenure_upsampled)
    
    
    # предсказываем значения целевого признака валидационных данных
    predictions_no_tenure_val = rfc.predict(features_no_tenure_val)
    
    # F1-мера - среднее гармоническое полноты и точности. Важна для правильного прогнозирования класса 1
    f1_score_rf = f1_score(target_no_tenure_val, predictions_no_tenure_val)

    # Посчитаем вероятность классов
    probabilities_no_tenure_val = rfc.predict_proba(features_no_tenure_val)
    # Значения вероятностей класса «1» 
    probabilities_no_tenure_one_val = probabilities_no_tenure_val[:, 1]

    
    # AUC-ROC площадь под кривой ошибок, показывает насколько модель далека/близка от/к случайной (случайная 0.5)
    auc_roc_score_rf = roc_auc_score(target_no_tenure_val, probabilities_no_tenure_one_val)
    
    # записываем значение метрик в соответствующие списки
    f1_rf.append(f1_score_rf)
    auc_roc_rf.append(auc_roc_score_rf)
    #print (f1_score_rf, auc_roc_score_rf)
    
# Лучшие значения гиперпараметров для валидационной выборки 
best_idx = np.argmax(f1_rf) # выбираем с максимальным значением F1-меры
print(f1_rf[best_idx], auc_roc_rf[best_idx], ParameterGrid(grid)[best_idx])
0.6166281755196306 0.8504321122077063 {'n_estimators': 60, 'min_samples_leaf': 5, 'max_features': 2}
  • Лучшая модель получилась с другими параметрами на данных без учета колонки "tenure": n_estimators = 60, min_samples_leaf = 5, max_features = 2
  • F1-мера = 0.62, AUC-ROC = 0.85

Проведем проверку нашей модели на тестовой выборке

In [90]:
rfc_no_tenure_best = RandomForestClassifier(random_state=12345, n_estimators = 60, max_features = 2, min_samples_leaf = 5) 
In [91]:
# запускаем обучение на обучающей выборке
rfc_no_tenure_best.fit(features_no_tenure_upsampled, target_no_tenure_upsampled)
    
Out[91]:
RandomForestClassifier(bootstrap=True, class_weight=None, criterion='gini',
                       max_depth=None, max_features=2, max_leaf_nodes=None,
                       min_impurity_decrease=0.0, min_impurity_split=None,
                       min_samples_leaf=5, min_samples_split=2,
                       min_weight_fraction_leaf=0.0, n_estimators=60,
                       n_jobs=None, oob_score=False, random_state=12345,
                       verbose=0, warm_start=False)
In [92]:
# предсказываем значения на тестовой выборке
predictions_no_tenure_test_rfc_best = rfc_no_tenure_best.predict(features_no_tenure_test)
In [93]:
# Посчитаем вероятность классов
probabilities_no_tenure_test_rfc_best = rfc_no_tenure_best.predict_proba(features_no_tenure_test)
# Значения вероятностей класса «1» 
probabilities_no_tenure_one_test_rfc_best = probabilities_no_tenure_test_rfc_best[:, 1]

#Выясним, как сильно наша модель отличается от случайной, посчитаем площадь под ROC-кривой — AUC-ROC 
auc_roc_no_tenure_rfc_best = roc_auc_score(target_no_tenure_test, probabilities_no_tenure_one_test_rfc_best)
In [94]:
# делаем отчет по основным метрикам модели
print('Отчет по метрикам\n {}'.format(classification_report(target_no_tenure_test, predictions_no_tenure_test_rfc_best)))
print('AUC-ROC площадь под кривой ошибок: {:.3f}'.format(auc_roc_no_tenure_rfc_best))
Отчет по метрикам
               precision    recall  f1-score   support

           0       0.91      0.87      0.89      1573
           1       0.58      0.68      0.63       427

    accuracy                           0.83      2000
   macro avg       0.75      0.77      0.76      2000
weighted avg       0.84      0.83      0.83      2000

AUC-ROC площадь под кривой ошибок: 0.860

Значение F1-меры на тестовой выборке равно 0.63, значение площади под кривой ошибок: 0.86

Выводы:

  • лучшие значения F1-меры получили для модели "Случайный лес"
  • В борьбе с дисбалансом лучше всего себя проявила техника upsample для моделей случайный лес
  • Для логистической регрессии явного фаворита в борьбе с дисбалансом нет: любой подход для решения проблемы с дисбалансом классов значительно повышал значение F1-меры
  • В данной задаче подход по удалению одной колонки с пустыми значениями с признаком, который не является сильно важным, привел к чуть более точным результатам, чем удаление строк с пустыми значениями: F1-мера 0.62 для лучшей модели на валидационной выборке и 0.63 для этой же модели на тестовом сете
In [ ]: