Background
Kamu pasti pernah merasa kurang puas dengan sebuah perusahaan telekomunikasi dan akhirnya memutuskan pindah ke perusahaan lain? Entah karena harganya terlalu mahal, sinyalnya yang kurang bagus, 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 telekomunikasi 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 apakah seorang pelanggan akan churn atau tidak. Harapannya, dengan adanya model ini, pihak perusahaan telekomunikasi dapat melakukan tindak preventif bagi pelanggan yang berpeluang besar untuk churn.
Workflow
Import Data
Data yang digunakan merupakan data profil pelanggan dari sebuah perusahaan telekomunikasi yang diperoleh dari Kaggle. Dataset tersebut berisikan data untuk 7043 pelanggan yang meliputi demografis pelanggan, informasi pembayaran akun, serta produk layanan yang didaftarkan oleh tiap pelanggan. Dari informasi tersebut, kita ingin memprediksi apakah seorang pelanggan akan Churn atau tidak.
customer <- read.csv("data_input/Telco-Customer-Churn.csv", stringsAsFactors = T)
head(customer)Berikut ini merupakan deskripsi untuk setiap variabel:
CustomerID: Customer IDGender: Gender pelanggan yaitu Female dan MaleSeniorCitizen: Apakah pelanggan merupakan senio citizen (0: No, 1: Yes)Partner: Apakah pelanggan memiliki partner atau tidak (Yes, No)Dependents: Apakah pelanggan memiliki tanggungan atau tidak (Yes, No)Tenure: Jumlah bulan dalam menggunakan produk perusahaanMultipleLines: Apakah pelanggan memiliki banyak saluran atau tidak (Yes, No, No phone service)OnlineSecurity: Apakah pelanggan memiliki keamanan online atau tidakOnlineBackup: Apakah pelanggan memiliki cadangan online atau tidakDeviceProtection: Apakah pelanggan memiliki perlindungan perangkat atau tidakTechSupport: Apakah pelanggan memiliki dukungan teknis atau tidakStreamingTV: Apakah pelanggan berlangganan TV streaming atau tidakStreamingMovies: Apakah pelanggan berlangganan movies streaming atau tidakContract: Ketentuan kontrak berlangganan (Month-to-month, One year, Two year)PaperlessBilling: Apakah pelanggan memiliki tagihan tanpa kertas atau tidak (Yes, No)PaymentMethod: Metode pembayaran (Electronic check, Mailed check, Bank transfer (automatic), Credit card (automatic))MonthlyCharges: Jumlah pembayaran yang dilakukan setiap bulanTotalCharges: Jumlah total yang dibebankan oleh pelangganChurn: Apakah pelanggan Churn atau tidak (Yes or No)
Data Cleansing
Sebelum masuk ke tahap modeling, mari kita membersihkan datanya terlebih dahulu.
Pertama, cek kelengkapan data, dari tahap ini kita akan memperoleh informasi apakah data kita sudah lengkap.
colSums(is.na(customer))#> customerID gender SeniorCitizen Partner
#> 0 0 0 0
#> Dependents tenure PhoneService MultipleLines
#> 0 0 0 0
#> InternetService OnlineSecurity OnlineBackup DeviceProtection
#> 0 0 0 0
#> TechSupport StreamingTV StreamingMovies Contract
#> 0 0 0 0
#> PaperlessBilling PaymentMethod MonthlyCharges TotalCharges
#> 0 0 0 11
#> Churn
#> 0
Dari 7043 observasi ternyata terdapat sebanyak 11 observasi pada kolom TotalCharges yang merupakan missing values (NA). Dikarenakan jumlah NA yang cukup sedikit, kita dapat membuang observasi tersebut.
Kedua, kita perlu buang variabel yang tidak relevan dengan pemodelan, yaitu CustomerID.
Ketiga, kita menyesuaikan tipe data kolom SeniorCitizen yang sebelumnya numerik menjadi kategorik.
customer <- customer %>%
select(-customerID) %>%
na.omit() %>%
mutate(SeniorCitizen = as.factor(SeniorCitizen)) Exploratory Data Analysis
Selanjutnya mari lakukan eksplorasi data baik untuk kolom kategorik maupun numerik.
Untuk mengetahui proporsi kelas pada setiap variabel kategori, kita dapat menggunakan fungsi inspect_cat dari package inspectdf seperti berikut:
customer %>% inspect_cat() %>% show_plot()Dari visualisasi di atas dapat diketahui proporsi kelas untuk variabel target Churn lebih banyak di kategori No dibandingkan Yes. Lalu, untuk proporsi variabel lainnya mayoritas seimbang.
Berikutnya kita dapat eksplorasi persebaran untuk variabel data numerik dengan fungsi inspect_num dari package inspectdf seperti berikut:
customer %>% inspect_num() %>% show_plot()Dari visualisasi di atas dapat disimpulkan bahwa persebaran data numerik cukup beragam untuk setiap variabelnya.
Train-Test Splitting
Setelah kita melakukan data cleansing dan eksplorasi data, tahap berikutnya adalah train-test splitting yaitu membagi data menjadi data train dan test dengan proporsi 80:20. Data train digunakan untuk membuat model sedangkan data test digunakan untuk mengevaluasi performa model.
set.seed(100)
idx <- initial_split(data = customer,
prop = 0.8,
strata = "Churn")
data_train <- training(idx)
data_test <- testing(idx)Modeling
Selanjutnya kita akan melakukan modeling menggunakan algoritma Random Forest (package caret) dengan menentukan banyaknya cross validation, repetisi, serta mencantumkan nama target variabel dan juga prediktor yang digunakan dari data train.
set.seed(100)
ctrl <- trainControl(method = "repeatedcv",
number = 5,
repeats = 3)
model_forest <- train(Churn ~ .,
data = data_train,
method = "rf",
trControl = ctrl)
# saveRDS(model_forest, "assets/model_forest.rds")Chunk di atas membutuhkan waktu yang cukup lama untuk dieksekusi. Untuk mempersingkat waktu, mari load model yang sebelumnya sudah disimpan ke dalam bentuk file RDS.
model_forest <- readRDS("assets/model_forest.rds")
model_forest#> Random Forest
#>
#> 5627 samples
#> 19 predictor
#> 2 classes: 'No', 'Yes'
#>
#> No pre-processing
#> Resampling: Cross-Validated (5 fold, repeated 3 times)
#> Summary of sample sizes: 4501, 4502, 4501, 4502, 4502, 4501, ...
#> Resampling results across tuning parameters:
#>
#> mtry Accuracy Kappa
#> 2 0.7837817 0.3252122
#> 16 0.7750746 0.3779712
#> 30 0.7731203 0.3727503
#>
#> Accuracy was used to select the optimal model using the largest value.
#> The final value used for the model was mtry = 2.
Untuk saat ini, kita memperoleh model Random Forest dengan tingkat akurasi di data train sebesar 78,38% dengan nilai mtry optimum sebanyak 2.
Selanjutnya kita akan melakukan tuning model dengan melakukan upsampling, yang artinya kita akan menyetarakan proporsi target variabel menjadi sama besar.
data_train_up <- upSample(x = data_train[, -20],
y = data_train$Churn,
yname = "Churn")
# cek proporsi
prop.table(table(data_train_up$Churn))#>
#> No Yes
#> 0.5 0.5
Dari data yang sudah dilakukan upsampling, kita akan membuat ulang model Random Forestnya.
set.seed(100)
ctrl <- trainControl(method = "repeatedcv",
number = 5,
repeats = 3)
model_forest_up <- train(Churn ~ .,
data = data_train_up,
method = "rf",
trControl = ctrl)
# saveRDS(model_forest_up, "assets/model_forest_up.rds")Untuk mempersingkat waktu, mari load model yang sebelumnya sudah disimpan ke dalam bentuk file RDS.
model_forest_up <- readRDS("assets/model_forest_up.rds")
model_forest_up#> Random Forest
#>
#> 8262 samples
#> 19 predictor
#> 2 classes: 'No', 'Yes'
#>
#> No pre-processing
#> Resampling: Cross-Validated (5 fold, repeated 3 times)
#> Summary of sample sizes: 6609, 6610, 6609, 6610, 6610, 6610, ...
#> Resampling results across tuning parameters:
#>
#> mtry Accuracy Kappa
#> 2 0.7760017 0.5520022
#> 16 0.8911472 0.7822945
#> 30 0.8875167 0.7750336
#>
#> Accuracy was used to select the optimal model using the largest value.
#> The final value used for the model was mtry = 16.
Setelah dilakukan upsampling, terlihat bahwa nilai akurasi di data train meningkat menjadi 89,11% dengan nilai mtry optimum sebanyak 16.
Model Evaluation
Terakhir, mari kita uji model random forest yang telah kita buat ke data test. Pada kasus ini, kita ingin memperoleh nilai recall atau sensitivitas yang sebesar mungkin agar model kita dapat mendeteksi pelanggan yang sebenarnya Churn sebanyak-banyaknya.
pred <- predict(model_forest_up, newdata = data_test, type = "prob")
pred$result <- as.factor(ifelse(pred$Yes > 0.45, "Yes", "No"))
confusionMatrix(pred$result, data_test$Churn, positive = "Yes")#> Confusion Matrix and Statistics
#>
#> Reference
#> Prediction No Yes
#> No 849 109
#> Yes 183 264
#>
#> Accuracy : 0.7922
#> 95% CI : (0.77, 0.8131)
#> No Information Rate : 0.7345
#> P-Value [Acc > NIR] : 0.00000031
#>
#> Kappa : 0.4989
#>
#> Mcnemar's Test P-Value : 0.00001937
#>
#> Sensitivity : 0.7078
#> Specificity : 0.8227
#> Pos Pred Value : 0.5906
#> Neg Pred Value : 0.8862
#> Prevalence : 0.2655
#> Detection Rate : 0.1879
#> Detection Prevalence : 0.3181
#> Balanced Accuracy : 0.7652
#>
#> 'Positive' Class : Yes
#>
Dengan menggunakan threshold 0.45, diperoleh recall sebesar 70,78% dengan akurasi sebesar 79,22%.
Selain menggunakan confusion matrix, kita dapat membentuk kurva ROC beserta nilai AUC dengan menggunakan package ROCR sebagai berikut:
pred_prob <- predict(object = model_forest_up, newdata = data_test, type = "prob")
pred <- prediction(pred_prob[,2], labels = data_test$Churn)
perf <- performance(prediction.obj = pred, measure = "tpr", x.measure = "fpr")
plot(perf)auc <- performance(pred, measure = "auc")
auc@y.values[[1]]#> [1] 0.8513259
Nilai AUC di atas menyatakan bahwa performa model kita sebesar 85,13% dalam memisahkan distribusi kelas positif Churn dengan negatif pada data test.
Conclusion
Dengan adanya model untuk memprediksi customer churn, pihak perusahaan telekomunikasi dengan mudah mengetahui pelanggan mana yang memiliki kecenderungan untuk churn.
Visualisasi berikut memperlihatkan hasil prediksi untuk dua pelanggan. Kedua pelanggan tersebut memiliki peluang yang cukup besar untuk churn dan kita juga dapat mengetahui variabel mana saja yang mendukung (supports) dan bertentangan (contradicts) terhadap hasil prediksi model.
library(lime)
test_x <- data_test %>%
dplyr::select(-Churn)
explainer <- lime(test_x, model_forest_up)
explanation <- lime::explain(test_x[1:2,],
explainer,
labels = c("Yes"),
n_features = 8)
plot_features(explanation)Dapat disimpulkan bahwa alasan terkuat kedua pelanggan tersebut berpeluang besar akan churn karena memiliki kontrak yang bersifat bulanan dan tenure yang masih dibawah 8 bulan. Dari sini, pihak marketing dapat melakukan promosi produk dengan sifat kontrak yang jangkanya lebih panjang sehingga kedua pelanggan ini dapat bertahan lebih lama.
External Resources
- Reference: Algoritma Book: ML Application in Industry
- Dataset: Kaggle: Telco Customer Churn
- Repository: GitHub: tomytjandra