후보 모델
본사와 지점
계층 로지스틱 (부분 풀링)
정류장·시간·셀 효과를 나눠 배우고, 얇은 셀을 0%로 단정하지 않는다.
1. Q. 왜 셀 비율을 그대로 쓰지 않나?
정류장×30분대 셀은 833개다. 이 중 281개(33.73%)는 학습 표본이 10건 이하다. 719개(86.31%)는 만석을 한 번도 못 봤다. 0회라는 기록과 만석 위험 0%는 같은 말이 아니다.
위치와 시간 자체는 강한 신호였다. 정류장·시간만 본 기준선의 검증 AUC는 .9613이었다. 하지만 셀 비율을 그대로 쓰면 학습에서 만석 0건인 셀에 확률 0을 줬다. 그 결과 log loss가 .1318이었다. 의사표본 1개만 섞어도 .1033으로 내려갔다. 셀은 살리되 얇은 셀의 확신은 눌러야 했다.
여기서 계층 로지스틱이 나왔다. 정류장 효과와 시간 효과를 따로 배우고, 둘이 만난 셀 효과도 남긴다. 표본이 적은 효과는 0 쪽으로 당기고, 기록이 쌓인 효과는 더 멀리 움직인다. 하루 매출 한 건만 나온 프랜차이즈 지점을 그 한 건으로 단정하지 않고, 본사 평균과 다른 날 기록도 함께 보는 것과 같다.
노선은 고정효과다. 정류장·30분대·정확한 셀은 0 중심 Gaussian 규제를 받는다.
2. Q. 한 셀을 어떻게 나누나?
Q. 무엇을 더하나?
모델은 만석확률을 먼저 낸다. 사용자에게 보이는 탈 확률은 그 여집합이다.
30분대는 KST 응답 수신 시각으로 만든다. σ는 더한 점수를 0과 1 사이 확률로 바꾸는 로지스틱 함수다. 마지막에 p_board = 1 - p_full로 뒤집는다.
Q. “공통 분포”는 실제 코드에서 무엇인가?
노선은 규제하지 않는 고정효과다. 노선×정류장 139개, 노선×30분대 12개, 정확한 셀 833개는 0을 중심으로 한 Gaussian 규제를 받는다. 랜덤효과 계수는 모두 984개다.
완전한 베이즈 추론은 아니다. 계수의 사후분포를 뽑지 않고, 로지스틱 손실과 Gaussian 벌점을 함께 최소화한 MAP 추정이다. 벌점 후보 .5, 1, 2, 4, 8, 16, 32를 학습 구간 안의 날짜 순방향 2-fold로 겨뤘고 .5가 골라졌다.
Q. 셀 효과는 왜 더 세게 누르나?
정류장 효과와 시간 효과의 벌점은 .5다. 정확한 셀은 그 4배인 2.0이다. 셀 하나의 우연한 0회·1회보다 여러 시간에 반복된 정류장 성향과 여러 정류장에 반복된 시간 성향을 먼저 믿겠다는 뜻이다.
처음 보는 정류장×시간 조합에는 정확한 셀 효과가 0으로 들어간다. 정류장과 시간은 학습에서 봤다면 두 효과는 그대로 남는다. 정류장 자체도 처음이면 그 효과까지 0이 된다.
학습 6건·만석 0회인 셀도 다른 시간의 정류장 효과를 받아 raw 만석확률 .1228이 됐다.
3. Q. 실제 숫자는 어떻게 합쳐지나?
코퍼스 학습분 12,065건·만석 705건으로 적합한 값이다. 3330번 안양시청 정류장(ID 209000108), 18:00~18:30 셀을 본다. 이 셀의 학습 표본은 6건이고 만석은 0건이었다. 같은 정류장의 여섯 30분대를 합치면 141건 중 만석 33건이었다.
| 더하는 항 | 적합값 |
|---|---|
| 절편 | -4.0796 |
| 3330 노선 | -0.9832 |
| 3330×안양시청 | +3.2674 |
| 3330×18:00~18:30 | +0.1977 |
| 안양시청×18:00~18:30 셀 | -0.3685 |
| 합 | -1.9661 |
σ(-1.9661) = .1228이다. 셀 비율만 보면 0/6 = 0%지만, 부분 풀링한 보정 전 만석확률은 12.28%다. 학습 안에서 날짜순으로 떼어 낸 예측값에 isotonic(단조) 보정을 맞추면 .1181이 된다. 최종 탈 확률은 1 - .1181 = .8819, 88.19%다. 시간순 검증에서도 이 셀은 8건 중 만석 0건이었다. 8건만으로 0%와 11.81% 중 어느 쪽이 맞는지는 가릴 수 없다.
Q. 그래서 기준선을 이겼나?
아니다. 아래는 코퍼스 검증 10,040건·만석 583건에서, 각 후보군의 대표 변형을 비교한 값이다. 계층 로지스틱은 isotonic 변형이다.
| 지표 | 정류장·시간 기준선 | 계층 로지스틱 | 좋은 방향 |
|---|---|---|---|
| ECE | .0063 | .0090 | 낮음 |
| Brier | .0327 | .0341 | 낮음 |
| log loss | .1033 | .1079 | 낮음 |
| AUC | .9613 | .9628 | 높음 |
| 비대칭 log loss | .1670 | .1815 | 낮음 |
AUC만 .0015 높았다. ECE·Brier·log loss·비대칭 log loss는 모두 나빠졌다. 순서는 조금 더 잘 세웠지만 확률값은 기준선보다 못 맞췄다.
4. Q. 무엇을 넣고, 무엇을 뺐나?
| 재료 | 쓰임 | 이유 |
|---|---|---|
| 노선 | 고정효과 | 두 노선의 전체 기저 차이를 분리한다. |
station_id | 노선×정류장 랜덤절편 | 위치 단독 AUC가 .9336이었다. |
query_half_hour | 노선×30분대 랜덤절편 | 시간만 본 AUC는 .6375였다. 위치와 따로 둔다. |
노선×정류장×30분대 | 강하게 규제한 셀 효과 | 정류장과 시간이 만날 때만 생기는 차이를 남긴다. |
출발 후 remainSeatCnt | 정답 생성에만 사용 | 0이면 label_full=1, 양수면 0이다. 예측 입력은 아니다. |
안 쓰는 재료도 분명하다.
| 아직 못 써본 재료 | 왜 뺐나 |
|---|---|
| 정원·점유율 | 코퍼스 가명 차량과 외부 정원의 조인이 0/77대다. 폐기된 옛 데이터 231,048행도 capacity_exact가 전부 0이었다. 관측 최대 잔여석은 정원이 아니라 하한이다. 라벨에도 정원이 필요 없다. |
crowded | 공식 제공 대상은 노선유형 13·15·23인데 대상 노선은 유형 11이다. 직접 수집분에서 같은 잔여석에 다른 코드가 붙은 값이 26개라 잔여석만의 함수는 아니다. 다만 같은 (vehId, remainSeatCnt)에서는 충돌이 0셀이고 산식도 미확정이라 아직 넣지 않았다. |
| 앞차 상태·실시간 잔여석·노선 전체 요약 | 금지 재료는 아니다. 이 후보는 정류장·시간 계층만 떼어 검정하려고 넣지 않았다. |
queryTime | 서버 시계가 연속 호출 38쌍 중 7쌍에서 되감겼다. 시간대는 로컬 응답 수신 시각으로만 만든다. |
| 지표 | 정류장·시간 기준선 | 계층 로지스틱 |
|---|---|---|
| ECE ↓ | .0063 | .0090 |
| Brier ↓ | .0327 | .0341 |
| log loss ↓ | .1033 | .1079 |
| AUC ↑ | .9613 | .9628 |
| 비대칭 log loss ↓ | .1670 | .1815 |
코퍼스 검증 10,040건·만석 583건. 계층 로지스틱은 isotonic 대표 변형이다.
5. Q. 어떤 그림으로 보면 되나?
그림 1은 한 셀을 노선·정류장·시간·정확한 셀 효과로 나누는 흐름이다. 그림 2는 안양시청 18:00 셀의 실제 적합값을 더하는 눈금이다. 그림 3은 AUC만 소폭 높고 다른 네 지표는 나빠진 비교표다.
6. Q. 언제 깨지고, 프로토타입과 무엇이 다른가?
Q. 언제 실패하나?
- 새 셀에서 실패 여부를 아직 모른다. 코퍼스 검증의 새 셀은 2행뿐이고 만석은 0건이었다. 정류장과 시간 효과를 이어 쓰는 구조가 실제 새 셀에서도 맞는지는 검증되지 않았다.
- 정류장 효과와 시간 효과를 더하는 가정이 틀리면 약하다. 특정 정류장의 특정 시간에만 생기는 급등은 정확한 셀 효과가 받아야 한다. 그 셀이 얇으면 4배 규제가 진짜 급등까지 누른다.
- Gaussian 규제 강도
.5는 코퍼스 앞 7,049건의 날짜 순방향 2-fold에서 골랐다. 다른 날짜·노선에서도 같은 강도가 맞는지는 아직 모른다. - AUC 차이
+.0015의 paired 95% 구간은[-.0026, +.0068]이었다. 순위 개선도 확정적이지 않다. isotonic(단조) 보정이 보정 전 ECE를 .0110에서 .0090으로 낮췄지만 기준선 .0063에는 못 미쳤다. - 직접 수집분에서는 train 1,081건·만석 40건으로 적합해 validation 443건·만석 14건에서 ECE .0321, Brier .0272, log loss .1103, AUC .9230을 냈다. 다만 train과 validation이 같은 날짜라 isotonic 보정 fold가 없었고 이 실행의 예측은 raw다. 검증 만석 14건·날짜 1개로는 날짜 간 변동을 보지 못한다.
- 이 후보는 실시간 앞차 상태와 잔여석을 보지 않는다. 같은 셀 안에서 당일 상황이 급변하면 반응하지 못한다.
Q. 프로토타입과 무엇이 다른가?
| 프로토타입 | 계층 로지스틱 |
|---|---|
화면의 탑승 확률은 정류장×평일/주말×30분대의 마지막 스냅샷에서 1-zeroCount/samples로 계산했다. | 출발 후 0석 라벨로 만석확률을 직접 학습하고 1-p_full을 탈 확률로 낸다. |
| 셀 비율을 그대로 썼다. 얇은 셀을 다른 정류장·시간과 묶지 않았다. | 정류장·시간·정확한 셀 효과를 나눠 부분 풀링한다. |
차량 좌석 예보는 현재 잔여석과 수요 μ·σ를 0~80석 분포로 전파한 별도 계산이었다. | 좌석 수를 예보하지 않는다. 노선·정류장·30분대만으로 확률 하나를 낸다. |
좌석 예보, +10 여유폭, 만석 연속으로 여유/빠듯/어려움을 정했다. | +10, 만석 연속, 대기열, 이동 추천을 쓰지 않는다. |
| 확률 보정층이 없었다. | 학습 안에서 날짜순으로 떼어 낸 예측값으로 Platt와 isotonic을 따로 검정했다. 이 모델에서는 isotonic도 기준선보다 나빴다. |
재료 한눈에
아직 못 써본 것과 그 이유
- 정원·점유율 — 코퍼스 차량과 외부 정원 조인이 0/77대이며 옛 데이터 231,048행의 capacity_exact도 전부 0이었다. 관측 최대는 정원이 아닌 하한이고 라벨에도 정원이 필요 없다.
- crowded — 대상 유형 11은 공식 제공 대상 13·15·23 밖이다. 직접 수집분에서 같은 잔여석에 다른 코드가 붙은 값이 26개라 잔여석만의 함수는 아니지만, 같은 (vehId, 잔여석)에서는 충돌이 0셀이고 산식도 미확정이라 아직 넣지 않았다.
- 앞차 상태·실시간 잔여석·노선 전체 요약 — 금지 재료는 아니지만 정류장·시간 계층만 떼어 검정하는 후보라 넣지 않았다.
- queryTime — 서버 시계가 연속 호출 38쌍 중 7쌍에서 되감겼다. 시간대는 로컬 응답 수신 시각으로만 만든다.
아래 성적은 남의 20일 코퍼스로 낸 것이다. 그 코퍼스는 차량 ID가 가명이라 정원·차종·저상 여부를 붙일 수 없었다(조인 0/77). 이 재료들은 테스트 자체를 못 했다.
우리 직접 수집분에는 원본 차량 ID가 남아 정원이 실제로 붙는다 — 고유차량 55/62대(88.7%). 다만 1회차 재대결에서 정원·차종을 넣어도 뚜렷한 개선은 확인되지 않았다. 표본이 얇아 판정이 안 될 뿐, 쓸모없다는 뜻은 아니다. 관측 최대 잔여석으로 정원을 추정하는 것은 계속 금지다 — 원본 ID를 써도 그 값은 관측할수록 올라가서 정원이 아니다.
직접 수집분으로 다시 잰 성적 1회차
우리 수집기가 직접 쌓은 데이터만 쓰고 코퍼스는 뺐다. 검증분 443행·만석 14건, 정류소×날짜 125군집 부트스트랩 5,000회.
| 지표 | 직접 수집분 (95% 구간) | 코퍼스 |
|---|---|---|
| 확률이 정직한가 | 0.0321[0.0230, 0.0449] | 0.0090 |
| 얼마나 빗나갔나 | 0.0272[0.0138, 0.0422] | 0.0341 |
| 확신하고 틀린 벌점 | 0.1103[0.0706, 0.1533] | 0.1079 |
| 만석을 골라내나 | 0.9230[0.8509, 0.9706] | 0.9628 |
표본이 얇아 21쌍 중 16쌍이 구분되지 않았다. 검증 날짜가 하루뿐이라 날짜 간 변동도 못 본다. 21쌍 중 16쌍을 못 가리고 validation 날짜가 1개라 전체 순위를 발표하지 않는다
코퍼스 순위가 뒤집힌 쌍은 0개다. 방향은 틀리지 않았고 미세한 차이만 미지로 남았다. ΔAUC 0.05를 가리려면 8 수집일, 날짜 변동까지 보려면 15일이 더 든다.
주의 — 이번 실행에서는 보정층이 작동하지 않았다. 검증 날짜가 하루뿐이라 적격 보정 fold가 없었다. 이름에 isotonic이 붙어 있어도 위 숫자는 보정 전 예측이다. 보정 효과는 반증된 것이 아니라 아직 재지 못한 것이다.
쓰는 재료 7개 — 출처 필드까지
| 재료 | 종류 | 어느 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 |
노선×정류장 효과 (route_station)route + '|' + station_id 문자열 범주를 만든다.주의 — 원 API에 같은 이름의 필드는 없다. 학습에서 못 본 조합은 새 범주로 처리하지 않고 구현의 기본 효과로 돌아간다. | 파생 | S1 route, station_id → 모델 내부 route_station계층 로지스틱 내부 조합 |
노선×30분대 효과 (route_time)route + '|' + query_half_hour 문자열 범주를 만든다.주의 — 원 API 필드가 아니다. | 파생 | S1 route, query_half_hour → 모델 내부 route_time계층 로지스틱 내부 조합 |
노선×정류장×30분 셀 효과 (cell)route + '|' + station_id + '|' + query_half_hour 문자열 범주를 만든다.주의 — 얇은 셀을 그대로 독립 비율로 쓰지 않고 상위 효과와 함께 축소한다. | 파생 | S1 route, station_id, query_half_hour → 모델 내부 cell계층 로지스틱 내부 조합 |
출발 후 만석 라벨 (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 통과 사건 빌더 |
지금은 안 쓰는 재료 4개
- 차량 정원·점유율 —
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_*이 후보는 역사적 시공간 위험만 비교하려고 실시간 스냅샷 재료를 읽지 않는다. - API queryTime 파생 시간 — GBIS getBusLocationListv2
msgHeader.queryTime; HTTP 응답 헤더Date같은 노선 연속 38쌍 중 7쌍에서queryTime이 뒤로 갔다. 직접 수집 92/92건에서는 응답 수신시각보다 미래였고 중앙 +17.265초였다. HTTPDate는queryTime의 초 절삭과 92/92 일치해 독립 시계가 아니다. 감사 원문만 보존하고 나이·속도·헤드웨이·분할에 쓰지 않는다.
언제 실패하나
- 코퍼스 검증의 새 셀은 2행·만석 0건이라 새 조합 일반화 성능은 아직 모른다.
- 특정 정류장의 특정 시간에만 생기는 급등은 얇은 정확한 셀에 걸린 4배 규제가 함께 눌러 버릴 수 있다.
- 규제 강도 .5는 코퍼스 앞 7,049건의 날짜 순방향 2-fold에서 골랐을 뿐 다른 날짜·노선 근거가 없다.
- AUC 차이 +.0015의 paired 95% 구간은 [-.0026, +.0068]이며 ECE·Brier·log loss·비대칭 log loss는 기준선보다 나빴다.
- 직접 수집분 validation은 443건·만석 14건이고 수집 날짜가 하루뿐이다. 이 표본에서는 후보 21쌍 중 16쌍이 차이의 부호조차 고정되지 않았고, 이 모델이 유의하게 이긴 상대는 평균으로 당기기 하나뿐이다.
- 실시간 앞차 상태와 잔여석을 보지 않아 같은 셀 안의 당일 급변에 반응하지 못한다.
기존 프로토타입과 다른 점
- 프로토타입 화면 확률은 정류장×평일/주말×30분대의 마지막 스냅샷에서 1-zeroCount/samples를 계산했지만, 이 모델은 출발 후 0석 라벨의 만석확률을 직접 학습한다.
- 프로토타입은 셀 비율을 그대로 썼지만 이 모델은 정류장·시간·정확한 셀 효과를 나눠 부분 풀링한다.
- 프로토타입 차량 예보는 현재 잔여석과 수요 μ·σ를 0~80석 분포로 전파했지만 이 모델은 좌석 수를 예보하지 않고 확률 하나만 낸다.
- 프로토타입의 +10 여유폭·만석 연속·대기열·이동 추천은 이 모델에 없다.
- 프로토타입에는 확률 보정층이 없었고, 이 모델은 학습 안에서 날짜순으로 떼어 낸 예측값으로 Platt·isotonic을 검정했으나 isotonic도 기준선보다 나빴다.