Trần Quốc Việt
← All posts

2026-04-23

Banking Domain

Reject Inference: Mô hình của bạn chưa bao giờ nhìn thấy người bị từ chối

Model học từ người được duyệt. Người bị từ chối? Model không biết họ sẽ trả nợ ra sao. Đây là bias có hệ thống — và hầu hết team không thừa nhận nó.

View
Lang

Tóm tắt

Mọi credit model đều được train trên approved population — những người đã được duyệt vay và có outcome thực tế (trả nợ đúng hạn hay quá hạn). Người bị từ chối? Không có outcome. Model không biết họ thuộc nhóm good hay bad.

Đây không phải lỗi kỹ thuật — đây là cấu trúc cơ bản của bài toán credit scoring. Và nó có hậu quả nghiêm trọng nếu bị bỏ qua.


Bác sĩ chỉ biết bệnh của người đến phòng khám

Hãy tưởng tượng một bác sĩ chỉ điều trị người tự đến phòng khám. Sau 5 năm, bác sĩ đó có rất nhiều kinh nghiệm về người bệnh nhẹ đủ khỏe để đi lại. Nhưng người bệnh nặng nằm ở nhà không đến được — bác sĩ chưa bao giờ thấy họ.

Nếu bác sĩ đó bắt đầu chẩn đoán từ xa, mô hình của họ sẽ có systematic blind spot với các triệu chứng nặng, vì không bao giờ có training data về nhóm đó.

Credit scoring hoạt động y hệt:

  • Ngân hàng approve 60% applicant
  • 60% đó được theo dõi: ai trả nợ, ai không
  • 40% bị reject: không có outcome — model không biết gì về họ
  • Model tiếp theo được train trên 60% đó → tiếp tục reject 40% tương tự → vòng lặp

Tại sao điều này nguy hiểm — ví dụ cụ thể

Giả sử trong 40% bị reject, thực ra có 20% là good borrowers — người trả nợ tốt nếu được approve. Nhưng họ bị reject vì model học từ dữ liệu lịch sử thiên lệch.

Năm tiếp theo, model mới vẫn train trên approved population. 20% good borrowers đó vẫn bị reject. Năm sau nữa, tương tự. Ngân hàng đang mất một lượng khách hàng tốt có hệ thống, không phải ngẫu nhiên.

Chiều ngược lại cũng nguy hiểm: nếu ngân hàng nới lỏng policy (approve thêm nhóm borderline) mà không làm reject inference, model không biết gì về rủi ro thực tế của nhóm đó. Approval rate tăng, NPL tăng theo mà không có early warning.


Ba cách tiếp cận reject inference

Không có giải pháp hoàn hảo — đây là ước lượng, không phải sự thật. Nhưng có ba cách tiếp cận phổ biến:

1. Augmentation (Hard Augmentation)

Cách làm: Gán tất cả reject = bad, thêm vào training set, train lại model.

Khi nào dùng: Khi không có gì khác tốt hơn, khi reject rate thấp (<20%), khi model cần thiên về conservative.

Nhược điểm: Overestimate risk của reject population. Nếu 50% reject thực ra là good, bạn đang mislabel hàng loạt. Model sẽ conservative hơn cần thiết.

Bias tạo ra: Model ngày càng tight hơn theo thời gian vì nó "học" rằng reject = bad.

2. Fuzzy Augmentation (Soft Augmentation)

Cách làm: Thay vì gán reject = bad, gán mỗi reject một xác suất bad dựa trên score hiện tại. Applicant có score thấp được gán xác suất bad cao; applicant score trung bình được gán xác suất thấp hơn.

Khi nào dùng: Khi có model cũ đủ tin cậy để estimate probability; khi reject rate vừa phải.

Nhược điểm: Circular reasoning. Bạn dùng score của model cũ (có bias) để label reject → train model mới → model mới cũng có bias đó → dùng model mới để label reject lần sau → ...

Fuzzy augmentation không loại bỏ bias — nó dilute nó, và đôi khi làm nó trở nên tinh vi hơn và khó phát hiện hơn.

3. Parceling (Through-the-Door Approach)

Cách làm: Dùng external data (credit bureau, alternative data) để ước lượng outcome cho reject population mà không cần dựa vào score nội bộ.

Khi nào dùng: Khi có access vào bureau data quality tốt; khi reject rate cao (>30%); khi muốn đánh giá toàn bộ "through-the-door" population.

Ưu điểm: Phá vỡ vòng circular reasoning vì dùng nguồn data độc lập.

Nhược điểm: Tốn kém (bureau data có giá); không phải thị trường nào cũng có bureau coverage tốt; vẫn là ước lượng.


Circular reasoning trap — minh hoạ

Đây là vòng lặp nguy hiểm nhất trong reject inference:

Score v1 được deploy → reject 40% applicant
↓
Dùng score v1 để fuzzy-label reject
↓
Train score v2 trên approved + fuzzy-labeled reject
↓
Score v2 cũng reject xấp xỉ 40% tương tự
↓
Dùng score v2 để fuzzy-label reject mới
↓
Train score v3 ... (lặp lại)

Mỗi iteration, model học bias của iteration trước. Sau 3–4 cycle, model đã "quên" mất rằng nó ban đầu không biết gì về reject population — nó chỉ biết những gì chính nó đã assume về họ.

Cách phát hiện: so sánh reject rate và score distribution của từng model generation. Nếu reject rate ngày càng tăng và score distribution ngày càng tight, đó là dấu hiệu của circular bias tích lũy.


Khi nào reject inference quan trọng nhất

Không phải lúc nào cũng cần làm reject inference đầy đủ. Ưu tiên khi:

Tình huốngMức độ cần thiết
Reject rate > 30%🔴 Cao — phần lớn population không được observe
Mở segment hoặc product mới🔴 Cao — không có historical data cho segment mới
Policy nới lỏng đáng kể🔴 Cao — approve nhóm trước đây bị reject
Reject rate < 15%, policy ổn định⚠️ Trung bình — bias nhỏ, impact có thể chấp nhận được
Chỉ update features, không thay đổi population🟢 Thấp — bias tương đối ổn định

Code thực tế: Ba cách tiếp cận reject inference với Python

python
import pandas as pd
import numpy as np
from sklearn.linear_model import LogisticRegression

approved = df[df['decision'] == 'approved'].copy()
rejected = df[df['decision'] == 'rejected'].copy()
features = ['monthly_income', 'age', 'dpd_max_6m', 'job_tenure_months']

# --- Cách 1: Hard Augmentation ---
# Đơn giản nhất, luôn khả thi, conservative
rejected_hard = rejected.copy()
rejected_hard['bad_flag'] = 1   # gán tất cả reject = bad

train_hard = pd.concat([approved, rejected_hard], ignore_index=True)
model_hard = LogisticRegression(C=0.1, max_iter=500)
model_hard.fit(train_hard[features], train_hard['bad_flag'])

# --- Cách 2: Fuzzy Augmentation ---
# Dùng score của model hiện tại để estimate bad probability cho reject
# ⚠️  Cảnh báo: circular reasoning nếu dùng lặp lại nhiều chu kỳ
rejected_fuzzy = rejected.copy()
rejected_fuzzy['bad_prob'] = model_v1.predict_proba(rejected[features])[:, 1]

X_augmented = pd.concat([approved[features], rejected_fuzzy[features]])
y_augmented = pd.concat([approved['bad_flag'],
                          pd.Series(np.ones(len(rejected_fuzzy)))])
# Dùng bad_prob làm sample_weight, không hard-label
weights = pd.concat([pd.Series(np.ones(len(approved))),
                     rejected_fuzzy['bad_prob']])

model_fuzzy = LogisticRegression(C=0.1, max_iter=500)
model_fuzzy.fit(X_augmented, y_augmented, sample_weight=weights)

# --- Monitor circular bias qua các generation ---
# Nếu reject rate tăng dần  dấu hiệu bias tích lũy
for gen_name, model in [('v1', model_v1), ('hard_aug', model_hard), ('fuzzy', model_fuzzy)]:
    scores   = model.predict_proba(applicant_pool[features])[:, 1]
    rej_rate = (scores > cutoff_threshold).mean()
    print(f"Model {gen_name}: reject rate = {rej_rate:.2%}")

# --- Cách 3: Parceling  dùng bureau data để break circular reasoning ---
# Nếu  credit bureau score cho rejected applicants
rejected_parceling = rejected.copy()
rejected_parceling['bad_flag'] = (rejected_parceling['bureau_score'] < 500).astype(int)

train_parceling = pd.concat([approved, rejected_parceling], ignore_index=True)
model_parceling = LogisticRegression(C=0.1, max_iter=500)
model_parceling.fit(train_parceling[features], train_parceling['bad_flag'])

Điều honest cần nói

Reject inference không phải là vấn đề có solution hoàn hảo. Mọi approach đều là ước lượng với các assumption khác nhau. Điều quan trọng không phải là "giải quyết" hoàn toàn — mà là:

  1. Acknowledge vấn đề trong model documentation
  2. Document approach được chọn và lý do
  3. Monitor reject rate và score distribution theo thời gian
  4. Sensitize kết quả: "Nếu 20% reject thực ra là good, impact lên model là gì?"

Một model documentation viết "chúng tôi biết reject inference bias tồn tại, đây là cách chúng tôi handle và monitor nó" credible hơn rất nhiều so với documentation không đề cập gì đến reject population.


Pitfalls phổ biến

Bỏ qua hoàn toàn. "Chúng ta không có data về reject nên không làm gì được." Sai. Hard augmentation là đơn giản nhất và luôn khả thi.

Dùng fuzzy augmentation mà không nhận ra circular reasoning. Kết quả trông "đẹp" vì model mới consistent với model cũ — nhưng đó là vì chúng cùng học một bias.

Không document approach. 18 tháng sau, model mới được train. Người mới không biết reject được handle như thế nào trong cycle trước. Inconsistency tích lũy.

Không theo dõi reject rate theo thời gian. Nếu reject rate tăng dần từ 30% lên 45% qua các năm mà không có lý do kinh doanh rõ ràng, đó là tín hiệu của circular bias tích lũy.


Takeaway

Mọi credit model đều có selection bias — đây không phải điều đáng xấu hổ, nó là đặc tính cơ bản của bài toán.

Practitioner giỏi là người biết điều đó, quantify nó, document nó, và monitor nó — không phải người nghĩ model của mình "sạch" chỉ vì AUC trên approved population trông tốt.

Reject inference là bài kiểm tra về intellectual honesty của một data scientist trong credit risk: bạn có sẵn sàng thừa nhận những gì model của mình không biết không?


Bài liên quan / Related posts