후보 모델
평균으로 당기기
경험적 베이즈 축소추정
얇은 셀을 노선 평균 쪽으로 당긴다.
위 넷은 20일 코퍼스로 낸 옛 성적이고, 초록 칸은 정류장×시간대 기준선 이상인 지표다. AUC .9613은 그 기준선과 같은 값이며 코퍼스 7종 중 가장 낮다. 직접 수집분 재대결에서는 ECE .0328, Brier .0384, log loss .1337, AUC .8527이었고 AUC는 여기서도 7종 중 가장 낮았다.
Q. 만석을 한 번도 못 본 셀은 위험 0%인가?
아니다. 코퍼스 학습분의 노선×정류장×30분 셀은 833개다. 이 중 281개(33.73%)는 표본이 10건 이하다. 719개(86.31%)는 만석을 한 번도 못 봤다. 학습 만석률 자체가 705/12,065=5.84%라서, 표본이 몇 건뿐이면 0회 관측이 흔하다.
위치와 시간은 버리기 아까운 신호다. 아무것도 안 본 기준선 AUC는 .5000, 앞차 한 대만 본 기준선은 .7187, 정류장·30분 셀 기준선은 .9613이었다. 문제는 셀을 쓰느냐가 아니다. 얇은 셀을 얼마나 믿느냐다.
무축소 셀 비율은 만석 0건인 셀에 확률 0을 냈다. 검증 log loss는 .1318이었다. 노선 평균에 해당하는 의사표본 1개를 섞자 .1033으로 줄었다. 이 진단 때문에 축소추정이 본 후보로 올라왔다.
비유하면 학생 한 명만 시험을 본 반의 평균을 그 한 점수로 확정하지 않는 것과 같다. 학생이 적은 반은 학교 평균을 조금 섞는다. 학생이 많아지면 그 반의 점수를 그대로 믿는다.
전역 평균으로 노선을 안정시키고, 노선 평균으로 정류장×30분 셀을 안정시킨다.
Q. 어디로, 얼마나 당기나?
Q. 한 칸은 무엇인가?
한 셀은 노선×station_id×로컬 30분대다. 셀 안에서 출발 후 잔여석이 0인 사건 수 m과 전체 사건 수 n을 센다. 도착 시점 0석은 쓰지 않는다. 하차 뒤 자리가 생길 수 있어서다. remainSeatCnt=-1도 결측이라 뺀다.
Q. 당김의 기준은 무엇인가?
먼저 전체 학습분에서 전역 만석률을 구한다. 만석과 비만석에 .5건씩 더하는 Jeffreys 보정을 쓴다. 노선 만석률은 전역 만석률 쪽으로 의사표본 4개만큼 당긴다. 마지막으로 각 셀을 그 노선 만석률 쪽으로 당긴다.
셀 예측식은 하나다.
α는 노선 평균이 몇 건의 표본처럼 작용하는지 정한다. 실제 후보는 .5, 1, 2, 4, 8, 16, 32, 64를 날짜 순방향 교차검증으로 겨뤘다. 선택값은 .5였다. 노선 평균을 반 건만 섞는 약한 축소다.
Q. 왜 ‘경험적’ 베이즈인가?
사전평균 p_route를 사람이 정하지 않고 같은 학습 데이터의 노선·전역 빈도에서 구하기 때문이다. beta-binomial 방식으로 노선 평균을 의사표본처럼 보고 셀의 만석/비만석 횟수와 합친다. 사후분포 전체가 아니라 사후평균 하나만 내보낸다.
Q. 처음 보는 셀은 어떻게 하나?
정확한 셀이 없으면 노선×정류장 → 노선×30분대 → 노선 → 전역 순서로 내려간다. 이미 본 셀은 표본이 1건이어도 이 폴백을 쓰지 않는다. 그 셀의 비율과 노선 평균만 섞는다.
출력은 만석 확률 p_full이다. 사용자에게 보이는 탈 확률은 별도 모델이 아니라 1-p_full이다.
| 셀 | 무축소 | 축소 후 |
|---|---|---|
| 태영아파트 0/5 | 0% | 0.625% |
| 롯데백화점.범계역 14/23 | 60.87% | 59.69% |
같은 α=.5라도 표본 수가 다르면 당기는 폭이 달라진다.
Q. 실제 숫자는 어떻게 바뀌나?
1650번 태영아파트(station_id 209000243), 18:00~18:30 셀을 보자. 전부 학습 데이터에서 계산한 값이다.
- 전체 학습분은 12,065건, 만석 705건이다. Jeffreys 보정을 거친 전역 만석률은 5.847%다.
- 학습분의 1650번은 4,331건 중 298건이 만석이다. 전역 평균을 의사표본 4개만큼 섞은 노선 만석률은 6.880%다.
- 이 셀은 5건 중 만석 0건이다. 무축소 값은 0%다.
- 선택된
α=.5를 넣으면(0 + .5×.068797) / (5+.5) = .006254다. 만석 확률 0.625%, 탈 확률 99.375%다.
검증 라벨은 이 계산에 넣지 않았다. 뒤 검증에서는 같은 셀 4건 중 1건이 만석이었다. 한 셀의 4건으로 모델 전체를 판정할 수는 없다. 다만 학습의 0/5가 위험 0이라는 뜻은 아니었다.
표본이 두꺼우면 덜 움직인다. 3330번 롯데백화점.범계역 07:00~07:30 셀은 학습 23건 중 14건이 만석이었다. 무축소 60.87%가 축소 뒤 59.69%가 됐다.
Q. 무엇을 쓰고 무엇을 버리나?
쓰는 재료는 네 가지뿐이다.
route: 노선별 평균을 만든다.station_id: 정류장 셀을 가른다. 이름은 표시에만 쓴다.- 로컬 응답 수신시각의 30분대: 서버
queryTime대신 KST 계산축으로 쓴다. - 출발 후 만석 라벨:
remainSeatCnt==0이면 1, 양수면 0이다. 예측 시점 이후 값이라 학습 정답으로만 쓴다.
안 쓰는 재료도 분명하다.
- 정원·점유율: 만석 라벨에 필요 없다. 코퍼스의 가명 차량키는 외부 정원 자료와 0/77대 조인됐다. 관측 최대 잔여석은 정원이 아니라 하한이고 시간에 따라 바뀐다.
crowded: 대상 노선 유형 11은 공식 제공 유형 13·15·23 밖이다. 180초 이하 인접 관측 81,147쌍에서 잔여석은 그대로인데 혼잡도만 바뀐 쌍은 0건이었다. 독립 신호로 쓸 근거가 없다.- 현재 상류 버스 잔여석·앞차 상태·ETA: 이 후보는 정류장·시간대 자체의 위험만 재려고 일부러 쓰지 않는다.
- API
queryTime: 직접 수집 92호출에서 92/92건이 로컬 수신 시각보다 미래였고 중앙 +17.265초였다. 되감김은 인접 호출 전체 16건, 같은 노선 안 2건이다. 정렬과 시간대 계산에 쓰지 않는다.
| 변형 | ECE | 평균 예측 | 관측 만석률 |
|---|---|---|---|
| EB raw | .0070 | 5.4829% | 5.8068% |
| EB Platt | .0134 | 4.4684% | 5.8068% |
| EB isotonic | .0131 | 4.4963% | 5.8068% |
raw보다 Platt·isotonic의 평균 예측이 관측 만석률에서 더 멀어졌다.
Q. 언제 실패하나?
Q. 축소했는데 왜 보정은 나빠졌나?
S1 층화 기준선은 의사표본 1개를 섞었다. 경험적 베이즈가 고른 α=.5는 그보다 약한 축소다. 날짜 순방향 2-fold에서 ECE·Brier·log loss의 합이 .188043으로, α=1의 .188211보다 아주 조금 낮아 선택됐다. ECE만 고른 결과가 아니다.
ECE·Brier·log loss는 낮을수록 좋고 AUC는 높을수록 좋다.
| 모델 | ECE | Brier | log loss | AUC |
|---|---|---|---|---|
| 층화 기준선 | .0063 | .0327 | .1033 | .9613 |
| 경험적 베이즈 raw | .0070 | .0327 | .1024 | .9613 |
log loss는 .0009 좋아졌지만 ECE는 .0007 나빠졌다. 0% 같은 극단값을 피한 이득과 전체 확률 눈금의 작은 편향이 서로 다른 지표에 잡혔다.
후처리는 더 분명히 실패했다. OOF는 각 날짜를 그보다 앞선 날짜로만 예측해 만든 값이다. 학습 OOF 10,236건·만석 572건에서는 ECE가 raw .0128에서 Platt .0064, isotonic .0016으로 좋아졌다. 하지만 뒤 검증 10,040건·만석 583건에서는 raw .0070이 Platt .0134, isotonic .0131로 나빠졌다. 평균 예측도 raw 5.4829%에서 4.4684%·4.4963%로 내려가 관측 5.8068%를 크게 밑돌았다. 앞 구간에서 배운 눈금이 뒤 날짜로 옮겨가지 않은 것이다. 날짜별 분포 이동, 보정 표본 구성, isotonic의 60개 계단이 각각 얼마나 영향을 줬는지 분리한 실측은 아직 없다.
그 밖의 약점도 남는다.
- 정확한 셀이 한 번이라도 관측되면 정류장 평균이나 시간 평균을 더 섞지 않는다. 표본 1건 셀도 노선 평균과만 섞는다.
- 이웃 정류장과 이웃 시간의 연속성을 쓰지 않는다. 정류장 순번을 ±1 옮겨도 AUC .9100/.9113이 남았지만 이 정보는 버린다.
- 모든 노선·셀에 같은
α하나를 쓴다. 셀마다 다른 흔들림을 따로 배우지 않는다. - 새 셀 성능은 아직 재지 못했다. 코퍼스 validation의 새 셀은 2행·만석 0건뿐이다.
- 직접 수집분에서는 AUC 점추정이 7종 중 가장 낮았다. train 1,081건·만석 40건, validation 443건·만석 14건에서 ECE .0328, AUC .8527이고 네 후보에 유의하게 밀렸다.
Q. 프로토타입과 무엇이 다른가?
- 프로토타입의 화면 탑승률은
정류장×평일/주말×30분에서 마지막 스냅샷의 비만석 비율1-z/n이었다. 이 모델의 한 행은 버스 한 대의 정류장 통과 사건이고, 정답은 출발 후 0석이다. - 프로토타입은 비율을 그대로 쓰고
n<10이면 추천을 멈췄다. 이 모델은 얇은 셀도 노선 평균과 섞고, 새 셀은 정류장·시간·노선·전역 순으로 폴백한다. - 프로토타입의 화면 탑승률과 차량별 좌석 예보는 서로 다른 계산이었다. 이 모델은 만석 확률 하나만 내고 탈 확률은
1-p_full로 바꾼다. 남을 좌석 수·대기 인원·이동 추천은 계산하지 않는다. - 프로토타입 좌석 예보는 현재 잔여석과 정류장별 수요분포를 흘렸다. 이 모델은 현재 좌석·앞차·대기열을 보지 않는 정류장·시간대 빈도 모형이다.
- 프로토타입 좌석 예보의 공통 상한 80석도 여기 없다. 이 모델은 정원·점유율·
crowded를 모두 입력에서 뺐다.
현재 상태는 직접 수집분 재대결(M2)까지 마친 단계다. 그 재대결은 21쌍 중 16쌍을 못 가려 승자를 정하지 않았고 채택·탈락도 아직 없다.
재료 한눈에
route: 노선별 평균을 만든다.station_id: 정류장 셀을 가른다. 이름은 표시에만 쓴다.로컬 응답 수신시각의 30분대: 서버 queryTime 대신 KST 계산축으로 쓴다.출발 후 만석 라벨: remainSeatCnt==0이면 1, 양수면 0이다. 예측 시점 이후 값이라 학습 정답으로만 쓴다.정원·점유율crowded현재 상류 버스 잔여석·앞차 상태·ETAAPI queryTime아직 못 써본 것과 그 이유
- 정원·점유율 — 만석 라벨에 필요 없다. 코퍼스의 가명 차량키는 외부 정원 자료와 0/77대 조인됐다. 관측 최대 잔여석은 정원이 아니라 하한이고 시간에 따라 바뀐다.
- crowded — 대상 노선 유형 11은 공식 제공 유형 13·15·23 밖이다. 180초 이하 인접 관측 81,147쌍에서 잔여석은 그대로인데 혼잡도만 바뀐 쌍은 0건이었다. 독립 신호로 쓸 근거가 없다.
- 현재 상류 버스 잔여석·앞차 상태·ETA — 이 후보는 정류장·시간대 자체의 위험만 재려고 일부러 쓰지 않는다.
- API
queryTime— 직접 수집 92호출에서 92/92건이 로컬 수신 시각보다 미래였고 중앙 +17.265초였다. 되감김은 인접 호출 전체 16건, 같은 노선 안 2건이다. 정렬과 시간대 계산에 쓰지 않는다.
아래 성적은 남의 20일 코퍼스로 낸 것이다. 그 코퍼스는 차량 ID가 가명이라 정원·차종·저상 여부를 붙일 수 없었다(조인 0/77). 이 재료들은 테스트 자체를 못 했다.
우리 직접 수집분에는 원본 차량 ID가 남아 정원이 실제로 붙는다 — 고유차량 55/62대(88.7%). 다만 1회차 재대결에서 정원·차종을 넣어도 뚜렷한 개선은 확인되지 않았다. 표본이 얇아 판정이 안 될 뿐, 쓸모없다는 뜻은 아니다. 관측 최대 잔여석으로 정원을 추정하는 것은 계속 금지다 — 원본 ID를 써도 그 값은 관측할수록 올라가서 정원이 아니다.
직접 수집분으로 다시 잰 성적 1회차
우리 수집기가 직접 쌓은 데이터만 쓰고 코퍼스는 뺐다. 검증분 443행·만석 14건, 정류소×날짜 125군집 부트스트랩 5,000회.
| 지표 | 직접 수집분 (95% 구간) | 코퍼스 |
|---|---|---|
| 확률이 정직한가 | 0.0328[0.0135, 0.0556] | 0.0070 |
| 얼마나 빗나갔나 | 0.0384[0.0207, 0.0575] | 0.0327 |
| 확신하고 틀린 벌점 | 0.1337[0.0727, 0.2031] | 0.1024 |
| 만석을 골라내나 | 0.8527[0.7514, 0.9402] | 0.9613 |
표본이 얇아 21쌍 중 16쌍이 구분되지 않았다. 검증 날짜가 하루뿐이라 날짜 간 변동도 못 본다. 21쌍 중 16쌍을 못 가리고 validation 날짜가 1개라 전체 순위를 발표하지 않는다
코퍼스 순위가 뒤집힌 쌍은 0개다. 방향은 틀리지 않았고 미세한 차이만 미지로 남았다. ΔAUC 0.05를 가리려면 8 수집일, 날짜 변동까지 보려면 15일이 더 든다.
쓰는 재료 4개 — 출처 필드까지
| 재료 | 종류 | 어느 API의 어느 필드 |
|---|---|---|
노선 (route)직접 수집은 calls.jsonl.route_name, 코퍼스는 route.name을 모델 범주로 읽고 같은 route.routeName 마스터 행을 찾는다. 수집기 설정의 route.route_id와 원응답 routeId는 원문에 남는다.주의 — 현재 S1 구현은 실제로 routeName으로 마스터를 찾는다. msgBody.busLocationList[].routeId를 모델의 route로 복사하지 않는다. 대상 두 노선 밖으로 넓힐 때는 ID 검증이 필요하다. | 응답 원본 | msgBody.busLocationList[].routeId; 운영 수집기 route.name, route.route_id; S1 E4 calls.jsonl.route_name; 코퍼스 route.name; route.routeName, route.routeId; S1 routeGBIS 버스위치정보 getBusLocationListv2 + 우리 수집기 노선 메타데이터 + 제한 코퍼스 + GBIS route 마스터 |
목표 정류장 ID (station_id)차량별 정규화 위치가 전진한 끝점의 station_seq를 만들고, 같은 노선의 staOrder와 맞춰 stationId를 붙인다.주의 — 응답의 msgBody.busLocationList[].stationId를 목표 통과 정류장 ID로 복사하지 않는다. 그 값은 응답 시점 차량 위치의 정류장이다. | 파생 | msgBody.busLocationList[].vehId, msgBody.busLocationList[].stationSeq, msgBody.busLocationList[].stateCd; routestation.routeName, routestation.staOrder, routestation.stationId; S1 station_idGBIS 버스위치정보 getBusLocationListv2 + GBIS routestation 마스터 + S1 통과 사건 빌더 |
KST 30분대 (query_half_hour)응답 수신시각을 KST로 바꾸고 hour*2 + 1[minute>=30]으로 0~47을 만든다.주의 — msgHeader.queryTime과 HTTP Date는 시간 계산에 쓰지 않는다. 코퍼스 collectedAt은 본문 파싱 뒤 시각이다. | 파생 | timing.response_received_at.kst, timing.response_received_at.utc; S1 E4 response_received_kst, response_received_utc; 코퍼스 collectedAt; S1 query_half_hour우리 수집기 응답 수신시각; 제한 코퍼스는 수집기가 붙인 collectedAt |
출발 후 만석 라벨 (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 통과 사건 빌더 |
지금은 안 쓰는 재료 5개
- 차량 정원·점유율 —
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건이라 차량별 임계화일 가능성이 남는다. 산식은 미확정이고 아직 특징으로 써 보지 않았다. - 현재 상류·지난 버스 상태 —
msgBody.busLocationList[].remainSeatCnt,msgBody.busLocationList[].stateCd,msgBody.busLocationList[].stationSeq,msgBody.busLocationList[].vehId에서 만드는 S1upstream_*,passed_*,previous_passage_*,route_seat_*이 후보는 역사적 시공간 위험만 비교하려고 실시간 스냅샷 재료를 읽지 않는다. - 도착 ETA — GBIS getBusArrivalListv2
msgBody.busArrivalList[].predictTime1,msgBody.busArrivalList[].predictTime2,msgBody.busArrivalList[].predictTimeSec1,msgBody.busArrivalList[].predictTimeSec2; getBusArrivalItemv2msgBody.busArrivalItem.predictTime1,msgBody.busArrivalItem.predictTime2,msgBody.busArrivalItem.predictTimeSec1,msgBody.busArrivalItem.predictTimeSec2공급자가 만든 예측값을 다시 설명변수로 배우는 구조가 된다. 분·초 필드 관계도 실측 6쌍 중 2쌍에서 맞지 않았다. v1 후보 입력에서 뺐다. - API queryTime 파생 시간 — GBIS getBusLocationListv2
msgHeader.queryTime; HTTP 응답 헤더Date같은 노선 연속 38쌍 중 7쌍에서queryTime이 뒤로 갔다. 직접 수집 92/92건에서는 응답 수신시각보다 미래였고 중앙 +17.265초였다. HTTPDate는queryTime의 초 절삭과 92/92 일치해 독립 시계가 아니다. 감사 원문만 보존하고 나이·속도·헤드웨이·분할에 쓰지 않는다.
언제 실패하나
- 정확한 셀이 한 번이라도 관측되면 정류장 평균이나 시간 평균을 더 섞지 않는다. 표본 1건 셀도 노선 평균과만 섞는다.
- 이웃 정류장과 이웃 시간의 연속성을 쓰지 않는다. 정류장 순번을 ±1 옮겨도 AUC .9100/.9113이 남았지만 이 정보는 버린다.
- 모든 노선·셀에 같은
α하나를 쓴다. 셀마다 다른 흔들림을 따로 배우지 않는다. - 새 셀 성능은 아직 재지 못했다. 코퍼스 validation의 새 셀은 2행·만석 0건뿐이다.
- 직접 수집분에서는 AUC 점추정이 7종 중 가장 낮았다. train 1,081건·만석 40건, validation 443건·만석 14건에서 ECE .0328, AUC .8527이고 네 후보에 유의하게 밀렸다.
기존 프로토타입과 다른 점
- 프로토타입의 화면 탑승률은
정류장×평일/주말×30분에서 마지막 스냅샷의 비만석 비율1-z/n이었다. 이 모델의 한 행은 버스 한 대의 정류장 통과 사건이고, 정답은 출발 후 0석이다. - 프로토타입은 비율을 그대로 쓰고
n<10이면 추천을 멈췄다. 이 모델은 얇은 셀도 노선 평균과 섞고, 새 셀은 정류장·시간·노선·전역 순으로 폴백한다. - 프로토타입의 화면 탑승률과 차량별 좌석 예보는 서로 다른 계산이었다. 이 모델은 만석 확률 하나만 내고 탈 확률은
1-p_full로 바꾼다. 남을 좌석 수·대기 인원·이동 추천은 계산하지 않는다. - 프로토타입 좌석 예보는 현재 잔여석과 정류장별 수요분포를 흘렸다. 이 모델은 현재 좌석·앞차·대기열을 보지 않는 정류장·시간대 빈도 모형이다.
- 프로토타입 좌석 예보의 공통 상한 80석도 여기 없다. 이 모델은 정원·점유율·
crowded를 모두 입력에서 뺐다.