Customer churn (berhentinya pelanggan) merupakan ancaman utama bagi bisnis telekomunikasi, karena biaya mendapatkan pelanggan baru jauh lebih besar daripada biaya mempertahankan pelanggan yang ada. Laporan ini menganalisis dataset Telco Customer Churn dengan tujuan:
data_raw <- read.csv("Telco-Customer-Churn.csv", stringsAsFactors = FALSE)
# TotalCharges bisa terbaca sebagai teks (berisi spasi) atau angka (berisi NA): tangkap keduanya
empty_idx <- which(is.na(suppressWarnings(as.numeric(trimws(data_raw$TotalCharges)))))
empty_rows <- data_raw[empty_idx, c("customerID", "tenure", "MonthlyCharges", "TotalCharges", "Churn")]
kable(head(empty_rows, 5), row.names = FALSE,
caption = paste0("Pelanggan dengan TotalCharges Kosong (5 dari ", nrow(empty_rows), " amatan)")) %>% tbl_style()| customerID | tenure | MonthlyCharges | TotalCharges | Churn |
|---|---|---|---|---|
| 4472-LVYGI | 0 | 52.55 | NA | No |
| 3115-CZMZD | 0 | 20.25 | NA | No |
| 5709-LVOEQ | 0 | 80.85 | NA | No |
| 4367-NUYAO | 0 | 25.75 | NA | No |
| 1371-DWPAZ | 0 | 56.05 | NA | No |
Dataset awal terdiri dari 7043 baris dan 21
kolom. Kolom TotalCharges memiliki 11
amatan yang kosong (tidak terisi). Seluruhnya adalah pelanggan
baru dengan tenure = 0 dan Churn = No yang
belum melewati siklus penagihan pertama. Jumlahnya hanya
0,16% dari data, sehingga baris tersebut dihapus.
data_clean <- data_raw
data_clean$TotalCharges <- as.numeric(trimws(data_clean$TotalCharges))
data_clean <- data_clean[!is.na(data_clean$TotalCharges), ]
data_clean$customerID <- NULL
data_clean$SeniorCitizen <- factor(ifelse(data_clean$SeniorCitizen == 1, "Yes", "No"))
chr_cols <- sapply(data_clean, is.character)
data_clean[chr_cols] <- lapply(data_clean[chr_cols], as.factor)
data_clean$Churn <- factor(data_clean$Churn, levels = c("No", "Yes"))Data bersih berisi 7032 amatan dengan 19
variabel prediktor dan satu variabel target
(Churn).
Data dibagi secara stratified dengan rasio 80:20 menggunakan
set.seed(0013).
set.seed(0013)
train_index <- createDataPartition(data_clean$Churn, p = 0.8, list = FALSE)
train_raw <- data_clean[train_index, ]
test_raw <- data_clean[-train_index, ]
train_y <- train_raw$Churn
test_y <- test_raw$Churn
cat("Data latih:", nrow(train_raw), "amatan\n")Data latih: 5627 amatan
Data uji : 1405 amatan
KNN mengklasifikasikan data berdasarkan jarak Euclidean ke tetangga
terdekat. Pada data ini, TotalCharges berkisar hingga
8.685, sedangkan tenure hanya hingga
72 dan variabel dummy hanya bernilai 0 atau 1.
Tanpa normalisasi, TotalCharges akan mendominasi
perhitungan jarak, sehingga seluruh fitur dinormalisasi dengan
Min-Max ke rentang \([0,
1]\). Nilai minimum dan maksimum diambil dari data latih saja
agar tidak terjadi kebocoran data.
# One-hot encoding
dummies <- dummyVars(Churn ~ ., data = train_raw)
train_dummy <- predict(dummies, newdata = train_raw)
test_dummy <- predict(dummies, newdata = test_raw)
# Min-Max scaling berdasarkan data latih
col_min <- apply(train_dummy, 2, min)
col_range <- apply(train_dummy, 2, max) - col_min
col_range[col_range == 0] <- 1
train_x <- as.data.frame(scale(train_dummy, center = col_min, scale = col_range))
test_x <- as.data.frame(scale(test_dummy, center = col_min, scale = col_range))Nilai \(k\) dipilih dari \(\{1, 3, 5, \dots, 41\}\) menggunakan 5-fold cross-validation pada data latih, berdasarkan akurasi rata-rata.
k_candidates <- seq(1, 41, by = 2)
set.seed(0013)
folds <- createFolds(train_y, k = 5)
cv_acc <- sapply(k_candidates, function(k) {
mean(sapply(folds, function(idx) {
pred <- knn(train_x[-idx, ], train_x[idx, ], cl = train_y[-idx], k = k)
mean(pred == train_y[idx])
}))
})
cv_results <- data.frame(k = k_candidates, Accuracy = cv_acc)
best_k <- cv_results$k[which.max(cv_results$Accuracy)]Nilai \(k\) terbaik adalah \(k = 33\) dengan akurasi validasi silang 78,85%.
Naive Bayes menghitung peluang kelas berdasarkan Teorema Bayes dengan asumsi bahwa antar fitur saling independen jika kelas diketahui:
\[P(Y = c \mid \mathbf{X}) \propto P(Y = c) \prod_{j=1}^{p} P(X_j \mid Y = c)\]
Asumsi ini dilanggar pada beberapa fitur dalam dataset:
MonthlyCharges dan
TotalCharges: \(TotalCharges \approx tenure \times
MonthlyCharges\), dengan korelasi 0,65. Keduanya
membawa informasi biaya yang sama.PhoneService dan
MultipleLines: pelanggan tanpa layanan telepon
otomatis bernilai “No phone service” pada
MultipleLines.InternetService dan layanan tambahan
(OnlineSecurity, OnlineBackup,
DeviceProtection, TechSupport,
StreamingTV, StreamingMovies): tanpa internet,
keenam fitur ini otomatis bernilai “No internet service”.tenure dan Contract:
pelanggan dengan tenure panjang cenderung memiliki kontrak 1
atau 2 tahun.Pelanggaran ini membuat probabilitas posterior cenderung terlalu ekstrem (overconfident), tetapi untuk klasifikasi biner Naive Bayes umumnya tetap bekerja baik.
| Metrik | KNN (k = 33) | Naive Bayes |
|---|---|---|
| Akurasi | 0,7779 | 0,7075 |
| Presisi | 0,5764 | 0,4708 |
| Recall | 0,6166 | 0,8204 |
| Spesifisitas | 0,8362 | 0,6667 |
| F1-Score | 0,5959 | 0,5982 |
Dalam konteks retensi pelanggan, kedua jenis kesalahan memiliki biaya yang berbeda:
Dari 373 pelanggan yang benar-benar churn pada data uji, KNN melewatkan 143 pelanggan (FP = 169), sedangkan Naive Bayes melewatkan 67 pelanggan (FP = 344).
Model yang direkomendasikan: Naive Bayes. Karena biaya false negative jauh lebih besar daripada false positive, model dengan recall tertinggi dipilih. Naive Bayes memiliki recall 82,04% dan hanya melewatkan 67 dari 373 pelanggan churn.
Apriori membutuhkan data kategorik, sehingga tiga fitur numerik dikelompokkan sebagai berikut:
| Fitur | Kategori |
|---|---|
tenure |
Short (0–12 bulan), Medium (13–48 bulan), Long (49–72 bulan) |
MonthlyCharges |
Low (< 35), Med (35–70), High (> 70) |
TotalCharges |
Low (< 1.000), Med (1.000–4.000), High (> 4.000) |
data_arules <- data_clean
data_arules$tenure_cat <- cut(data_arules$tenure, breaks = c(-1, 12, 48, 72),
labels = c("Tenure_Short(0-12m)", "Tenure_Medium(13-48m)", "Tenure_Long(49-72m)"))
data_arules$MonthlyCharges_cat <- cut(data_arules$MonthlyCharges, breaks = c(0, 35, 70, 150),
labels = c("Charge_Low(<35)", "Charge_Med(35-70)", "Charge_High(>70)"))
data_arules$TotalCharges_cat <- cut(data_arules$TotalCharges, breaks = c(-1, 1000, 4000, 10000),
labels = c("Total_Low(<1k)", "Total_Med(1k-4k)", "Total_High(>4k)"))
data_arules <- data_arules[, !(names(data_arules) %in% c("tenure", "MonthlyCharges", "TotalCharges"))]
trans <- as(data_arules, "transactions")Untuk aturan \(X \Rightarrow Y\) dengan \(N\) transaksi:
| Metrik | Rumus | Makna |
|---|---|---|
| Support | \(P(X \cap Y)\) | Seberapa sering \(X\) dan \(Y\) muncul bersama |
| Confidence | \(P(Y \mid X)\) | Peluang \(Y\) muncul jika \(X\) muncul |
| Lift | \(\dfrac{Conf(X \Rightarrow Y)}{P(Y)}\) | Lift > 1 berarti asosiasi positif |
| Leverage | \(P(X \cap Y) - P(X)P(Y)\) | Selisih kemunculan bersama dari yang diharapkan jika independen |
| Conviction | \(\dfrac{1 - P(Y)}{1 - Conf(X \Rightarrow Y)}\) | Semakin besar, semakin kuat ketergantungan aturan |
Aturan ditambang secara terpisah untuk Churn=Yes dan
Churn=No. Aturan untuk Churn=No memakai ambang
yang lebih ketat karena kelas ini jauh lebih dominan.
mine_rules <- function(rhs, supp, conf) {
r <- apriori(trans,
parameter = list(supp = supp, conf = conf, minlen = 2, maxlen = 4),
appearance = list(rhs = rhs, default = "lhs"),
control = list(verbose = FALSE))
quality(r) <- cbind(quality(r), interestMeasure(r, c("leverage", "conviction"), transactions = trans))
r
}
rules_yes <- mine_rules("Churn=Yes", supp = 0.05, conf = 0.65)
rules_no <- mine_rules("Churn=No", supp = 0.15, conf = 0.90)
# Ambil aturan dengan LHS tertentu
pick_rule <- function(rules, lhs_items, target) {
lhs_list <- as(lhs(rules), "list")
idx <- which(sapply(lhs_list, setequal, lhs_items))
q <- quality(rules[idx[1]])
data.frame(Target = target,
Rule = labels(rules[idx[1]]),
Support = q$support, Count = q$count, Confidence = q$confidence,
Lift = q$lift, Leverage = q$leverage, Conviction = q$conviction)
}
rule_tbl <- rbind(
pick_rule(rules_yes, c("InternetService=Fiber optic", "PaperlessBilling=Yes", "tenure_cat=Tenure_Short(0-12m)"), "Churn=Yes"),
pick_rule(rules_yes, c("InternetService=Fiber optic", "OnlineSecurity=No", "tenure_cat=Tenure_Short(0-12m)"), "Churn=Yes"),
pick_rule(rules_no, "Contract=Two year", "Churn=No")
)| Aturan | Support | Confidence | Lift | Leverage | Conviction |
|---|---|---|---|---|---|
| Rule 1: {InternetService=Fiber optic, PaperlessBilling=Yes, tenure_cat=Tenure_Short(0-12m)} => {Churn=Yes} | 0,0745 | 0,7228 | 2,7193 | 0,0471 | 2,6483 |
| Rule 2: {InternetService=Fiber optic, OnlineSecurity=No, tenure_cat=Tenure_Short(0-12m)} => {Churn=Yes} | 0,0853 | 0,7168 | 2,6971 | 0,0537 | 2,5930 |
| Rule 3: {Contract=Two year} => {Churn=No} | 0,2328 | 0,9715 | 1,3232 | 0,0569 | 9,3302 |
Rule 1: Pelanggan baru Fiber optic dengan tagihan digital \(\Rightarrow\) Churn
Rule 2: Pelanggan baru Fiber optic tanpa Online Security \(\Rightarrow\) Churn
Rule 3: Kontrak dua tahun \(\Rightarrow\) Tidak churn
Hasil eksplorasi data, model klasifikasi, dan aturan asosiasi saling menguatkan: risiko churn tertinggi ada pada pelanggan baru (0–12 bulan) berkontrak month-to-month, menggunakan Fiber optic, dan tanpa layanan tambahan. Segmen ini dapat dijaring lebih awal dengan model Naive Bayes (recall 82,04%).
Lampiran ini memverifikasi logika masing-masing algoritma menggunakan data mini.
Data latih mini (5 pelanggan) dengan fitur Tenure (\(X_1\)) dan MonthlyCharges (\(X_2\)):
| ID | Tenure | MonthlyCharges | Churn |
|---|---|---|---|
| 1 | 2 | 75 | Yes |
| 2 | 6 | 85 | Yes |
| 3 | 24 | 30 | No |
| 4 | 48 | 90 | No |
| 5 | 60 | 20 | No |
Akan diprediksi pelanggan baru \((\text{Tenure} = 4, \text{MonthlyCharges} = 80)\) dengan \(k = 3\).
Langkah 1: Normalisasi Min-Max. Rentang Tenure \(= 60 - 2 = 58\) dan rentang MonthlyCharges \(= 90 - 20 = 70\).
Langkah 2: Jarak Euclidean ke titik baru.
| Titik | Perhitungan | Jarak | Kelas |
|---|---|---|---|
| \(P_1\) | \(\sqrt{0{,}0345^2 + 0{,}0714^2}\) | 0,0793 | Yes |
| \(P_2\) | \(\sqrt{0{,}0345^2 + 0{,}0715^2}\) | 0,0794 | Yes |
| \(P_3\) | \(\sqrt{0{,}3448^2 + 0{,}7142^2}\) | 0,7931 | No |
| \(P_4\) | \(\sqrt{0{,}7586^2 + 0{,}1429^2}\) | 0,7719 | No |
| \(P_5\) | \(\sqrt{0{,}9655^2 + 0{,}8571^2}\) | 1,2911 | No |
Langkah 3: Voting. Tiga tetangga terdekat adalah \(P_1\) (Yes), \(P_2\) (Yes), dan \(P_4\) (No). Suara Yes = 2 dan No = 1, sehingga prediksi: \(\hat{Y} = \textbf{Yes}\).
Data latih mini (6 pelanggan) dengan fitur Contract, InternetService, TechSupport:
| ID | Contract | InternetService | TechSupport | Churn |
|---|---|---|---|---|
| 1 | Month-to-month | Fiber optic | No | Yes |
| 2 | Month-to-month | DSL | Yes | No |
| 3 | One year | DSL | Yes | No |
| 4 | Two year | No | No internet service | No |
| 5 | Month-to-month | Fiber optic | No | Yes |
| 6 | Two year | Fiber optic | Yes | No |
Akan diprediksi pelanggan baru (Contract = Month-to-month, InternetService = Fiber optic, TechSupport = No).
Langkah 1: Prior. \(P(\text{Yes}) = \frac{2}{6} = 0{,}3333\) dan \(P(\text{No}) = \frac{4}{6} = 0{,}6667\).
Langkah 2: Likelihood dengan Laplace smoothing \(\left(\frac{n + 1}{N_{\text{kelas}} + K}\right)\), dengan \(K = 3\) kategori pada setiap fitur.
| Likelihood | Kelas Yes (\(N = 2\)) | Kelas No (\(N = 4\)) |
|---|---|---|
| \(P(\text{Contract = Month-to-month} \mid \cdot)\) | \(\frac{2+1}{2+3} = 0{,}6000\) | \(\frac{1+1}{4+3} = 0{,}2857\) |
| \(P(\text{InternetService = Fiber optic} \mid \cdot)\) | \(\frac{2+1}{2+3} = 0{,}6000\) | \(\frac{1+1}{4+3} = 0{,}2857\) |
| \(P(\text{TechSupport = No} \mid \cdot)\) | \(\frac{2+1}{2+3} = 0{,}6000\) | \(\frac{0+1}{4+3} = 0{,}1429\) |
Langkah 3: Skor posterior tak ternormalisasi.
\[\text{Skor}(\text{Yes}) = \frac{1}{3} \times 0{,}6^3 = \mathbf{0{,}0720}\] \[\text{Skor}(\text{No}) = \frac{2}{3} \times 0{,}2857 \times 0{,}2857 \times 0{,}1429 \approx \mathbf{0{,}00777}\]
Langkah 4: Normalisasi dan keputusan.
\[P(\text{Yes} \mid \mathbf{x}) = \frac{0{,}0720}{0{,}0720 + 0{,}00777} \approx 90{,}26\%, \qquad P(\text{No} \mid \mathbf{x}) \approx 9{,}74\%\]
Karena \(P(\text{Yes} \mid \mathbf{x}) > P(\text{No} \mid \mathbf{x})\), prediksi: \(\hat{Y} = \textbf{Yes}\).
Dari \(N = 10\) transaksi dengan
atribut \(A\) =
Contract = Month-to-month dan \(B\) = Churn = Yes:
| Churn=Yes (B) | Churn=No | |
|---|---|---|
| Month-to-month (A) | 5 | 1 |
| Bukan Month-to-month | 2 | 2 |
Diperoleh \(n(A \cap B) = 5\), \(n(A) = 6\), dan \(n(B) = 7\), sehingga \(Supp(A) = 0{,}60\) dan \(Supp(B) = 0{,}70\). Metrik untuk aturan \(A \Rightarrow B\):
| Metrik | Perhitungan | Hasil |
|---|---|---|
| Support | \(\frac{5}{10}\) | 0,5000 |
| Confidence | \(\frac{0{,}50}{0{,}60}\) | 0,8333 |
| Lift | \(\frac{0{,}8333}{0{,}70}\) | 1,1905 |
| Leverage | \(0{,}50 - (0{,}60 \times 0{,}70)\) | +0,0800 |
| Conviction | \(\frac{1 - 0{,}70}{1 - 0{,}8333}\) | 1,8000 |
Lift > 1, leverage positif, dan conviction > 1 menunjukkan bahwa kontrak month-to-month berasosiasi positif dengan churn, dengan risiko 1,19 kali lebih tinggi dari rata-rata.