8 Нейронні мережі. Deep Learning. Класифікація. Кредитний скоринг
Курс: “Математичне моделювання в R”
8.1 Набір даних
Джерело: https://www.kaggle.com/barelydedicated/bank-customer-churn-modeling.
Завантажимо файли з даними:
Цей датасет містить такі стовпці:
RowNumber- номер рядка.CustomerId- ідентифікатор клієнта.Surname- прізвище клієнта.CreditScore- кредитний рейтинг клієнта.Geography- регіон.Gender- стать.Age- вік.Tenure- час обслуговування цього клієнта в банку.IsActiveMember- активний клієнт, виконує операції.EstimatedSalary- заробітна плата.Exited- залишив/не залишив банк.
Інформація про клієнтів:
'data.frame': 10000 obs. of 11 variables:
$ RowNumber : int 1 2 3 4 5 6 7 8 9 10 ...
$ CustomerId : int 15634602 15647311 15619304 15701354 15737888 15574012 15592531 15656148 15792365 15592389 ...
$ Surname : chr "Hargrave" "Hill" "Onio" "Boni" ...
$ CreditScore : int 619 608 502 699 850 645 822 376 501 684 ...
$ Geography : chr "France" "Spain" "France" "France" ...
$ Gender : chr "Female" "Female" "Female" "Female" ...
$ Age : int 42 41 42 39 43 44 50 29 44 27 ...
$ Tenure : int 2 1 8 1 2 8 7 4 4 2 ...
$ IsActiveMember : int 1 1 0 0 1 0 1 0 1 1 ...
$ EstimatedSalary: num 101349 112543 113932 93827 79084 ...
$ Exited : int 1 0 1 0 0 1 0 1 0 0 ...
| RowNumber | CustomerId | Surname | CreditScore | Geography | Gender | Age | Tenure | IsActiveMember | EstimatedSalary | Exited | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| <int> | <int> | <chr> | <int> | <chr> | <chr> | <int> | <int> | <int> | <dbl> | <int> | |
| 1 | 1 | 15634602 | Hargrave | 619 | France | Female | 42 | 2 | 1 | 101348.88 | 1 |
| 2 | 2 | 15647311 | Hill | 608 | Spain | Female | 41 | 1 | 1 | 112542.58 | 0 |
| 3 | 3 | 15619304 | Onio | 502 | France | Female | 42 | 8 | 0 | 113931.57 | 1 |
| 4 | 4 | 15701354 | Boni | 699 | France | Female | 39 | 1 | 0 | 93826.63 | 0 |
| 5 | 5 | 15737888 | Mitchell | 850 | Spain | Female | 43 | 2 | 1 | 79084.10 | 0 |
| 6 | 6 | 15574012 | Chu | 645 | Spain | Male | 44 | 8 | 0 | 149756.71 | 1 |
8.2 Оглядовий аналіз даних
8.2.1 Таблиця customers
Оглянемо дані колекції customers візуально та за допомогою крос-таблиць:
Кредитний рейтинг (CreditScore):
ggplot(customers, aes(x=CreditScore, fill=factor(Exited))) +
geom_histogram(binwidth = 10, alpha=0.7) + theme_bw() 
Регіон/країна (Geography):
ggplot(customers, aes(x=Geography, fill=factor(Exited))) +
geom_bar(position = "stack") + theme_bw()
Cell Contents
|-------------------------|
| N |
| Chi-square contribution |
| N / Row Total |
| N / Col Total |
| N / Table Total |
|-------------------------|
Total Observations in Table: 10000
| customers$Exited
customers$Geography | 0 | 1 | Row Total |
--------------------|-----------|-----------|-----------|
France | 4204 | 810 | 5014 |
| 11.188 | 43.736 | |
| 0.838 | 0.162 | 0.501 |
| 0.528 | 0.398 | |
| 0.420 | 0.081 | |
--------------------|-----------|-----------|-----------|
Germany | 1695 | 814 | 2509 |
| 45.927 | 179.537 | |
| 0.676 | 0.324 | 0.251 |
| 0.213 | 0.400 | |
| 0.170 | 0.081 | |
--------------------|-----------|-----------|-----------|
Spain | 2064 | 413 | 2477 |
| 4.251 | 16.617 | |
| 0.833 | 0.167 | 0.248 |
| 0.259 | 0.203 | |
| 0.206 | 0.041 | |
--------------------|-----------|-----------|-----------|
Column Total | 7963 | 2037 | 10000 |
| 0.796 | 0.204 | |
--------------------|-----------|-----------|-----------|
Стать (Gender):
Cell Contents
|-------------------------|
| N |
| Chi-square contribution |
| N / Row Total |
| N / Col Total |
| N / Table Total |
|-------------------------|
Total Observations in Table: 10000
| customers$Exited
customers$Gender | 0 | 1 | Row Total |
-----------------|-----------|-----------|-----------|
Female | 3404 | 1139 | 4543 |
| 12.611 | 49.298 | |
| 0.749 | 0.251 | 0.454 |
| 0.427 | 0.559 | |
| 0.340 | 0.114 | |
-----------------|-----------|-----------|-----------|
Male | 4559 | 898 | 5457 |
| 10.499 | 41.041 | |
| 0.835 | 0.165 | 0.546 |
| 0.573 | 0.441 | |
| 0.456 | 0.090 | |
-----------------|-----------|-----------|-----------|
Column Total | 7963 | 2037 | 10000 |
| 0.796 | 0.204 | |
-----------------|-----------|-----------|-----------|
Вік (Age):
ggplot(customers, aes(x=Age, fill=factor(Exited))) +
geom_histogram(binwidth = 1, alpha=0.7) + theme_bw() 
Час обслуговування клієнта (Tenure):
ggplot(customers, aes(x=Tenure, fill=factor(Exited))) +
geom_histogram(binwidth = 1, alpha=0.7) + theme_bw() 
Активність (IsActiveMember):
Cell Contents
|-------------------------|
| N |
| Chi-square contribution |
| N / Row Total |
| N / Col Total |
| N / Table Total |
|-------------------------|
Total Observations in Table: 10000
| customers$Exited
customers$IsActiveMember | 0 | 1 | Row Total |
-------------------------|-----------|-----------|-----------|
0 | 3547 | 1302 | 4849 |
| 25.577 | 99.984 | |
| 0.731 | 0.269 | 0.485 |
| 0.445 | 0.639 | |
| 0.355 | 0.130 | |
-------------------------|-----------|-----------|-----------|
1 | 4416 | 735 | 5151 |
| 24.077 | 94.122 | |
| 0.857 | 0.143 | 0.515 |
| 0.555 | 0.361 | |
| 0.442 | 0.073 | |
-------------------------|-----------|-----------|-----------|
Column Total | 7963 | 2037 | 10000 |
| 0.796 | 0.204 | |
-------------------------|-----------|-----------|-----------|
Заробітна плата (EstimatedSalary):
ggplot(customers, aes(x=EstimatedSalary, fill=factor(Exited))) +
geom_histogram(binwidth = 1000, alpha=0.7) + theme_bw() 
Exited:
Cell Contents
|-------------------------|
| N |
| N / Table Total |
|-------------------------|
Total Observations in Table: 10000
| 0 | 1 |
|-----------|-----------|
| 7963 | 2037 |
| 0.796 | 0.204 |
|-----------|-----------|
8.2.2 Таблиця cards
Інформація про карти клієнта:
'data.frame': 13545 obs. of 4 variables:
$ CustomerId : int 15634602 15647311 15647311 15619304 15619304 15701354 15701354 15737888 15737888 15574012 ...
$ CardNo : int 684618 357092 802678 888594 987103 507476 928960 370210 935036 581042 ...
$ IsCreditCard: int 1 0 0 1 0 0 0 1 0 1 ...
$ Balance : num 5 41922 41913 79846 79859 ...
| CustomerId | CardNo | IsCreditCard | Balance | |
|---|---|---|---|---|
| <int> | <int> | <int> | <dbl> | |
| 1 | 15634602 | 684618 | 1 | 5.00 |
| 2 | 15647311 | 357092 | 0 | 41921.93 |
| 3 | 15647311 | 802678 | 0 | 41912.93 |
| 4 | 15619304 | 888594 | 1 | 79846.40 |
| 5 | 15619304 | 987103 | 0 | 79859.40 |
| 6 | 15701354 | 507476 | 0 | 28.00 |
CustomerId CardNo IsCreditCard Balance
Min. :15565701 Min. :100231 Min. :0.0000 Min. : 0
1st Qu.:15628272 1st Qu.:323587 1st Qu.:0.0000 1st Qu.: 24
Median :15691011 Median :545073 Median :0.0000 Median : 52991
Mean :15690848 Mean :548414 Mean :0.4688 Mean : 51096
3rd Qu.:15752816 3rd Qu.:774943 3rd Qu.:1.0000 3rd Qu.: 77276
Max. :15815690 Max. :999985 Max. :1.0000 Max. :221549
8.2.3 Таблиця products
Інформація про продукти клієнта:
'data.frame': 13862 obs. of 2 variables:
$ CustomerId : int 15634602 15647311 15619304 15619304 15619304 15701354 15701354 15737888 15574012 15574012 ...
$ ProductName: chr "PROD_1" "PROD_1" "PROD_1" "PROD_2" ...
| CustomerId | ProductName | |
|---|---|---|
| <int> | <chr> | |
| 1 | 15634602 | PROD_1 |
| 2 | 15647311 | PROD_1 |
| 3 | 15619304 | PROD_1 |
| 4 | 15619304 | PROD_2 |
| 5 | 15619304 | PROD_3 |
| 6 | 15701354 | PROD_1 |
8.3 Feature engeniering
Сформуємо додаткові змінні на основі наявних даних:
Balance- сума по усіх картах клієнат.NumOfProducts- кількість продуктів банку, які використовує клієнт.HasCreditCard- dummy-змінна, наявність кредитної карти у клієнта.
8.3.1 Показник NumOfProducts
Варто звернути увагу, що генерувати нові фічі можна із використанням можлиовстей агрегації та фільтрування даних (наприклад, методи пакету dplyr) або за допомогою простих алгоритмічних структур (цикли, розгалуження).
Для R використання циклів для подібних задач є не досить хорошим рішенням, адже код виглядає складно і виконується повільно. Проте ми напишемо для требування і пояснення приклади з використанням обох підходів.
Імперативний підхід до програмування:
# Створюємо пусту змінну
customers$NumOfProducts <- c(0)
for(i in 1:nrow(customers))
{
id <- customers$CustomerId[i]
prods <- subset(products, CustomerId == id)
customers$NumOfProducts[i] <- nrow(prods)
}
head(customers, 4)| RowNumber | CustomerId | Surname | CreditScore | Geography | Gender | Age | Tenure | IsActiveMember | EstimatedSalary | Exited | NumOfProducts | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| <int> | <int> | <chr> | <int> | <chr> | <chr> | <int> | <int> | <int> | <dbl> | <int> | <dbl> | |
| 1 | 1 | 15634602 | Hargrave | 619 | France | Female | 42 | 2 | 1 | 101348.88 | 1 | 1 |
| 2 | 2 | 15647311 | Hill | 608 | Spain | Female | 41 | 1 | 1 | 112542.58 | 0 | 1 |
| 3 | 3 | 15619304 | Onio | 502 | France | Female | 42 | 8 | 0 | 113931.57 | 1 | 3 |
| 4 | 4 | 15701354 | Boni | 699 | France | Female | 39 | 1 | 0 | 93826.63 | 0 | 2 |
Декларативний приклад коду:
suppressMessages(library(dplyr))
customers_tmp <- customers |>
left_join(products |>
group_by(CustomerId) |>
mutate(NumOfProducts = n()) |>
select(CustomerId, NumOfProducts) |> distinct(), by = "CustomerId")
head(customers_tmp, 4)
# P.S. Це набагато швидше| RowNumber | CustomerId | Surname | CreditScore | Geography | Gender | Age | Tenure | IsActiveMember | EstimatedSalary | Exited | NumOfProducts.x | NumOfProducts.y | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| <int> | <int> | <chr> | <int> | <chr> | <chr> | <int> | <int> | <int> | <dbl> | <int> | <dbl> | <int> | |
| 1 | 1 | 15634602 | Hargrave | 619 | France | Female | 42 | 2 | 1 | 101348.88 | 1 | 1 | 1 |
| 2 | 2 | 15647311 | Hill | 608 | Spain | Female | 41 | 1 | 1 | 112542.58 | 0 | 1 | 1 |
| 3 | 3 | 15619304 | Onio | 502 | France | Female | 42 | 8 | 0 | 113931.57 | 1 | 3 | 3 |
| 4 | 4 | 15701354 | Boni | 699 | France | Female | 39 | 1 | 0 | 93826.63 | 0 | 2 | 2 |
8.3.2 Показник HasCreditCard
customers <- customers |>
left_join(cards |>
group_by(CustomerId) |>
summarise(HasCreditCard = ifelse(sum(IsCreditCard) == 0, 0, 1)) |>
select(CustomerId, HasCreditCard) |> distinct(), by = "CustomerId")
head(customers, 4)| RowNumber | CustomerId | Surname | CreditScore | Geography | Gender | Age | Tenure | IsActiveMember | EstimatedSalary | Exited | NumOfProducts | HasCreditCard | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| <int> | <int> | <chr> | <int> | <chr> | <chr> | <int> | <int> | <int> | <dbl> | <int> | <dbl> | <dbl> | |
| 1 | 1 | 15634602 | Hargrave | 619 | France | Female | 42 | 2 | 1 | 101348.88 | 1 | 1 | 1 |
| 2 | 2 | 15647311 | Hill | 608 | Spain | Female | 41 | 1 | 1 | 112542.58 | 0 | 1 | 0 |
| 3 | 3 | 15619304 | Onio | 502 | France | Female | 42 | 8 | 0 | 113931.57 | 1 | 3 | 1 |
| 4 | 4 | 15701354 | Boni | 699 | France | Female | 39 | 1 | 0 | 93826.63 | 0 | 2 | 0 |
8.3.3 Показник Balance
customers <- customers |>
left_join(cards |>
group_by(CustomerId) |>
mutate(Balance = round(sum(Balance))) |>
select(CustomerId, Balance) |> distinct(), by = "CustomerId")
head(customers, 4)| RowNumber | CustomerId | Surname | CreditScore | Geography | Gender | Age | Tenure | IsActiveMember | EstimatedSalary | Exited | NumOfProducts | HasCreditCard | Balance | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| <int> | <int> | <chr> | <int> | <chr> | <chr> | <int> | <int> | <int> | <dbl> | <int> | <dbl> | <dbl> | <dbl> | |
| 1 | 1 | 15634602 | Hargrave | 619 | France | Female | 42 | 2 | 1 | 101348.88 | 1 | 1 | 1 | 5 |
| 2 | 2 | 15647311 | Hill | 608 | Spain | Female | 41 | 1 | 1 | 112542.58 | 0 | 1 | 0 | 83835 |
| 3 | 3 | 15619304 | Onio | 502 | France | Female | 42 | 8 | 0 | 113931.57 | 1 | 3 | 1 | 159706 |
| 4 | 4 | 15701354 | Boni | 699 | France | Female | 39 | 1 | 0 | 93826.63 | 0 | 2 | 0 | 47 |
Звичано код можна поєднати в 1 запит.
8.3.4 Нормалізація даних
Видалимо зайві покзники, що заважатимуть будувати моделі:
Варто звернути увагу, що інженереія нових фіч завершена не до кінця і у нас ще присутні параметри, що мають нечислові значення Geography + Gender. Спробуйте замінити їх, наприклад, на dummy-змінні або використати бінінг.
Для скорочення часу на вивчення матеріали ми скористаємося звичайним приведенням даних до числового типу:
'data.frame': 10000 obs. of 11 variables:
$ CreditScore : int 619 608 502 699 850 645 822 376 501 684 ...
$ Geography : num 1 3 1 1 3 3 1 2 1 1 ...
$ Gender : num 1 1 1 1 1 2 2 1 2 2 ...
$ Age : int 42 41 42 39 43 44 50 29 44 27 ...
$ Tenure : int 2 1 8 1 2 8 7 4 4 2 ...
$ IsActiveMember : int 1 1 0 0 1 0 1 0 1 1 ...
$ EstimatedSalary: num 101349 112543 113932 93827 79084 ...
$ Exited : int 1 0 1 0 0 1 0 1 0 0 ...
$ NumOfProducts : num 1 1 3 2 1 2 2 4 2 1 ...
$ HasCreditCard : num 1 0 1 0 1 1 1 1 0 1 ...
$ Balance : num 5 83835 159706 47 125571 ...
Використовуючи функцію scale() нормалізуємо дані:
| CreditScore | Geography | Gender | Age | Tenure | IsActiveMember | EstimatedSalary | NumOfProducts | HasCreditCard | Balance | Exited | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| <dbl> | <dbl> | <dbl> | <dbl> | <dbl> | <dbl> | <dbl> | <dbl> | <dbl> | <dbl> | <int> | |
| 1 | -0.32620511 | -0.9018411 | -1.0959327 | 0.293502747 | -1.041708 | 0.970194 | 0.0218854 | -0.5420116 | 0.6521559 | -1.2240103 | 1 |
| 2 | -0.44001395 | 1.5149916 | -1.0959327 | 0.198153924 | -1.387468 | 0.970194 | 0.2165229 | -0.5420116 | -1.5332062 | 0.1179507 | 0 |
| 3 | -1.53671734 | -0.9018411 | -1.0959327 | 0.293502747 | 1.032856 | -1.030619 | 0.2406749 | 2.2648841 | 0.6521559 | 1.3325030 | 1 |
| 4 | 0.50149556 | -0.9018411 | -1.0959327 | 0.007456278 | -1.387468 | -1.030619 | -0.1089125 | 0.8614363 | -1.5332062 | -1.2233380 | 0 |
| 5 | 2.06378057 | 1.5149916 | -1.0959327 | 0.388851570 | -1.041708 | 0.970194 | -0.3652575 | -0.5420116 | 0.6521559 | 0.7860657 | 0 |
| 6 | -0.05720239 | 1.5149916 | 0.9123735 | 0.484200392 | 1.032856 | -1.030619 | 0.8636071 | 0.8614363 | 0.6521559 | 0.5974260 | 1 |
8.4 Формування вибірок
Розділимо вибірку на тестову та тренувальну за допомогою пакету caTools та функції sample.split():
8.5 Побудова Deep Learning моделі
Викосритаємо можливості пакету h2o для побудови моделі на основі deep learning. Підключимо пакет.
Увага! Для запуску пакету потрібна віртуальна машина Java на ПК (JVM). Завантажити актуальну версію Java можна з сайту https://www.java.com/en/download/.
Запустимо двигун h2o:
# Увага, це специфічні налаштуваняя для ПК на якому налагоджувався проєкт
Sys.setenv(JAVA_HOME = "C:/Program Files/Java/jdk-19/")
print(Sys.getenv("JAVA_HOME"))[1] "C:/Program Files/Java/jdk-19/"
H2O is not running yet, starting it now...
Note: In case of errors look at the following log files:
D:\Temp\RtmpWcfQIa\file86d022e97546/h2o_yura_started_from_r.out
D:\Temp\RtmpWcfQIa\file86d01affe0a/h2o_yura_started_from_r.err
Starting H2O JVM and connecting: . Connection successful!
R is connected to the H2O cluster:
H2O cluster uptime: 4 seconds 650 milliseconds
H2O cluster timezone: Europe/Kiev
H2O data parsing timezone: UTC
H2O cluster version: 3.38.0.1
H2O cluster version age: 14 days, 4 hours and 46 minutes
H2O cluster name: H2O_started_from_R_yura_svm422
H2O cluster total nodes: 1
H2O cluster total memory: 3.95 GB
H2O cluster total cores: 8
H2O cluster allowed cores: 8
H2O cluster healthy: TRUE
H2O Connection ip: localhost
H2O Connection port: 54321
H2O Connection proxy: NA
H2O Internal Security: FALSE
R Version: R version 4.1.3 (2022-03-10)
Побудуємо математичну модель:
h2o_model <- h2o.deeplearning(y = 'Exited',
training_frame = as.h2o(train_data),
activation = "Rectifier",
hidden = c(6,6),
epochs = 100) |======================================================================| 100%
|======================================================================| 100%
Варто перегляднути набір параметрів, що може приймати функція h2o.deeplearning(), адже вона досить складна:
Здійснимо прогноз на тестовій вибірці (h2o.predict()), а також класифікуємо значення за cutOff = 0.5:
|======================================================================| 100%
|======================================================================| 100%
Побудуємо матрицю Confusion Matrix:
suppressMessages(library(caret))
caret::confusionMatrix(factor(test_data$Exited), factor(h2o_predict_class), positive = "1")Confusion Matrix and Statistics
Reference
Prediction 0 1
0 2285 104
1 343 268
Accuracy : 0.851
95% CI : (0.8377, 0.8636)
No Information Rate : 0.876
P-Value [Acc > NIR] : 1
Kappa : 0.4624
Mcnemar's Test P-Value : <2e-16
Sensitivity : 0.72043
Specificity : 0.86948
Pos Pred Value : 0.43863
Neg Pred Value : 0.95647
Prevalence : 0.12400
Detection Rate : 0.08933
Detection Prevalence : 0.20367
Balanced Accuracy : 0.79496
'Positive' Class : 1
Побудуємо ROC-криву:
Attaching package: 'InformationValue'
The following objects are masked from 'package:caret':
confusionMatrix, precision, sensitivity, specificity

Побудуємо модель з більшою кількістю прихованих шарів та нейронів:
h2o_model2 <- h2o.deeplearning(y = 'Exited',
training_frame = as.h2o(train_data),
activation = "Rectifier",
hidden = c(10,10),
epochs = 100) |======================================================================| 100%
|======================================================================| 100%
Здійснимо прогноз:
|======================================================================| 100%
|======================================================================| 100%
Confiusion Matrix:
Confusion Matrix and Statistics
Reference
Prediction 0 1
0 2302 87
1 343 268
Accuracy : 0.8567
95% CI : (0.8436, 0.869)
No Information Rate : 0.8817
P-Value [Acc > NIR] : 1
Kappa : 0.4765
Mcnemar's Test P-Value : <2e-16
Sensitivity : 0.75493
Specificity : 0.87032
Pos Pred Value : 0.43863
Neg Pred Value : 0.96358
Prevalence : 0.11833
Detection Rate : 0.08933
Detection Prevalence : 0.20367
Balanced Accuracy : 0.81263
'Positive' Class : 1
ROC-крива:
Зупинимо двигун h2o:

