Survival Model
Introduction
Kamu pasti pernah merasa kurang puas dengan sebuah perusahaan perbankan dan akhirnya memutuskan pindah ke perusahaan lain? Entah karena system kerjanya kurang bagus , ketidaknyamanan sebagai nasabah karena alasan tertentu, atau karena pelayanannya yang kurang baik. Nah hal itu disebut dengan Customer Churn. Customer churn didefinisikan sebagai kecenderungan pelanggan untuk berhenti melakukan interaksi dengan sebuah perusahaan. Perusahaan perbankan memiliki kebutuhan untuk mengetahui apakah pelanggan akan berhenti berlangganan atau tidak, karena biaya untuk mempertahankan pelanggan yang sudah ada jauh lebih sedikit dibandingkan memperoleh pelanggan baru. Perusahaan biasanya mendefinisikan 2 tipe customer churn, yaitu voluntary dan involuntary. Voluntary churn merupakan pelanggan yang dengan sengaja berhenti dan beralih ke perusahaan lain, sedangkan involuntary churn merupakan pelanggan yang berhenti karena sebab eksternal seperti berpindah lokasi, kematian, atau alasan lainnya. Diantara kedua tipe tersebut, voluntary churn lah yang tidak sulit untuk dilakukan karena kita dapat mempelajari karakteristik pelanggan yang dapat dilihat dari profil pelanggan. Permasalahan ini dapat dijawab dengan membuat sebuah model Machine Learning yang dapat memprediksi Dengan menggunakan Analisis Survival, tidak hanya perusahaan yang dapat memprediksi apakah pelanggan akan churn atau tidak, tetapi juga kapan peristiwa itu mungkin terjadi.
Dataset
Description and Overview
| Feature Category | Feature Name | Type | Description |
|---|---|---|---|
| Time | Tenure | Numerical | Lama menjadi nasabah dalam bulan |
| Event | Exited | Categorical | Penunjuk nasabah telah churn atau tidak |
| Customer information | CustomerID | Categorical | ID unik tiap nasabah |
| Customer information | Surname | Categorical | Nama Belakang nasabah |
| Customer information | Geography | Categorical | Tempat asal nasabah |
| Customer information | Gender | Categorical | Jenis Kelamin nasabah |
| Customer Information | Age | Numerical | Usia nasabah |
| Customer Information | EstimatedSalary | Numerical | Perkiraan Gaji Nasabah |
| Product | CreditScore | Numerical | Nilai kelayakan nasabah untuk kredit |
| Product | Balance | Numerical | Jumlah simpanan nasabah |
| Product | HasCrCard | Categorical | Indikasi apakah nasabah memiliki kartu kredit atau tidak |
| Satisfication | NumofProducts | Numerical | Jumlah Produk yang digunakan nasabah |
| Satisfication | IsActiveMember | Categorical | Indikasi apakah nasabah aktif menggunakan produk atau tidak |
#Import Data
Churn <- read.csv("churn1.csv")
# Overview of the dataset
head(Churn)## RowNumber CustomerId Surname CreditScore Geography Gender Age Tenure
## 1 1 15634602 Hargrave 619 France Female 42 2
## 2 2 15647311 Hill 608 Spain Female 41 1
## 3 3 15619304 Onio 502 France Female 42 8
## 4 4 15701354 Boni 699 France Female 39 1
## 5 5 15737888 Mitchell 850 Spain Female 43 2
## 6 6 15574012 Chu 645 Spain Male 44 8
## Balance NumOfProducts HasCrCard IsActiveMember EstimatedSalary Exited
## 1 0.00 1 1 1 101348.88 1
## 2 83807.86 1 0 1 112542.58 0
## 3 159660.80 3 1 0 113931.57 1
## 4 0.00 2 0 0 93826.63 0
## 5 125510.82 1 1 1 79084.10 0
## 6 113755.78 2 1 0 149756.71 1
From categorical to numerical
Kita akan mengkategorikan EstimatedSalary dan Balance ke beberapa segmen sebagai berikut:
- 0-10,000
- 10,001-40,000
- 40,001-80,000
- 80,001-120,000
- 120,001-200,000
- 200,001-280,000
finance_category <- function(x){
if (x >= 0 && x <= 10000){
return('0-10000')
}else if(x > 10001 && x <= 40000){
return('10001-40000')
}else if (x > 40001 && x <= 80000){
return('40001-80000')
}else if (x > 80001 && x <=120000){
return('80001-120000')
}else if (x > 120001 && x <=200000){
return('120001-200000')
}else if (x > 200000){
return('> 200000')
}
}
Churn$Tenure <- as.numeric(Churn$Tenure)
Churn$Balance <- sapply(Churn$Balance,finance_category)
Churn$EstimatedSalary <- sapply(Churn$EstimatedSalary,finance_category)library("fastDummies")
Churn$Gender[Churn$Gender=='Male'] <- 1
Churn$Gender[Churn$Gender=='Female'] <- 2
Churn$Gender <- as.integer(Churn$Gender)
Churnnew <- dummy_cols(Churn,
select_columns = c("EstimatedSalary","Balance","Geography"),
remove_selected_columns = TRUE)
# extracting features
time_column <- 'Tenure'
event_column <- 'Exited'
features <-colnames(Churnnew)[!colnames(Churnnew)
%in%
c(time_column,event_column)]Exploratory Data Analysis
Null Values and Duplicates
# cek apakah ada nilai NA
sapply(Churnnew, function(x) sum(is.na(x)))## RowNumber CustomerId
## 0 0
## Surname CreditScore
## 0 0
## Gender Age
## 0 0
## Tenure NumOfProducts
## 0 0
## HasCrCard IsActiveMember
## 0 0
## Exited EstimatedSalary_0-10000
## 0 0
## EstimatedSalary_10001-40000 EstimatedSalary_40001-80000
## 0 0
## EstimatedSalary_80001-120000 EstimatedSalary_120001-200000
## 0 0
## Balance_> 200000 Balance_0-10000
## 0 0
## Balance_10001-40000 Balance_40001-80000
## 0 0
## Balance_80001-120000 Balance_120001-200000
## 0 0
## Geography_France Geography_Germany
## 0 0
## Geography_Spain
## 0
Dari hasil di atas bisa dilihat bahwa tidak ada null value terdeteksi
# cek apakah ada nilai Duplicates
sapply(Churnnew, function(x) sum(duplicated(x)))## RowNumber CustomerId
## 0 0
## Surname CreditScore
## 2208 3560
## Gender Age
## 3997 3933
## Tenure NumOfProducts
## 3988 3995
## HasCrCard IsActiveMember
## 3997 3997
## Exited EstimatedSalary_0-10000
## 3997 3997
## EstimatedSalary_10001-40000 EstimatedSalary_40001-80000
## 3997 3997
## EstimatedSalary_80001-120000 EstimatedSalary_120001-200000
## 3997 3997
## Balance_> 200000 Balance_0-10000
## 3997 3997
## Balance_10001-40000 Balance_40001-80000
## 3997 3997
## Balance_80001-120000 Balance_120001-200000
## 3997 3997
## Geography_France Geography_Germany
## 3997 3997
## Geography_Spain
## 3997
Dari hasil di atas dilihat banyak nilai duplikat, tapi hal itu merupakan hal yang lumrah. Yang penting adalah CustomerID unik. Untuk kolom yang lain, duplikat adalah hal yang wajar. CustomerID, RowNumber, dan Surname tidak memiliki fungsi dalam analysis ini, sehingga akan dihilangkan
Churnnew <- Churnnew[-c(1:3)]
features <-colnames(Churnnew)[!colnames(Churnnew)
%in%
c(time_column,event_column)]Correlations
library(corrplot)
corrplot(cor(Churnnew[features]),
title = "Correlation Plot",
method = "color",
tl.cex = 0.5,
tl.srt = 45,
number.cex = 0.35,
col = COL2('BrBG',10),
addCoef.col = "grey50",
mar = c(0,0,2,0)) Tidak ada korelasi signifikan (>0.6) antar variabel.
Modelling
Building the Model
# Membuat set training dan testing
library('caret')
set.seed(3456)
trainIndex <- createDataPartition(Churnnew$Tenure, p = .7,
list = FALSE,
times = 1)
Train <- Churnnew[ trainIndex,]
Test <- Churnnew[-trainIndex,]Selanjutnya kita menerapkan Random Forest Model
library(randomForestSRC)
library(randomForest)
library(survival)
library(dplyr)
set.seed(3456)
#fit <- rfsrc(Surv(time = Tenure, event = Exited==1)~.,
# data = Train,
# ntree=1000,
# mtry = sqrt(20),
# importance = TRUE)
#saveRDS(fit, "fit.rds")
fit <- readRDS("fit.rds")Model kita simpan ke dalam format rds untuk mengurangi waktu load model.
Variable Importance
Hasil dari menerapkan Random Forest Model, kita bisa mendapatkan Importance Variable, yaitu nilai-nilai yang menunjukkan pentingnya suatu variabel dari yang tertinggi sampai terendah.
importance<-fit$importance %>% data.frame()
colnames(importance) <- "ImportanceValue"
importance %>% arrange(desc(ImportanceValue)) %>% head()## ImportanceValue
## Age 0.18811953
## NumOfProducts 0.16968063
## Balance_> 200000 0.06985201
## IsActiveMember 0.05322681
## CreditScore 0.04415604
## Balance_0-10000 0.03160698
Dapat kita lihat yang paling mempengaruhi churn disini adalah Age dan NumofProducts
Cross Validation
Selanjutnya kita akan menguji performa model yang telah kita bangun menggunakan data yang telah kita bagi sebelumnya menjadi Train dan Test.
C-Index
C-Index mewakili penilaian kemampuan diskriminasi secara global. Secara umum ketika C-index mendekati 1, maka model memiliki kemampuan diskriminasi yang tinggi, tetapi di saat mendekati 0.5, maka model tidak memilki kemampuan untuk menentukan subjek dengan risiko tinggi atau rendah.
library(pec)
cindex(fit, formula =Surv(time = Tenure, event = Exited==1)~.,data = Train )$AppCindex## $rfsrc
## [1] 0.8559559
Nilai C-Index adalah 0.8559559, yang mana lebih mendekati 1 dibanding 0.5. Sehingga model ini dinyatakan memiliki kemampuan determinasi yang baik.
Brier Score
Brier score mengukur perbedaan rata-rata antara status dan perkiraan probabilitas pada waktu tertentu. Dengan demikian, semakin rendah skor (biasanya di bawah 0,25), semakin baik kinerja prediktif.
\[BS=\frac{1}{N}\sum^N_{t=1}{(f_t-o_t)^2}\]
bs <- get.brier.survival(fit)
bs$brier.score %>% tail(1)## time brier.score
## 11 10 0.101717
plot(c(1,9.7), c(0,.3),type = 'n', xlab = "Time", ylab = "")
lines(bs$brier.score, col = "blue")
abline(h=.25, col = "red", lwd = 3)
text(10,.26,"0.25 Limit", col = "red", lty = 5, adj = c(1,0))Brier score yang didapatkan adalah 0.1033 pada keseluruhan model yang mengindikasikan bahwa model sangat bagus untuk melakukan prediksi.
Predictions
Overall Predictions
Setelah membuat model untuk data kita, kita dapat membandingkan hasil prediksi dengan data aktual. Perbandingan dapat kita lakukan secara visual, yaitu dengan plot hasil prediksi model terhadap data Test
#Membuat prediksi dengan model dari data train terhadap data test
y.pred <- predict(fit,newdata = Test)
times <- y.pred$time.interest
survprob <- data.frame(y.pred$survival)
avgprob <- sapply(survprob,mean)
#Membangun model test untuk mengekstrak probability survival
#fittest <- rfsrc(Surv(time = Tenure, event = Exited==1)~.,
# data = Test,
# ntree=1000,
# mtry = sqrt(20),
# importance = TRUE)
#saveRDS(fittest, "fittest.rds")
fittest <- readRDS('fittest.rds')
survprobtest <- data.frame(fittest$survival)
avgprobtest <- sapply(survprobtest,mean)#PLOT
plot(times,
avgprob,
type="l",
xlab="Time (Year)",
ylab="Survival", col=1, lty=1, lwd=2,
main = "Perbandingan antara aktual dan prediksi")
lines(times, avgprobtest, col=2, lty=2, lwd=2)
legend("topright", legend = c("Predicted", "Actual"), col = c(1:2), lty = c(1:2)) Hasil Plot survival prediksi tidak jauh berbeda dengan aktual. Membuktikan hasil prediksi model sudah baik.
Individuals Predictions
Variabel terpenting dari hasil analisa di atas adalah Age, sehingga kita akan memplot survival rate berdasarkan Usia.
newdata <- data.frame(lapply(1:ncol(fit$xvar),function(i){median(fit$xvar[,i])}))
colnames(newdata) <- fit$xvar.names
newdata1 <- newdata2 <- newdata3 <- newdata4 <- newdata5 <- newdata6 <- newdata7 <- newdata8 <- newdata9 <- newdata
newdata1[,which(fit$xvar.names == "Age")] <- quantile(fit$xvar$Age, 0.1)
newdata2[,which(fit$xvar.names == "Age")] <- quantile(fit$xvar$Age, 0.2)
newdata3[,which(fit$xvar.names == "Age")] <- quantile(fit$xvar$Age, 0.3)
newdata4[,which(fit$xvar.names == "Age")] <- quantile(fit$xvar$Age, 0.4)
newdata5[,which(fit$xvar.names == "Age")] <- quantile(fit$xvar$Age, 0.5)
newdata6[,which(fit$xvar.names == "Age")] <- quantile(fit$xvar$Age, 0.6)
newdata7[,which(fit$xvar.names == "Age")] <- quantile(fit$xvar$Age, 0.7)
newdata8[,which(fit$xvar.names == "Age")] <- quantile(fit$xvar$Age, 0.8)
newdata9[,which(fit$xvar.names == "Age")] <- quantile(fit$xvar$Age, 0.9)
newdata <- rbind(newdata1,newdata2, newdata3, newdata4, newdata5, newdata6, newdata7, newdata8, newdata9)
y.pred <- predict(fit,newdata = newdata)
#PLOT
cols <- colors()
plot(times,
y.pred$survival[1,],
type = 'l',
ylim = c(0,1),
xlab="Time (Year)",
ylab="Survival", col=1, lty=1, lwd = 2,
main = "Bank's Customer Survival Curves by Age")
for (n in 2:9) {
lines(times, y.pred$survival[n,], type = 'l', col = cols[n], lwd = 2)
}
legend("bottomleft",
legend=c("Usia sekitar 27 Tahun",
"Usia sekitar 30 Tahun",
"Usia sekitar 33 Tahun",
"Usia sekitar 35 Tahun",
"Usia sekitar 37 Tahun",
"Usia sekitar 37 Tahun",
"Usia sekitar 37 Tahun",
"Usia sekitar 46 Tahun",
"Usia sekitar 53 Tahun"),
col=c(1:9), cex=1, lwd=2) Dari plot di atas, bisa dilihat bahwa semakin muda seseorang, kemungkinan untuk mereka churned pun semakin kecil.
set.seed(28)
exited_times <- fit$time.interest
surv_prob <- data.frame(fit$survival)
avg_prob <- sapply(surv_prob,mean)
# Plot the survival models for each patient
plot(fit$time.interest,fit$survival[1,],
type = "l",
ylim = c(0,1),
col = "red",
xlab = "Tenure",
ylab = "survival",
main = "Bank's Customer Survival Curves")
cols <- colors()
for (n in sample(c(2:dim(Train)[1]), 20)){
lines(fit$time.interest, fit$survival[n,], type = "l", col = cols[n])
}
lines(exited_times, avg_prob, lwd = 2)
legend("bottomleft", legend = c('Average = black'), lty = 1, lwd = 2)Dari hasil ini dapat dilihat dari rataan bahwa sampai sepuluh tahun kemudian, sekitar 60% nasabah masih akan bertahan.
Conclusion
Dapat disimpulkan bahwa model ini dapat digunakan untuk memprediksi nasabah seperti apa yang akan berhenti menjadi nasabah, dan dalam jangka waktu berapa lama. Analisa ini akan membantu bank untuk mengetahui nasabah-nasabah yang kemungkinan akan Churned, dan dapat memahami alasan-alasan yang dapat menyebabkan nasabah mereka Churned