전편에서 위험도 예측 모델을 만들었다면, 이제 다른 종류의 문제가 시작됩니다. 모델은 배포한 다음 날부터 낡기 시작합니다. 재개발로 건물 분포가 바뀌고, 기후가 밀려 건조기가 이동하고, 신고 행태가 달라집니다. 코드가 그대로여도 세상이 변하면 모델은 틀리기 시작하는데 — 오류 메시지는 없습니다. 지난달과 똑같이 그럴듯한 위험 지도가 나올 뿐입니다.
낡음을 알리는 3가지 신호
| 신호 | 무엇을 보나 | 성격 |
|---|---|---|
| 1 성능 하락 | 상위 K% 포착률의 추세 | 확실하지만 늦다 — 화재가 나야 계산된다 |
| 2 입력 드리프트 | 피처 분포의 변화(PSI) | 간접적이지만 빠르다 — 즉시 계산된다 |
| 3 캘린더 | 계절 전환·데이터 소스 개편 | 예측 가능 — 달력에 미리 적는다 |
셋 중 하나만 보면 놓칩니다. 성능만 보면 몇 달 늦고, 드리프트만 보면 오탐에 시달리고, 캘린더만 보면 갑작스러운 변화를 놓칩니다.
1. 백테스트 — 배포 전에 운영을 리허설한다
전편의 시간 분할 검증은 한 번의 시험입니다. 운영 판단에는 그걸로 부족합니다 — 기준 시점을 옮겨 가며 여러 번 반복해, 성능이 원래 얼마나 출렁이는지부터 재야 합니다. 이 범위가 없으면 운영 중 점수가 떨어졌을 때 "정상 변동"인지 "진짜 하락"인지 구분할 수 없습니다.
import pandas as pd # 롤링 백테스트: 기준월을 한 달씩 밀며 '과거로 학습 → 다음 달 검증' 반복 scores = [] for cut in pd.period_range("2025-01", "2025-12", freq="M"): train = df[df["기간"] < str(cut)] test = df[df["기간"] == str(cut)] m = fit_model(train) # 전편의 모델 학습 그대로 topk = predict_topk(m, test, k=0.10) # 위험 상위 10% 구역 hit = test[test["격자"].isin(topk)]["화재"].sum() / max(test["화재"].sum(), 1) scores.append({"기준월": str(cut), "포착률": hit}) bt = pd.DataFrame(scores) print(bt["포착률"].mean(), bt["포착률"].std()) # 예: 평균 0.61 ± 0.08
이렇게 얻은 "평시 0.61±0.08" 같은 범위가 운영 대시보드의 기대 밴드가 됩니다. 이 밴드 밑으로 내려가는 것이 신호 1입니다.
2. 드리프트 감시 — 성능은 늦고, 입력은 빠르다
포착률은 그 달의 화재가 실제로 나야 계산됩니다. 반면 입력 피처의 분포는 예측을 돌리는 순간 바로 비교할 수 있습니다. 학습 당시 분포와 지금 들어오는 분포가 벌어졌다면, 성능이 떨어지기 전에 경보를 받을 수 있습니다.
가장 널리 쓰는 잣대가 PSI(Population Stability Index)입니다. 두 분포를 같은 구간으로 나눠 비율 차이를 합산한 값입니다.
import numpy as np def psi(train_col, live_col, bins=10): """학습 시점 분포 대비 현재 분포의 이동량. 구간은 학습 분포 기준.""" edges = np.quantile(train_col.dropna(), np.linspace(0, 1, bins + 1)) edges[0], edges[-1] = -np.inf, np.inf p = np.histogram(train_col, bins=edges)[0] / max(len(train_col), 1) q = np.histogram(live_col, bins=edges)[0] / max(len(live_col), 1) p, q = np.clip(p, 1e-4, None), np.clip(q, 1e-4, None) # 0 나눗셈 방지 return float(np.sum((q - p) * np.log(q / p))) for col in ["건조지수", "건물나이", "최근출동"]: # 전편의 핵심 피처 print(col, round(psi(train_df[col], live_df[col]), 3))
| PSI | 해석(업계 관례 기준) | 대응 |
|---|---|---|
| < 0.1 | 안정 | 없음 |
| 0.1 ~ 0.25 | 주의 — 분포가 움직이는 중 | 원인 확인, 다음 달 재측정 |
| > 0.25 | 경보 — 학습 때와 다른 세상 | 재학습 검토(신호 2 발동) |
3. 재학습 — 주기가 아니라 조건으로
"분기마다 재학습"은 기억하기 좋지만, 조용한 분기엔 낭비이고 급변한 달엔 늦습니다. 조건으로 정합니다.
- 트리거 ① 성능: 상위 K% 포착률이 기대 밴드 하한 아래로 2기간 연속
- 트리거 ② 드리프트: 핵심 피처 중 하나라도 PSI > 0.25
- 트리거 ③ 캘린더: 난방기 진입·해제(계절 전환), 데이터 소스 개편, 행정구역·격자 변경
재학습은 "다시 fit 한 번"이 아니라 작은 프로젝트입니다. 전편의 절차를 다시 밟습니다 — 시간 분할 검증, 베이스라인 비교, 그리고 이전 모델과 같은 검증 기간에서 나란히 비교해 이길 때만 교체합니다. 교체할 때는 학습 데이터 기간·코드 버전·검증 점수를 기록으로 남깁니다.
모델 카드 — 한 장짜리 신분증
담당자가 바뀌어도 모델이 설명 가능하도록, 카드 한 장을 모델과 함께 보관합니다.
[모델 카드] 화재 위험도 v3 (2026-07 재학습) - 목적: 격자별 월간 화재 위험 우선순위 (상위 10% 예방점검 배치 참고) - 학습 데이터: 2022-01 ~ 2026-05 (출동·기상·건축물대장, 격자 500m) - 기대 성능: 상위 10% 포착률 0.61 ± 0.08 (롤링 백테스트 12회) - 알려진 한계: 신축 밀집 지역 과소평가 경향, 점검 이력 피드백 편향 - 재학습 트리거: 포착률 < 0.53 2개월 연속 / 핵심 피처 PSI > 0.25 / 계절 전환 - 담당: 예방연구팀 ○○○ · 이력: v1 2025-03, v2 2025-11, v3 2026-07
운영 루틴 한 장
| 언제 | 무엇을 | 통과 기준 |
|---|---|---|
| 매월 (화재 집계 확정 후) | 상위 K% 포착률 기록 | 기대 밴드 안 |
| 매월 (예측 직후) | 핵심 피처 PSI | 전부 < 0.25 |
| 트리거 발동 시 | 재학습 + 신구 모델 비교 | 새 모델이 같은 기간에서 우세할 때만 교체 |
| 재학습 때마다 | 피드백 루프 편향 재점검 | 점검 이력 분포 함께 확인 |
사람 검토 체크리스트
- [ ] 롤링 백테스트로 기대 성능 범위(평균±표준편차)를 배포 전에 쟀습니다. - [ ] 상위 K% 포착률을 매월 기록하고 기대 밴드와 비교합니다. - [ ] 핵심 피처의 PSI를 매월 계산하고, 튀면 파이프라인 사고부터 확인합니다. - [ ] 재학습 트리거 3종(성능·드리프트·캘린더)이 문서화돼 있습니다. - [ ] 새 모델은 이전 모델과 같은 기간에서 비교해 이길 때만 교체합니다. - [ ] 모델 카드에 데이터 기간·기대 성능·한계·담당자·이력이 적혀 있습니다.