Data Analytics | Insights & Research

PUAY101

ทำนายแนวโน้มจะเลิกใช้บริการ ด้วย Random Forest

Introduction

Business Problem

ในโลกธุรกิจการแข่งขันสูงในปัจจุบัน การรักษาฐานลูกค้าไว้ให้มั่นคงเป็นหัวใจสำคัญสู่ความสำเร็จ

การสูญเสียลูกค้าหรือที่เรียกว่า “Customer Churn” ไม่ได้หมายถึงแค่การสูญเสียรายได้ในปัจจุบัน แต่ยังรวมถึงโอกาสในอนาคตที่หายไปอีกด้วย

คำถามคือ “เราจะรู้ได้อย่างไรว่าลูกค้าคนไหนมีแนวโน้มจะจากเราไปก่อนที่พวกเขาจะตัดสินใจ?”

.

มาทำความรู้จักกับ Random Forest กัน! บทความนี้จะพาไปดูวิธีใช้โมเดลประเภท Classification เพื่อทำนายว่าลูกค้าจะ “อยู่ต่อ” หรือ “ไป” (เป็น Binary Classification หรือการจำแนกแค่ 2 กลุ่มนั่นเอง)

.

Banking Customer Churn Prediction Dataset Dataset จากคุณ Saurabh Badole

ขนาดข้อมูล: 10,000 rows × 14 columns

คอลัมน์ทั้งหมด:

  • RowNumber (index), CustomerId, Surname → เป็น identifier (ไม่ใช้ในการ train)
  • CreditScore (คะแนนเครดิต)
  • Geography (ประเทศ: France, Spain, Germany)
  • Gender (ชาย/หญิง)
  • Age (อายุ)
  • Tenure (จำนวนปีที่เป็นลูกค้า)
  • Balance (ยอดเงินในบัญชี)
  • NumOfProducts (จำนวนผลิตภัณฑ์ที่ถือ)
  • HasCrCard (มีบัตรเครดิตหรือไม่)
  • IsActiveMember (ลูกค้ามีการใช้งานจริงหรือไม่)
  • EstimatedSalary (เงินเดือนโดยประมาณ)
  • Exited (Target → 1 = เลิกใช้บริการ, 0 = ยังอยู่)

ประเภทข้อมูล:

  • Numeric (ตัวเลข): CreditScore, Age, Tenure, Balance, NumOfProducts, HasCrCard, IsActiveMember, EstimatedSalary, Exited
  • Categorical (แยกประเภท): Geography, Gender
  • Identifier: RowNumber, CustomerId, Surname

Dataset นี้พร้อมใช้ได้เลย ไม่ต้อง handle กับ missing value

แต่บางคอลัมน์ยังไม่ใช้ข้อมูลประเภทตัวเลข และบางคอลัมน์ไม่จำเป็นต้องเอามาใส่ใน Model อย่างเช่น (RowNumber, CustomerId, Surname)

ลบ Column ที่เป็น identifier ออก


Explore Data

ดูจากกราฟนี้จะเห็นได้ชัดว่าข้อมูลของเรากำลังเจอกับปัญหาที่เรียกว่า Imbalanced Target (Proportion ของ Churn มีมากกว่า คนที่ Stay อยู่มาก)ซึ่งอาจจะทำให้โมเดลเกิด bias ได้

การกระจายตัวของแต่ละ Feature แบ่งตาม Class (0 กับ 1)

คนอายุเยอะมีโอกาส churn สูงกว่า

ลูกค้าที่ balance สูงก็ churn มาก

ลูกค้า Germany มี churn rate สูงกว่า France/Spain

เพศหญิง churn rate สูงกว่าเพศชายเล็กน้อย

import seaborn as sns
import matplotlib.pyplot as plt
sns.countplot(x="Exited",data=df)
plt.title("Target Distribution (Exited = Churn)")
plt.xlabel("Exited (1=Churn, 0=Stay)")
plt.ylabel("Count")
plt.show()
plt.figure(figsize=(6,4))
sns.histplot(data=df,x="Age",hue="Exited",multiple="stack", palette="viridis")
plt.title("Age Distribution by Exited")
plt.show()
plt.figure(figsize=(6,4))
sns.histplot(data=df,x="Balance",hue="Exited",bins=30, multiple="stack", palette="viridis")
plt.title("Balance Distribution by Exited")
plt.show()
plt.figure(figsize=(6,4))
sns.histplot(data=df,x="Geography",hue="Exited",multiple="stack", palette="viridis")
plt.title("Churn by Geography")
plt.show()
plt.figure(figsize=(6,4))
sns.countplot(x="Gender", hue="Exited", data=df, palette="viridis")
plt.title("Churn by Gender")
plt.show()
view raw Visualize.py hosted with ❤ by GitHub

Feature Encoding

Model จะทำงานได้กับข้อมูลที่เป็นตัวเลข เพราะฉะนั้นเราจึงต้องแปลงข้อมูลที่เป็น String ให้เป็นตัวเลข ด้วยเทคนิคที่เรียกว่าการทำ Encoding

คอลัมน์ Gender ใช้การ Label Encoding (e.g 0 = Male, 1 = Female)

คอลัมน์ Geography ใช้วิธี One Hot Encoding เพราะ Geography เป็น Nominal categorical variable (ไม่มีลำดับชั้น)

One Hot Encoding คืออะไร

One-Hot Encoding = วิธีแปลงข้อมูล หมวดหมู่ (categorical) ให้เป็น ตัวเลขแบบ binary โดยสร้าง 1 คอลัมน์ต่อ 1 หมวด

เช่น คอลัมน์ Geography ที่มีค่า {France, Germany, Spain} → จะกลายเป็น 3 คอลัมน์:

FranceGermanySpain
100
010
001

ข้อดี: ไม่มีการบอกลำดับผิด ๆ เหมือน Label Encoding
ใช้ได้ดีกับ nominal categorical ที่มีจำนวน category ไม่เยอะ

ถ้าใช้ Label Encoding โมเดลอาจ เข้าใจผิดว่า 0 < 1 < 2 มีความหมายเชิงลำดับ

from sklearn.preprocessing import LabelEncoder
import pandas as pd
encoder = LabelEncoder()
df["Gender"] = encoder.fit_transform(df["Gender"])
df = pd.get_dummies(data=df,columns=["Geography"]).astype("int")
view raw encoding.py hosted with ❤ by GitHub

Reference: What is One Hot Encoding and How to Implement it in Python? | Codecademy


พอ Feature ของเราพร้อมที่จะนำมาเทรนโมเดลแล้ว

ML ที่เราจะมาใช้ในวันนี้คือ Randomforest เป็นโมเดลจำพวก Rule Base

Split Data

แยกส่วนที่เป็น Feature กับ Target ออกจากกัน

Recap อีกที ตัวแปร X มี (‘CreditScore’, ‘Gender’, ‘Age’, ‘Tenure’, ‘Balance’, ‘NumOfProducts’, ‘HasCrCard’, ‘IsActiveMember’, ‘EstimatedSalary’, ‘Geography_France’, ‘Geography_Germany’, ‘Geography_Spain’)

ตัวแปร y คือ(Exited)

X = df.drop(columns="Exited")
y = df["Exited"]
view raw Split_X_y.py hosted with ❤ by GitHub

Split Data ออกเป็น Train set กับ Test set

การแสดงแยกข้อมูลทั้งหมดออกเป็นชุดข้อมูลสำหรับฝึกอบรมและทดสอบ โดยมีข้อมูลฝึกอบรม 1,000 แถวและข้อมูลทดสอบ 300 แถว
Image from Data Science Bootcamp11 by DataRockie
from sklearn.model_selection import train_test_split
x_train,x_test,y_train,y_test = train_test_split(X,y,test_size=0.2,stratify=y)

Fitting Our Model

from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier()
model = rf.fit(x_train,y_train)
view raw fit_model.py hosted with ❤ by GitHub

Finally !!

Model Evaluation

เมื่อเราได้ Model แล้วขั้นตอนต่อมาคือเอาไป Fit กับ Test Set ที่เรา Split ไว้ตอนแรก

y_pred = rf.predict(x_test)
view raw pred.py hosted with ❤ by GitHub

ต่อไปคือการเอา y_pred ที่เราทำนายได้ ไปเทียบกับ y_test ซึ่งคือค่าจริงๆของมัน เพื่อดูว่า model ของเราทายถูก/ผิด เท่าไหร่

from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score
y_pred = rf.predict(x_test)
y_proba = rf.predict_proba(x_test)[:,1]
print("Classification Report:\n", classification_report(y_test, y_pred))
print("Confusion Matrix:\n", confusion_matrix(y_test, y_pred))
print("ROC-AUC:", roc_auc_score(y_test, y_proba))

Result:

Classification Report:
precision recall f1-score support
0 0.91 0.88 0.89 1593
1 0.58 0.66 0.62 407
accuracy 0.83 2000
macro avg 0.75 0.77 0.76 2000
weighted avg 0.84 0.83 0.84 2000
Confusion Matrix:
[[1400 193]
[ 137 270]]
ROC-AUC: 0.864136864136864

Confusion Matric บอกอะไรบ้าง คร่าวๆ?

True Negative (TN) = 1400 → ทำนายถูกว่าไม่เลิก

False Positive (FP) = 193 → ทำนายผิดว่าลูกค้าจะเลิก แต่จริง ๆ อยู่ต่อ

False Negative (FN) = 137 → ทำนายผิดว่าลูกค้าจะอยู่ แต่จริง ๆ เลิก

True Positive (TP) = 270 → ทำนายถูกว่าเลิก

Accuracy = 0.83 (83%)
→ ฟังดูสูง แต่ต้องระวัง เพราะ dataset imbalanced (ส่วนใหญ่เป็น class 0)


Variable Important

ต่อไปเราจะมาดู ว่าตัวแปรไหนส่งผลต่อ Churn Rate มาก->น้อย ที่สุด

import matplotlib.pyplot as plt
import seaborn as sns
# ดูความสำคัญของตัวแปร
importances = rf.feature_importances_
features = X.columns
# สร้าง DataFrame สำหรับเรียงลำดับ
feat_imp = pd.DataFrame({"Feature": features, "Importance": importances})
feat_imp = feat_imp.sort_values(by="Importance", ascending=False)
# Plot
plt.figure(figsize=(10,6))
sns.barplot(x="Importance", y="Feature",hue="Feature", data=feat_imp, palette="viridis")
plt.title("Feature Importance (Random Forest)")
plt.show()
view raw VarImp.py hosted with ❤ by GitHub

Insights จากการวิเคราะห์ Churn

  1. อายุ (Age)
    • ลูกค้าที่มีอายุมากกว่า 40–50 ปีขึ้นไปมีแนวโน้ม churn สูงกว่า กลุ่มอายุน้อย
    • สะท้อนว่า segment ลูกค้ารุ่นใหญ่ควรได้รับการดูแลพิเศษ เช่น บริการส่วนบุคคล
  2. NumOfProducts (จำนวนผลิตภัณฑ์ที่ถือครอง)
    • ลูกค้าที่มี เพียง 1 ผลิตภัณฑ์ มีแนวโน้ม churn สูง
    • ในขณะที่ผู้ที่ถือครองหลายผลิตภัณฑ์ (cross-sell) มี churn ต่ำกว่า → Cross-selling ช่วยลด churn
  3. Balance (ยอดเงินในบัญชี)
    • ลูกค้าที่มี ยอดเงินคงเหลือสูง (Balance สูง) มีโอกาส churn มากกว่ากลุ่มที่ balance เป็นศูนย์
    • ชี้ให้เห็นว่าลูกค้ากลุ่มนี้อาจมีบัญชี/บริการทางการเงินกับธนาคารอื่น → เสี่ยงย้าย

สรุปเชิงกลยุทธ์

  • ควรเน้น Retention Program ไปที่
    • ลูกค้าอายุ 40–60 ปี
    • ลูกค้าที่ balance สูงแต่มีผลิตภัณฑ์น้อย
    • ลูกค้าที่ inactive
    • ลูกค้าใน Germany
  • กลยุทธ์ที่ควรทำ เช่น
    • Cross-sell/Up-sell ให้ลูกค้า balance สูงถือหลายผลิตภัณฑ์
    • Campaign สำหรับลูกค้า inactive → โปรโมชันใช้งานครั้งแรก/คืนค่าธรรมเนียม
    • Customer experience สำหรับลูกค้าอายุสูง เช่น call center เฉพาะกลุ่ม


Reference

Data Science Bootcamp 11 -Datarookie

Feature Engineering | Codecademy

Comments

Leave a Reply

Discover more from PUAY101

Subscribe now to keep reading and get access to the full archive.

Continue reading