본문 바로가기
/ 학습 / 안내서 / 예측·분석
L3 심화 · 7분 읽기

예측 모델 운영, 드리프트 감지와 재학습 시점

예방연구팀 2026.07.03 갱신 예측 모델을 만들어 운영 단계에 들어선 분석 담당자
#예측#드리프트#모니터링#재학습

전편에서 위험도 예측 모델을 만들었다면, 이제 다른 종류의 문제가 시작됩니다. 모델은 배포한 다음 날부터 낡기 시작합니다. 재개발로 건물 분포가 바뀌고, 기후가 밀려 건조기가 이동하고, 신고 행태가 달라집니다. 코드가 그대로여도 세상이 변하면 모델은 틀리기 시작하는데 — 오류 메시지는 없습니다. 지난달과 똑같이 그럴듯한 위험 지도가 나올 뿐입니다.

이 안내서를 끝내면 배포 전에 롤링 백테스트로 "이 모델의 평상시 성능 범위"를 재고, 운영 중 입력 분포 변화(드리프트)를 PSI로 조기 감지하고, 막연한 주기 대신 명확한 조건으로 재학습 시점을 정할 수 있습니다.

낡음을 알리는 3가지 신호

신호무엇을 보나성격
1 성능 하락상위 K% 포착률의 추세확실하지만 늦다 — 화재가 나야 계산된다
2 입력 드리프트피처 분포의 변화(PSI)간접적이지만 빠르다 — 즉시 계산된다
3 캘린더계절 전환·데이터 소스 개편예측 가능 — 달력에 미리 적는다

셋 중 하나만 보면 놓칩니다. 성능만 보면 몇 달 늦고, 드리프트만 보면 오탐에 시달리고, 캘린더만 보면 갑작스러운 변화를 놓칩니다.

1. 백테스트 — 배포 전에 운영을 리허설한다

전편의 시간 분할 검증은 한 번의 시험입니다. 운영 판단에는 그걸로 부족합니다 — 기준 시점을 옮겨 가며 여러 번 반복해, 성능이 원래 얼마나 출렁이는지부터 재야 합니다. 이 범위가 없으면 운영 중 점수가 떨어졌을 때 "정상 변동"인지 "진짜 하락"인지 구분할 수 없습니다.

PYTHON
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)입니다. 두 분포를 같은 구간으로 나눠 비율 차이를 합산한 값입니다.

PYTHON
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 발동)
💡 팁
PSI가 튀면 먼저 데이터 쪽 사고부터 의심하세요. 기상 API 단위 변경, 건축물대장 스키마 개편, 결측 급증 같은 수집·정제 단계의 문제가 드리프트로 위장하는 경우가 가장 흔합니다. 세상이 변한 게 아니라 파이프라인이 깨진 것이면 재학습이 아니라 수리가 답입니다.

3. 재학습 — 주기가 아니라 조건으로

"분기마다 재학습"은 기억하기 좋지만, 조용한 분기엔 낭비이고 급변한 달엔 늦습니다. 조건으로 정합니다.

재학습은 "다시 fit 한 번"이 아니라 작은 프로젝트입니다. 전편의 절차를 다시 밟습니다 — 시간 분할 검증, 베이스라인 비교, 그리고 이전 모델과 같은 검증 기간에서 나란히 비교해 이길 때만 교체합니다. 교체할 때는 학습 데이터 기간·코드 버전·검증 점수를 기록으로 남깁니다.

모델 카드 — 한 장짜리 신분증

담당자가 바뀌어도 모델이 설명 가능하도록, 카드 한 장을 모델과 함께 보관합니다.

TEXT
[모델 카드] 화재 위험도 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
트리거 발동 시재학습 + 신구 모델 비교새 모델이 같은 기간에서 우세할 때만 교체
재학습 때마다피드백 루프 편향 재점검점검 이력 분포 함께 확인
▸ 소방 활용 포인트
순찰·예방점검 배치의 근거가 되는 위험 지도가 "언제까지 믿을 수 있는 지도인지"를 숫자로 관리합니다. 포착률 밴드와 PSI 경보는 "모델을 계속 써도 되는가"라는 질문에 감이 아닌 기록으로 답하게 해 주고, 모델 카드는 담당자가 바뀌어도 판단 기준을 물려줍니다.

사람 검토 체크리스트

TEXT
- [ ] 롤링 백테스트로 기대 성능 범위(평균±표준편차)를 배포 전에 쟀습니다.
- [ ] 상위 K% 포착률을 매월 기록하고 기대 밴드와 비교합니다.
- [ ] 핵심 피처의 PSI를 매월 계산하고, 튀면 파이프라인 사고부터 확인합니다.
- [ ] 재학습 트리거 3종(성능·드리프트·캘린더)이 문서화돼 있습니다.
- [ ] 새 모델은 이전 모델과 같은 기간에서 비교해 이길 때만 교체합니다.
- [ ] 모델 카드에 데이터 기간·기대 성능·한계·담당자·이력이 적혀 있습니다.
⚠️ 흔한 실수
드리프트 지표도 대리 신호일 뿐입니다 — PSI가 조용해도 세상은 변할 수 있습니다. 그리고 전편의 피드백 루프 편향은 재학습을 반복할수록 오히려 굳어질 수 있으니, 재학습 때마다 점검 이력 분포를 함께 봐야 합니다. 낡은 모델을 억지로 쓰는 것보다 "이 모델은 은퇴시킨다"고 인정하는 쪽이 언제나 안전합니다.