Classifier Chain


Mô hình Classifier Chain cho dự báo rủi ro tín dụng với các mức DPD

Bối cảnh:
- Dữ liệu đặc trưng:
- Nhân khẩu học: Tuổi, thu nhập, tình trạng việc làm
- Lịch sử tín dụng: Dư nợ hiện tại, số lần trễ hạn trước đây, tỷ lệ sử dụng tín dụng
- Đặc trưng khoản vay: Số tiền vay, lãi suất, kỳ hạn, tỷ lệ nợ trên giá trị tài sản

Các nhãn này không loại trừ lẫn nhau. Ví dụ, nếu khách hàng trễ 90 ngày, họ chắc chắn đã trễ 30 và 60 ngày trước đó. Điều này tạo ra mối liên hệ chặt chẽ giữa các nhãn.


Ý tưởng chính của Classifier Chain:
Thay vì dự đoán từng nhãn một cách độc lập, mô hình sẽ dự đoán tuần tự và sử dụng kết quả dự đoán trước đó như một phần đầu vào cho bước tiếp theo. Điều này giúp mô hình tận dụng quan hệ giữa các nhãn.

Minh họa (mô tả ảnh):
Hãy tưởng tượng một sơ đồ dạng chuỗi:

        ┌────────────────┐
        │   Đặc trưng    │
        │  (Features)    │
        └───────┬────────┘
                │
                v
        ┌────────────────┐
        │ Classifier A    │
        │ Dự đoán DPD30+  │
        └───────┬────────┘
                │
         (Xác suất DPD30+)
                │
                v
        ┌────────────────┐
        │ Classifier B    │
        │ Dự đoán DPD60+  │
        │ (dựa trên       │
        │ đặc trưng +     │
        │ kết quả A)      │
        └───────┬────────┘
                │
         (Xác suất DPD60+)
                │
                v
        ┌────────────────┐
        │ Classifier C    │
        │ Dự đoán DPD90+  │
        │ (dựa trên       │
        │ đặc trưng +     │
        │ kết quả A, B)   │
        └───────┬────────┘
                │
         (Xác suất DPD90+)
                │
                v
              (Kết quả)

Trong sơ đồ trên, mỗi mũi tên thể hiện việc truyền thông tin: Dự đoán ở bước trước hỗ trợ bước sau.


Quy trình dự đoán:
1. Bước 1:
- Đầu vào: Đặc trưng khách hàng (nhân khẩu học, lịch sử tín dụng, khoản vay)
- Đầu ra A: Xác suất khách trễ ≥30 ngày (ví dụ: 0.7)

  1. Bước 2:
    • Đầu vào: Đặc trưng ban đầu + kết quả dự đoán DPD30+ (0.7)
    • Đầu ra B: Xác suất trễ ≥60 ngày (ví dụ: 0.4)
  2. Bước 3:
    • Đầu vào: Đặc trưng ban đầu + kết quả DPD30+ (0.7) + kết quả DPD60+ (0.4)
    • Đầu ra C: Xác suất trễ ≥90 ngày (ví dụ: 0.1)

Điểm nổi bật:
- Khai thác quan hệ giữa các nhãn:
Các mức trễ hạn cao (DPD90+) phụ thuộc logic vào khả năng trễ hạn ở mức thấp hơn (DPD30+, DPD60+).


import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.multioutput import ClassifierChain
from sklearn.model_selection import train_test_split

# Giả lập dữ liệu:
#  - X: ma trận đặc trưng, giả sử có 1000 khách hàng, mỗi khách có 10 đặc trưng.
#  - Y: ma trận nhãn, giả sử có 3 nhãn: DPD30+, DPD60+, DPD90+.
# Dữ liệu giả lập mang tính minh họa, trong thực tế bạn dùng dữ liệu thật.
np.random.seed(42)
X = np.random.rand(1000, 10)           # 1000 khách, 10 đặc trưng
Y = np.zeros((1000, 3))                # 3 nhãn: DPD30+, DPD60+, DPD90+

# Tạo nhãn giả lập dựa trên X. Ví dụ:
# - Nếu tổng một số đặc trưng > 5 thì khách có khả năng DPD30+,
# - DPD60+ và DPD90+ sẽ phụ thuộc vào DPD30+ (giả lập quan hệ nhãn).
threshold_30 = 5.0
threshold_60 = 5.5
threshold_90 = 6.0
sum_feats = X.sum(axis=1)

Y[:, 0] = (sum_feats > threshold_30).astype(int)  # DPD30+
Y[:, 1] = ((sum_feats > threshold_60) & (Y[:,0] == 1)).astype(int)  # DPD60+ (phụ thuộc vào DPD30+)
Y[:, 2] = ((sum_feats > threshold_90) & (Y[:,1] == 1)).astype(int)  # DPD90+ (phụ thuộc vào DPD60+)

# Chia tập dữ liệu
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.2, random_state=42)

# Khởi tạo mô hình LogisticRegression
base_lr = LogisticRegression()

# Tạo Classifier Chain
# Thứ tự nhãn: [DPD30+, DPD60+, DPD90+] tương ứng các cột Y[:,0], Y[:,1], Y[:,2]
chain = ClassifierChain(base_lr, order=[0, 1, 2], cv=None)

# Huấn luyện mô hình
chain.fit(X_train, Y_train)

# Dự đoán trên dữ liệu test
Y_pred = chain.predict(X_test)

# Đánh giá
from sklearn.metrics import accuracy_score, hamming_loss

# accuracy_score với multi-label sẽ tính số mẫu mà tất cả các nhãn dự đoán đúng
acc = accuracy_score(Y_test, Y_pred)
h_loss = hamming_loss(Y_test, Y_pred)  # Tỷ lệ nhãn sai trên tổng số nhãn

print("Accuracy (multi-label exact match):", acc)
print("Hamming Loss:", h_loss)

# In ra vài kết quả dự đoán
print("Dự đoán một vài khách hàng (DPD30+, DPD60+, DPD90+):")
for i in range(5):
    print("Thực tế:", Y_test[i], "Dự đoán:", Y_pred[i])
Accuracy (multi-label exact match): 0.925
Hamming Loss: 0.025
Dự đoán một vài khách hàng (DPD30+, DPD60+, DPD90+):
Thực tế: [0. 0. 0.] Dự đoán: [0. 0. 0.]
Thực tế: [0. 0. 0.] Dự đoán: [0. 0. 0.]
Thực tế: [0. 0. 0.] Dự đoán: [0. 0. 0.]
Thực tế: [0. 0. 0.] Dự đoán: [0. 0. 0.]
Thực tế: [0. 0. 0.] Dự đoán: [0. 0. 0.]
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.multioutput import ClassifierChain
from sklearn.metrics import cohen_kappa_score, roc_auc_score
from lightgbm import LGBMClassifier

np.random.seed(42)

X = np.random.rand(1000, 10)
Y = np.zeros((1000, 5))

sum_feats = X.sum(axis=1)
thresholds = [4.5, 5.0, 5.5, 6.0, 6.5]

Y[:, 0] = (sum_feats > thresholds[0]).astype(int)
Y[:, 1] = ((sum_feats > thresholds[1]) & (Y[:, 0] == 1)).astype(int)
Y[:, 2] = ((sum_feats > thresholds[2]) & (Y[:, 1] == 1)).astype(int)
Y[:, 3] = ((sum_feats > thresholds[3]) & (Y[:, 2] == 1)).astype(int)
Y[:, 4] = ((sum_feats > thresholds[4]) & (Y[:, 3] == 1)).astype(int)

X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.2, random_state=42)

# Thay LogisticRegression bằng LGBMClassifier
base_clf = LGBMClassifier(n_estimators=100, random_state=42, verbose=-1)

chain = ClassifierChain(base_clf, order=[0,1,2,3,4], cv=None)
chain.fit(X_train, Y_train)

Y_pred = chain.predict(X_test)
Y_pred_proba = chain.predict_proba(X_test)

kappas = []
ginis = []

for i in range(5):
    kappa_i = cohen_kappa_score(Y_test[:, i], Y_pred[:, i])
    kappas.append(kappa_i)
    
    proba_i = Y_pred_proba[i]
    # Kiểm tra xem có hai lớp (2 chiều) không
    if proba_i.ndim == 1 or proba_i.shape[1] == 1:
        # Nếu chỉ có một lớp
        ginis.append(0.0)
    else:
        auc_i = roc_auc_score(Y_test[:, i], proba_i[:, 1])
        gini_i = 2 * auc_i - 1
        ginis.append(gini_i)

mean_kappa = np.mean(kappas)
mean_gini = np.mean(ginis)

print("Kết quả đánh giá:")
for i in range(5):
    print(f"Nhãn {i}: Kappa = {kappas[i]:.4f}, Gini = {ginis[i]:.4f}")

print(f"Trung bình Kappa (macro): {mean_kappa:.4f}")
print(f"Trung bình Gini (macro): {mean_gini:.4f}")
Kết quả đánh giá:
Nhãn 0: Kappa = 0.7465, Gini = 0.0000
Nhãn 1: Kappa = 0.7375, Gini = 0.0000
Nhãn 2: Kappa = 0.7867, Gini = 0.0000
Nhãn 3: Kappa = 0.6131, Gini = 0.0000
Nhãn 4: Kappa = 0.1870, Gini = 0.0000
Trung bình Kappa (macro): 0.6142
Trung bình Gini (macro): 0.0000
print("Y.shape:", Y.shape)
print("Y_train.shape:", Y_train.shape)
Y.shape: (1000, 5)
Y_train.shape: (800, 5)