[심화] 거절한 사람은 어디로 갔을까: 거절 추론과 rejectkit
신용 모델은 승인한 사람의 결과만 보고 배우지만, 정작 평가는 거절자를 포함한 전체 신청자에게 합니다. 이 표본 선택 편향을 보정하는 거절 추론 기법 여덟 가지를 한 API로 묶고, 그 보정이 내 데이터에서 실제로 도움이 되는지까지 재주는 파이썬 라이브러리 rejectkit을 만들어 공개했습니다.
Part 4에서 거절 추론(reject inference)을 짧게 다뤘습니다. 승인한 고객만 보고 만든 모델을 전체 신청자에게 적용하면 편향된다는 이야기였죠. 이번 글은 그 거절 추론을 직접 코드로 작성한 기록입니다. 고전 기법들을 한데 묶고, 무엇보다 “이 보정이 내 데이터에서 실제로 도움이 되는지”까지 평가해 주는 라이브러리 rejectkit을 만들어 PyPI와 GitHub에 공개했습니다.
거절자의 결과는 영원히 모릅니다
대출 심사를 떠올려보세요. 신청자가 오면 정보를 보고 승인하거나 거절합니다. 승인한 사람은 몇 달 뒤 결과를 압니다. 잘 갚으면 정상, 연체하면 부도입니다. 그런데 거절한 사람은 대출을 안 줬으니 갚을 일도 없습니다. 결과를 영원히 알 수 없죠.
문제는 여기서 시작됩니다. 내년에 들어올 신청자가 연체할지 예측하는 모델을 만들려면 정답이 있는 데이터가 필요한데, 그건 승인자뿐입니다. 그런데 승인자는 애초에 괜찮아 보여서 통과된, 한쪽으로 치우친 표본입니다. 이들만으로 학습한 모델은 실제로 문 앞에 오는 전체 신청자와 분포가 어긋납니다.
다시 찾아온 환자만 보고 “내 치료법은 효과가 좋다”고 결론 내리는 의사와 같습니다. 효과가 없어 다시 안 온 환자는 데이터에 안 잡히니까요. Part 0에서 본 선택편향, 그리고 Part 4에서 본 거절 추론이 바로 이 이야기입니다.
그런데 파이썬 도구가 비어 있었습니다
거절 추론은 신용 리스크 업계에서 수십 년 된 표준 주제입니다. 그런데 막상 파이썬 도구가 비어 있었습니다.
R에는 scoringTools라는 패키지가 있지만 CRAN에도 없이 GitHub에만 있습니다. 파이썬의 스코어카드 라이브러리들(scorecardpy, optbinning, toad)은 Part 4에서 본 WOE/IV 비닝과 로지스틱 스코어카드는 잘 하지만, 거절 추론은 아예 다루지 않습니다. 남은 건 논문용 일회성 연구 코드뿐이었고요.
그래서 rejectkit을 만들었습니다. 목표는 둘이었습니다. 하나는 고전 기법 여덟 가지를 scikit-learn 스타일 한 API로 묶는 것입니다. 다른 하나는, 더 중요한 건데, “이 보정이 내 데이터에서 실제로 도움이 되는가”를 재는 벤치마크를 제공하는 것입니다. 사실 거절 추론은 “효과가 의문스럽다, 어떤 기법도 항상 우월하지 않다”가 학계의 오랜 결론입니다. 1993년에 이미 “거절 추론이 과연 통할 수 있는가”라는 제목의 논문이 나왔을 정도죠. 그러니 믿고 쓰지 말고 먼저 평가해 보자는 게 이 라이브러리의 진짜 메시지입니다.
여덟 가지 기법, 그리고 핵심 가정
거절자를 다시 학습에 포함하는 방식은 크게 세 갈래입니다.
첫째, 거절자에게 라벨을 만들어 채워 넣는 방식입니다(augmentation 계열). 승인 모델로 거절자 점수를 매겨 컷오프로 잘라 라벨을 붙이거나(simple), 한 명을 good 버전과 bad 버전 두 줄로 나눠 가중치와 함께 넣거나(fuzzy), 점수 구간별 부도율에 가중치를 곱해 “같은 점수라도 거절자는 더 나쁠 것”이라는 실무 가정을 숫자로 넣거나(parcelling), 특징이 비슷한 승인자 이웃의 부도율을 빌려옵니다(extrapolation).
둘째, 라벨을 만들지 않고 보정하는 방식입니다. 승인과 거절을 가르는 선택 모델을 학습해 승인자에게 역가중치를 주거나(IPW reweighting, Part 2에서 본 편향 보정과 같은 논리입니다), 계량경제학의 Heckman 통제함수를 분류 문제에 맞춰 추가 피처로 넣습니다.
셋째, 준지도 학습입니다. 거절자를 라벨 없는 데이터로 두고, 확신이 높은 것만 의사 라벨을 붙여 재학습하는 과정을 반복합니다(self-training).
어떤 기법이 통하느냐는 결국 한 가지 가정에 달려 있습니다. 거절이 무엇에 의존하느냐입니다.
- 거절이 관측된 특징에만 의존하면(MAR), 잘 만든 모델은 사실 편향이 크지 않습니다.
- 거절이 관측되지 않은 결과에도 의존하면(MNAR), 예컨대 과거 심사자가 데이터에 없는 정보로 나쁜 사람을 걸러냈다면, 승인자만 본 모델이 가장 크게 편향됩니다.
여기에 중요한 한계가 있습니다. augmentation 계열은 편향된 승인 모델에 기대어 거절자 라벨을 추측하므로, 강한 MNAR 상황을 스스로 벗어나지 못합니다. 그래서 “어떤 기법을 쓸까”보다 “지금 내 상황에서 거절 추론이 도움이 되긴 하나”를 먼저 따지는 게 맞습니다.
도움이 되는지 어떻게 재나
거절 추론의 근본적 난점은 이겁니다. 거절자는 정답이 없어서, 보정이 잘 됐는지 직접 채점할 수가 없습니다.
rejectkit은 이걸 우회합니다. 정답을 다 아는 데이터를 가져와서, 일부를 일부러 거절자로 골라 정답을 가립니다(mask). 그리고 각 기법이 그 가린 정답을 얼마나 잘 복원하는지를, 건드리지 않은 테스트셋에서 채점합니다. 이름에 들어간 Masked가 이 정답 가리기입니다.
핵심 지표는 auc_recovery입니다. 0이면 승인자만 쓴 naive 모델과 동급, 1이면 전체 정답을 쓴 oracle만큼 회복, 음수면 오히려 악화입니다.
거절을 만드는 방식은 셋 중에 고릅니다. 특징에만 의존하는 mar, 숨은 결과까지 의존하는 mnar(가장 가혹), 예측 위험이 낮은 순으로 승인하는 cutoff(실제 신용 정책에 가장 가까움)입니다.
그래서 도움이 되나: 합성 데이터 vs 실데이터
깔끔한 합성 데이터에 MNAR로 돌려봤습니다. oracle이 0.820, naive가 0.749인데, 거절 추론 기법들은 naive를 거의 못 넘고 몇몇은 오히려 손해를 냅니다. fuzzy가 +0.001로 겨우 본전, parcelling은 -0.116으로 악화, Heckman만 간신히 naive 선을 지킵니다. 선택이 결과에 의존하는 MNAR에서는 이론이 예측하는 그대로입니다. 거절 추론은 공짜 점심이 아닙니다.
그럼 실데이터에서 확인해 봅시다. Kaggle Home Credit 데이터(약 30만 건, 부도율 8%)에 그대로 적용했습니다.
| 모델 | AUC | auc_recovery |
|---|---|---|
| oracle (전체 정답) | 0.741 | 1.00 |
| naive (승인자만) | 0.568 | 0.00 |
| parcelling | 0.582 | +0.084 |
| extrapolation | 0.582 | +0.079 |
| heckman | 0.580 | +0.071 |
여기서는 정반대입니다. 거절자를 무시하면 0.74에서 0.57로 크게 무너지고, 거절 추론이 그 손실의 7~8%를 실제로 회복합니다. 합성 데이터에서는 거의 무용했는데 실데이터에서는 쓸모가 있습니다. 그리고 같은 데이터라도 mar나 cutoff로 바꾸면 naive가 이미 oracle급이라 회복할 여지 자체가 없어집니다(auc_recovery가 NaN으로 나옵니다).
결론은 하나로 모입니다. 거절 추론이 도움이 되는지는 데이터에 달렸고, 그래서 쓰기 전에 벤치마크로 확인해야 합니다.
사용법
한 모델에 거절 추론을 입히는 건 이렇게 합니다.
from sklearn.linear_model import LogisticRegression
from rejectkit import RejectInferenceClassifier
# X_accept, y_accept: 승인자와 결과(1=부도) / X_reject: 거절자(특징만)
clf = RejectInferenceClassifier(
estimator=LogisticRegression(max_iter=1000),
method="parcelling",
method_params={"uplift": 1.3}, # 거절자는 같은 점수대보다 ~30% 더 나쁘다고 가정
)
clf.fit(X_accept, y_accept, X_reject)
pd_bad = clf.predict_proba(X_new)[:, 1]
도움이 되는지 먼저 재보는 건 한 줄입니다.
from rejectkit import MaskedRejectBenchmark
bench = MaskedRejectBenchmark(selection="mnar", accept_rate=0.6, random_state=0)
print(bench.compare(["fuzzy", "parcelling", "reweighting", "heckman"], X, y).round(4))
입력은 pandas, polars, numpy를 다 받습니다. 승인과 거절의 분포가 얼마나 다른지 보는 진단(변수별 PSI)과 시각화도 들어 있습니다.
정리
- 신용 모델을 승인자 데이터로만 학습하면 표본 선택 편향이 생깁니다. Part 0과 Part 4에서 본 그 문제입니다.
- rejectkit은 이를 보정하는 고전 기법 여덟 가지를 한 API로 묶었습니다.
- 진짜 차별점은 그 보정이 내 데이터에서 도움이 되는지 재는 벤치마크입니다. 거절 추론은 만능이 아니고 때로 해롭기까지 하니, 믿고 쓰지 말고 먼저 측정하자는 게 핵심입니다.
- 신용뿐 아니라 합격자만 결과를 보는 모든 선택 편향 상황에 같은 원리가 적용됩니다. 채용 성과나, 플래그된 건만 라벨링되는 사기 탐지 같은 것들입니다.
그리고 거절 추론의 가장 믿을 수 있는 답은 사실 따로 있습니다. 소수를 일부러 무작위로 승인해 진짜 결과를 확보하는 실험입니다. 추정이 아니라 사실을 주니까요. 그 이야기는 Part 6에서 인과추론과 실험으로 이어집니다.
설치는 한 줄입니다.
pip install rejectkit
- GitHub: github.com/HangilKim11/rejectkit
- PyPI: pypi.org/project/rejectkit
피드백, 이슈, PR 환영합니다.