후보 모델

숫자 다듬기

보정 후처리

모델이 낸 위험 순서는 두고, 확률 눈금만 다시 맞춘다.

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 날짜
앞 날짜로 뒤 날짜 예측
다음 train 날짜 OOF p_full·라벨 10,236건/572건
눈금 적합
Platt 또는 isotonic
전체 train 재적합 모델 12,065건/705건
답안 없이 예측
validation raw p_full
보정된 p_full
사용자 표시
p_board=1-p_full

train 날짜 안에서 만든 OOF 예측으로 보정기를 맞추고, 최종 validation 라벨은 흐름 밖에 둔다.

2. 원리 — 무엇을 어떻게 고치나

Q. 보정기는 무엇을 입력으로 받나?

입력은 후보가 낸 원 만석확률 p_full 하나다. 정류장, 시간, 앞차 잔여석을 다시 넣지 않는다. 그 정보는 이미 원확률 안에 들어 있다. 보정기는 p_full과 실제 출발 후 만석 빈도의 대응만 배운다. 마지막에 사용자용 탈 확률을 p_board = 1 - p_full로 바꾼다.

Q. Platt는 어떻게 고치나?

원확률을 로짓으로 바꾼 뒤 기울기와 절편 두 개를 붙인다.

p_cal = sigmoid(a × logit(p_raw) + b)

한 번에 휘는 매끈한 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은 어디까지 휘나
항목Plattisotonic
모양기울기·절편 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를 배웠다.

logit(.496211) = -.015155 → sigmoid(1.575053 × -.015155 + .850292) = .695598
변형만석확률 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 DatequeryTime 초 절삭과 92/92 일치해 독립 시계가 아니다. 보정 fold의 시간축으로 쓰지 않는다.
후보마다 달랐던 ECE 변화
후보rawPlattisotonic점추정 판정
경험적 베이즈.0070.0134.0131raw
계층 로지스틱.0110.0099.0090개선 CI에 0 포함
GAM/스플라인.0038.0190.0082raw
로지스틱+상호작용.0098.0094.0065isotonic, 개선 CI에 0 포함
구간 검열.0200.0093.0053isotonic
Gradient boosting.0068.0055.0053isotonic·상한 전용

코퍼스 validation 10,040건·만석 583건. 낮을수록 낫다.

5. 그림 명세

Q. 무엇을 그려야 이 층의 역할이 보이나?

6. 어디까지 믿나

Q. 언제 실패하나?

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 라벨은 보정 적합에서 뺀다.

프로토타입에는 이 층이 없었다. 지금도 하나의 만능 보정층을 정한 상태는 아니다. 달라진 것은 보정 여부까지 검증 대상이 됐다는 점이다.

재료 한눈에

후보별 원 만석확률 p_full — 다시 맞출 눈금train 내부 순방향 OOF의 출발 후 만석 라벨 — 처음 보는 날의 원확률과 실제 빈도 연결로컬 날짜 순서 — 과거에서 미래 train 날짜로만 넘어가는 fold 구성train 전체 만석률 — OOF 표본의 prevalence 가중치 기준최종 validation 라벨정원·점유율crowded 혼잡도queryTime

아직 못 써본 것과 그 이유

주의 — 이건 "검증해서 버린 재료"가 아니다

아래 성적은 남의 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개

언제 실패하나

기존 프로토타입과 다른 점