후보 모델
숫자 다듬기
보정 후처리
모델이 낸 위험 순서는 두고, 확률 눈금만 다시 맞춘다.
1. 왜 이 방법인가
Q. 만석 위험 순서를 잘 세웠는데 왜 손을 더 대나?
AUC가 높다는 말은 위험한 버스를 위에 놓았다는 뜻이다. 적어 둔 확률이 실제 만석 빈도와 맞는다는 뜻은 아니다.
코퍼스 보조 validation의 구간 검열 모형이 그랬다. 원예측 AUC는 .9748이었지만 ECE는 .0200이었다. isotonic을 얹자 AUC는 .9751로 거의 그대로였고, ECE는 .0053으로 줄었다. Brier는 .0284에서 .0268, log loss는 .0968에서 .0878로 내려갔다. 순위 신호는 이미 있었고 확률 눈금이 어긋나 있었다. ECE는 확률 구간별 예측과 실제 만석률의 차이를 표본 수로 가중한 값이다.
정류장×시간대 train 셀 833개 중 281개(33.73%)는 표본 10건 이하였다. 719개(86.31%)는 만석을 한 번도 못 봤다. 후보마다 이 얇은 셀을 다루는 법이 달라 원확률의 눈금도 달라졌다.
사람의 무게 순서는 맞히지만 모두 무겁게 찍는 저울과 같다. 저울을 새로 만들 필요는 없다. 눈금만 다시 맞추면 된다. 보정 후처리는 일곱 번째 모델이 아니다. 각 후보 위에 붙여 따로 겨루는 한 층이다.
보정만 좋다고 쓸 수도 없다. 평균만 내는 기준선은 ECE .0004였지만 AUC .5000이었고 만석을 전부 놓쳤다. 먼저 위험 순서를 가르고, 그다음 눈금을 본다.
다만 아무 모델에나 붙이면 좋아지는 층도 아니다. 경험적 베이즈의 ECE는 raw .0070에서 Platt .0134, isotonic .0131로 나빠졌다. GAM도 raw .0038이 Platt .0190, isotonic .0082보다 나았다. 그래서 공통 보정법은 아직 정하지 않았다.
train 날짜 안에서 만든 OOF 예측으로 보정기를 맞추고, 최종 validation 라벨은 흐름 밖에 둔다.
2. 원리 — 무엇을 어떻게 고치나
Q. 보정기는 무엇을 입력으로 받나?
입력은 후보가 낸 원 만석확률 p_full 하나다. 정류장, 시간, 앞차 잔여석을 다시 넣지 않는다. 그 정보는 이미 원확률 안에 들어 있다. 보정기는 p_full과 실제 출발 후 만석 빈도의 대응만 배운다. 마지막에 사용자용 탈 확률을 p_board = 1 - p_full로 바꾼다.
Q. Platt는 어떻게 고치나?
원확률을 로짓으로 바꾼 뒤 기울기와 절편 두 개를 붙인다.
한 번에 휘는 매끈한 S자 눈금이다. 표본이 비교적 적어도 모양이 쉽게 요동치지 않는다. 대신 중간 구간만 꺾이거나 양끝이 서로 다르게 틀어진 모양은 잘 못 잡는다.
Q. isotonic은 무엇이 다른가?
모양을 S자로 정하지 않는다. 원확률이 큰 사례의 보정값이 더 작아지지만 않게 묶고, 그 안에서 실제 만석률에 가장 가까운 단조 함수를 찾는다. 구간 검열 후보에서는 임계점 47개가 생겼다. 국소 굽힘을 잡는 대신 표본이 적은 구간에서는 넓은 평지와 급한 턱이 생긴다. 같은 평지에 묶인 사례는 순위도 동률이 된다.
Q. 모델이 이미 본 답으로 눈금을 맞춰도 되나?
안 된다. 학습일 안에서 날짜를 앞으로만 넘겼다. 앞선 날짜로 모델을 맞추고 다음 날짜를 예측해 OOF 원확률을 모았다. 보정기가 본 것은 10,236건, 만석 572건이다. 최종 검증 10,040건과 그중 만석 583건은 보정기에 들어가지 않았다.
하이퍼파라미터는 앞 3일 7,049건·만석 478건의 순방향 2개 fold에서 먼저 골랐다. 뒤 5,016건은 이 선택에서 뺐다. 보정용 OOF를 모은 뒤에는 기본 모델을 train 12,065건·만석 705건 전체에 다시 맞추고, 그 원확률에 보정 함수를 적용했다.
OOF 만석률 5.5881%와 전체 train 만석률 5.8433%의 차이는 가중치로 맞췄다. 만석 행 가중치는 1.045673, 비만석 행은 .997297이었다. isotonic의 정확한 0과 1은 OOF 표본 수에서 나온 Jeffreys 경계 .00004884와 .99995116으로만 잘랐다.
| 항목 | Platt | isotonic |
|---|---|---|
| 모양 | 기울기·절편 2개 | 자료가 만든 단조 구간 |
| 강점 | 적은 표본에서 비교적 안정 | 국소 왜곡을 따라감 |
| 약점 | 복잡한 꺾임을 놓침 | 희소 구간에서 턱·평지 |
| 순위 | 양의 기울기면 그대로 | 평지에서 동률 가능 |
Platt는 두 계수로 전체 눈금을 매끈하게 고치고, isotonic은 감소만 금지한 채 구간별로 고친다.
3. 숫자로 따라가기
Q. 실제 한 건은 어떻게 바뀌나?
2026년 8월 5일 07:39:09.882, 3330번 농수산물시장 61번 순번 사례다. 구간 검열 모형의 원 만석확률은 .496211이었다. 07:40:13.513 출발 관측은 잔여석 0, 실제 라벨은 만석이었다.
Platt 보정기는 이 후보의 OOF 자료에서 기울기 1.575053, 절편 .850292를 배웠다.
| 변형 | 만석확률 p_full | 탈 확률 1-p_full |
|---|---|---|
| raw | .496211 | .503789 |
| Platt | .695598 | .304402 |
| isotonic | .605123 | .394877 |
isotonic은 같은 원확률 .496211을 학습한 단조 눈금에서 .605123으로 옮겼다. 세 값은 한꺼번에 섞지 않는다. raw·Platt·isotonic을 독립 변형으로 평가한다. 이 한 건이 Platt의 승리를 뜻하지도 않는다. 구간 검열 후보의 전체 검증에서는 isotonic ECE .0053이 Platt .0093보다 낮았다.
4. 쓰는 재료
Q. 이 층이 직접 읽는 재료는 무엇인가?
| 재료 | 왜 쓰나 |
|---|---|
후보별 원 만석확률 p_full | 다시 맞출 눈금이다. 후보마다 별도 보정기를 둔다. |
| train 내부 순방향 OOF의 출발 후 만석 라벨 | 처음 보는 날의 원확률과 실제 빈도를 짝짓는다. 출발 후 잔여석 0만 만석이다. |
| 로컬 날짜 순서 | 과거로 미래 train 날짜를 예측하는 fold를 만든다. |
| train 전체 만석률 | OOF 구간과 전체 train의 만석률 차이를 가중치로 맞춘다. |
정류장·시간·앞차 상태·절대 잔여석은 기본 모델이 읽는다. 보정층은 이를 다시 보지 않는다.
Q. 무엇은 빼나?
| 아직 못 써본 재료 | 이유 |
|---|---|
| 최종 validation 라벨 | 보정 함수 선택과 적합에 쓰면 검증 답안을 미리 보는 누수다. |
| 정원·점유율 | 코퍼스 가명 차량키와 정원 사전의 교집합이 0/77이다. 관측 최대를 정원으로 대신하면 시간에 따라 값이 바뀐다. 라벨도 정원 없이 출발 후 0석만으로 정해진다. |
crowded 혼잡도 | 대상 노선 유형 11은 공식 제공 유형 13·15·23 밖이다. 직접 수집분에서 같은 잔여석에 다른 코드가 붙은 값이 26개라 잔여석의 파생값은 아니다. 다만 crowded만 바뀐 연속쌍은 코퍼스 81,147쌍·직접 수집분 1,934쌍 모두 0건이고 산식도 미확정이라 이 층에서는 넣지 않는다. |
queryTime | 직접 수집 92호출에서 queryTime은 전부 수신 시각보다 미래였고(중앙 +17.265초) 인접 호출 되감김도 16건이었다. HTTP Date는 queryTime 초 절삭과 92/92 일치해 독립 시계가 아니다. 보정 fold의 시간축으로 쓰지 않는다. |
| 후보 | raw | Platt | isotonic | 점추정 판정 |
|---|---|---|---|---|
| 경험적 베이즈 | .0070 | .0134 | .0131 | raw |
| 계층 로지스틱 | .0110 | .0099 | .0090 | 개선 CI에 0 포함 |
| GAM/스플라인 | .0038 | .0190 | .0082 | raw |
| 로지스틱+상호작용 | .0098 | .0094 | .0065 | isotonic, 개선 CI에 0 포함 |
| 구간 검열 | .0200 | .0093 | .0053 | isotonic |
| Gradient boosting | .0068 | .0055 | .0053 | isotonic·상한 전용 |
코퍼스 validation 10,040건·만석 583건. 낮을수록 낫다.
5. 그림 명세
Q. 무엇을 그려야 이 층의 역할이 보이나?
6. 어디까지 믿나
Q. 언제 실패하나?
- 직접 수집분의 보정 효과: 아직 없음. 1회차 재대결은 직접 라벨 1,524건·만석 54건, validation 443건·만석 14건으로 돌려 네 지표를 다 냈다. 다만 수집 날짜가 하루뿐이라 적격 보정 fold가 만들어지지 않았고, isotonic 래퍼도 raw 예측을 그대로 냈다.
- isotonic은 얇은 확률 구간에 약하다. 구간 검열 승자도
[.1,.2)97건에서 평균예측 .1216, 실제 만석률 .2577이었다.[.6,.7)은 21건뿐이고 .6051 대 .8095였다. - 전체 ECE가 노선별 오차를 가릴 수 있다. 구간 검열 isotonic의 평균예측-관측률은 1650에서 -.0093, 3330에서 +.0010이었다.
- 이미 눈금이 맞는 후보에 다시 손대면 망가진다. 경험적 베이즈와 GAM이 실제 반례다.
- 보정기는 기본 모델과 한 묶음이다. 특징, 학습기간, 하이퍼파라미터가 바뀌면 같은 보정 함수를 재사용하지 않는다.
- 지금 수치는 코퍼스 보조 원천과 개발 validation에 한정된다. 같은 validation을 개발 중 반복 확인해 후보 선택 성적이 좋게 부풀 가능성도 남았다. 모델과 보정기를 동결한 뒤 이후 연속 날짜와 직접 수집 만석 사건으로 다시 재야 한다.
Q. 그래서 Platt와 isotonic 중 무엇을 쓰나?
공통 답은 아직 없다. 구간 검열 후보에는 isotonic을 붙인 변형이 잠정 선택됐다. 경험적 베이즈와 GAM은 raw가 낫다. 계층 로지스틱과 상호작용 로지스틱의 isotonic 개선 구간은 0을 포함했다. 후보 이름 뒤에 raw·platt·isotonic을 붙여 서로 다른 모델 버전처럼 관리하고, 원확률도 함께 남긴다.
Q. 프로토타입과 무엇이 다른가?
| 프로토타입 | 보정 후처리 |
|---|---|
화면 탑승률은 정류장×평일/주말×30분 셀의 1-z/n이었다. | 모든 후보가 같은 출발 후 만석 라벨의 p_full을 낸다. 탈 확률은 그 여집합이다. |
차량별 seatAvailableProbability는 정규 수요를 흘린 별도 계산이었다. 화면 탑승률과 서로 맞추지 않았다. | 후보가 달라도 raw·Platt·isotonic을 같은 시간순 검증에서 맞붙인다. |
| Wilson 95% 구간은 표본비율의 불확실성 표시였다. 확률 눈금을 고치지 않았다. | OOF 원확률과 실제 빈도로 후보별 눈금 함수를 따로 학습한다. |
| 도착·출발 의미가 계산 경로마다 달랐다. | 출발 뒤 잔여석 0만 만석으로 고정한다. validation 라벨은 보정 적합에서 뺀다. |
프로토타입에는 이 층이 없었다. 지금도 하나의 만능 보정층을 정한 상태는 아니다. 달라진 것은 보정 여부까지 검증 대상이 됐다는 점이다.
재료 한눈에
아직 못 써본 것과 그 이유
- 최종 validation 라벨 — 보정 함수 선택과 적합에 쓰면 검증 답안을 미리 보는 누수다.
- 정원·점유율 — 코퍼스 가명 차량키와 정원 사전의 교집합이 0/77이고 관측 최대 대리는 시간에 따라 바뀐다. 라벨에도 필요 없다.
- crowded 혼잡도 — 대상 유형 11은 공식 제공 범위 밖이며 180초 이하 인접쌍 81,147개에서 혼잡도만 바뀐 쌍이 0건이라 독립 정보가 확인되지 않았다.
- queryTime — 38개 연속쌍 중 7번 되감긴 서버 시계라 보정 fold의 시간축으로 쓰지 않는다.
아래 성적은 남의 20일 코퍼스로 낸 것이다. 그 코퍼스는 차량 ID가 가명이라 정원·차종·저상 여부를 붙일 수 없었다(조인 0/77). 이 재료들은 테스트 자체를 못 했다.
우리 직접 수집분에는 원본 차량 ID가 남아 정원이 실제로 붙는다 — 고유차량 55/62대(88.7%). 다만 1회차 재대결에서 정원·차종을 넣어도 뚜렷한 개선은 확인되지 않았다. 표본이 얇아 판정이 안 될 뿐, 쓸모없다는 뜻은 아니다. 관측 최대 잔여석으로 정원을 추정하는 것은 계속 금지다 — 원본 ID를 써도 그 값은 관측할수록 올라가서 정원이 아니다.
직접 수집분으로 다시 잰 성적 1회차
보정은 다른 모델 위에 얹는 층이라 단독 성적이 없다. 효과는 같은 모델의 보정 전후로 잰다.
검증 날짜가 하루뿐이라 적격 보정 fold가 만들어지지 않았다. 그래서 보정 전후 예측이 동일하게 나왔다 — 구간 검열 모델의 raw와 isotonic이 네 지표 모두 같은 값(ECE .0138 · Brier .0244 · log loss .0987 · AUC .9093)이었다.
코퍼스에서 보정이 ECE를 개선했던 결과가 반증된 것이 아니라 미검증이다. 날짜가 둘 이상 쌓이면 그때 다시 잰다.
쓰는 재료 5개 — 출처 필드까지
| 재료 | 종류 | 어느 API의 어느 필드 |
|---|---|---|
| 기본 모델 원확률 (p_full)기본 후보가 낸 보정 전 만석확률을 그대로 보정기의 한 개 입력값으로 넣는다.주의 — 직접 API 필드가 아니다. 학습용 값은 시간순 OOF 예측이고 서빙용 값은 최종 적합 모델의 새 예측이다. | 파생 | 후보 출력 p_full; 보정기 입력 p_full선택된 기본 후보의 predict() 출력 |
출발 후 만석 라벨 (label_full)정류장 s 통과 뒤 같은 차량에서 (stateCd,stationSeq)=(2,s),(0,s),(1,s+1) 중 처음 잡힌 유효 좌석값을 찾는다. 0이면 1, 양수면 0이다.주의 — remainSeatCnt=-1은 결측이다. (stateCd,stationSeq)=(1,s)는 도착 상태라 라벨이 아니다. 예측 시점 뒤에 생기므로 학습 정답으로만 쓴다. | 파생 | msgBody.busLocationList[].vehId, msgBody.busLocationList[].stationSeq, msgBody.busLocationList[].stateCd, msgBody.busLocationList[].remainSeatCnt; timing.response_received_at.kst, timing.response_received_at.utc; S1 passage_events.label_full, label_remain_seatsGBIS 버스위치정보 getBusLocationListv2 + 우리 수집기 응답 수신시각 + S1 통과 사건 빌더 |
보정 순방향 날짜축 (query_day_index)KST 날짜에서 2026-07-19를 뺀 뒤, 앞선 날짜로 적합하고 다음 날짜의 p_full을 얻는 순방향 OOF fold를 만든다.주의 — 마지막 세 순방향 fold는 후보 튜닝에 쓰고, 보정 OOF는 적격 순방향 fold 전체를 잇는다. 최종 validation 날짜는 끼우지 않는다. | 파생 | timing.response_received_at.kst, timing.response_received_at.utc; S1 E4 response_received_kst, response_received_utc; 코퍼스 collectedAt; S1 query_day_index우리 수집기 응답 수신시각; 제한 코퍼스는 수집기가 붙인 collectedAt |
단일 날짜 분할 보조시각 (query_minute_of_day)train 날짜가 하나뿐일 때 KST hour*60+minute과 원래 행 순서로 정렬해 앞 70%를 core로 둔다.주의 — 단일 날짜에서는 날짜 순방향 OOF fold가 생기지 않아 보정기는 identity로 남는다. msgHeader.queryTime은 쓰지 않는다. | 파생 | timing.response_received_at.kst, timing.response_received_at.utc; S1 E4 response_received_kst, response_received_utc; 코퍼스 collectedAt; S1 query_minute_of_day우리 수집기 응답 수신시각; 제한 코퍼스는 수집기가 붙인 collectedAt |
훈련 만석률과 클래스 가중치전체 train의 label_full 평균을 목표 만석률로 두고, OOF 보정 구간의 가중 만석률이 그 값과 같아지도록 양성·음성 표본을 재가중한다.주의 — validation 라벨은 이 계산에 넣지 않는다. | 파생 | S1 label_full; candidate_lib.fit_calibrators(target_prevalence); 모델 내부 calibration_prevalence, positive_weight, negative_weight, sample_weight보정 후처리 내부 |
지금은 안 쓰는 재료 4개
- validation 정답으로 보정 학습 — S1
split,label_full시간순 validation 라벨은 평가에만 둔다. 보정기는 train의 OOF 확률과 train 라벨만 읽는다. - 차량 정원·점유율 —
TBBMSVEHINFOM.SEAT_RDNG_PSN_CNT; 점유율 후보식1-msgBody.busLocationList[].remainSeatCnt/TBBMSVEHINFOM.SEAT_RDNG_PSN_CNT20일 코퍼스 차량키가plateNo우선 HMAC이라 외부 정원과 조인된 차량이 0/77이다. 기존 231,048행의capacity_exact도 모두 0이다. 관측 최대 잔여석은 정원의 하한일 뿐 고정 정원이 아니다. 라벨은 출발 후 좌석이 0인지에 정원이 필요 없다. 프로토타입의defaultSeatCapacity=80은 분포 배열 상한이며 점유율 분모가 아니다. - 혼잡도 — GBIS getBusLocationListv2
msgBody.busLocationList[].crowded공식 제공 노선유형은 13·15·23이고 대상 노선은 11이다. 직접 수집분에서 같은 잔여석 값에 다른 코드가 붙은 값이 26개 나와 잔여석만의 전역 함수라는 해석은 반증됐다. 다만 같은(vehId, remainSeatCnt)에서는 충돌이 0셀이고crowded만 바뀐 연속쌍도 0건이라 차량별 임계화일 가능성이 남는다. 산식은 미확정이고 아직 특징으로 써 보지 않았다. - API queryTime 파생 시간 — GBIS getBusLocationListv2
msgHeader.queryTime; HTTP 응답 헤더Date같은 노선 연속 38쌍 중 7쌍에서queryTime이 뒤로 갔다. 직접 수집 92/92건에서는 응답 수신시각보다 미래였고 중앙 +17.265초였다. HTTPDate는queryTime의 초 절삭과 92/92 일치해 독립 시계가 아니다. 감사 원문만 보존하고 나이·속도·헤드웨이·분할에 쓰지 않는다.
언제 실패하나
- 직접 수집분의 보정 효과: 아직 없음. 라벨 1,524건·만석 54건을 모았지만 수집 날짜가 하루뿐이었다. 보정 fold가 생기지 않아 isotonic이 raw와 같은 값을 냈다.
- isotonic은 얇은 확률 구간에서 턱과 평지가 생긴다. [.1,.2) 97건은 .1216 대 .2577, [.6,.7) 21건은 .6051 대 .8095였다.
- 전체 ECE는 1650의 평균 과소예측 -.0093과 3330의 과대예측 +.0010을 가릴 수 있다.
- 이미 보정이 좋은 경험적 베이즈와 GAM에는 Platt·isotonic이 오히려 ECE를 키웠다.
- 기본 모델이나 학습기간이 바뀌면 보정기도 다시 맞춰야 한다.
- 코퍼스 개발 validation을 반복 확인해 후보 선택 성적이 좋게 부풀 가능성이 남아 있어 이후 연속 날짜와 직접 수집 만석 사건 검증이 필요하다.
기존 프로토타입과 다른 점
- 프로토타입 화면 탑승률은 정류장×평일/주말×30분 셀의 1-z/n이었고 차량별 seatAvailableProbability와 별개였다.
- 프로토타입의 Wilson 95% 구간은 불확실성 표시였을 뿐 확률 눈금을 고치는 층이 아니었다.
- 지금은 모든 후보가 출발 후 0석이라는 같은 라벨의 p_full을 내고 p_board는 1-p_full로만 만든다.
- raw·Platt·isotonic을 같은 시간순 OOF와 validation에서 비교하며 validation 라벨은 보정 적합에서 뺀다.
- 보정 여부 자체가 후보별 검증 대상이며 공통 보정법은 아직 정하지 않았다.