후보 모델
한계 재보기
Gradient boosting
허용된 재료의 최고 기록만 재는 비운영 비교자다.
1. Q. 정류장·시간만 봐도 AUC .9613인데, 왜 더 복잡하게 보나?
노선×정류장×30분 셀은 833개다. 이 중 281개(33.73%)는 학습 표본이 10건 이하다. 719개(86.31%)는 만석을 한 번도 못 봤다. 셀 비율만으로는 얇은 칸을 0% 위험으로 굳히기 쉽다.
Gradient boosting에는 허용된 신호를 한꺼번에 넣었다. 위치와 시간뿐 아니라 앞선 버스의 절대 잔여석, 직전 통과, 헤드웨이 구간, 노선 전체 좌석 분포까지 준다. 목적은 배포가 아니다. 다른 운영 후보가 놓친 신호가 얼마나 남았는지 잰다.
오픈북 모의고사와 같다. 허용된 재료는 다 펼치지만, 그 답안지를 운영안으로 내지는 않는다.
보조 코퍼스의 시간순 검증 10,040건 중 만석은 583건이었다. isotonic 보정 뒤 이 모델은 Brier .0155, log loss .0524, AUC .9910을 냈다. 잠정 운영 후보인 구간 검열 모형은 .0268, .0878, .9751이었다. 차이는 남았다. 직접 수집분 validation 443건·만석 14건에서는 이 모델이 Brier .0144, log loss .0731, AUC .9595, 구간 검열이 .0244, .0987, .9093이었다. 다만 두 모델의 차이는 이 표본으로 가리지 못한다. train과 validation이 같은 날짜라 isotonic 보정 fold가 없었고 양쪽 예측 모두 raw다.
여기서 상한은 수학적 한계가 아니다. 이번 재료, 이번 구현, 이번 검증에서 찍힌 참조 기록이다.
허용 특징 46개가 250개 나무를 거쳐 raw 만석확률이 되고, 시간순 OOF isotonic이 눈금을 다시 맞춘다.
2. 원리
Q. 나무 한 그루는 무엇을 하나?
한 그루는 짧은 예·아니오 질문을 이어 붙인다.
- 상류 버스 잔여석이 1.5석 이하인가?
- 정류장 순번이 64 이하인가?
- 직전 통과 버스가 만석이었나?
- 지금 시각이 학습된 경계보다 이른가?
질문을 통과한 행들은 같은 잎에 모인다. 잎에는 만석 여부를 바로 적지 않는다. 현재 점수를 올리거나 내릴 보정값을 적는다. 숫자 결측은 억지로 0이나 평균으로 채우지 않는다. 나무가 결측을 어느 쪽으로 보낼지도 학습한다.
Q. 왜 한 그루로 끝내지 않나?
첫 나무가 크게 틀린 행을 다음 나무가 다시 본다. 그다음 나무도 남은 오차를 본다. 이 구현은 log loss를 줄이는 작은 나무 250개를 순서대로 더한다.
마지막에는 p_full = 1 / (1 + exp(-F_250))로 만석확률을 만든다. 250개 나무의 다수결이 아니다. 앞선 답을 뒤 나무가 조금씩 고치는 누적 계산이다. 조기 종료는 쓰지 않았다. 250개를 끝까지 더했다.
Q. 얇은 셀은 어떻게 다루나?
정류장×시간 셀을 따로 외우는 규칙은 없다. 대신 한 잎에 최소 30건이 들어가야 한다. 나무 한 그루의 잎은 최대 63개다. 잎 보정값에는 L2 10을 건다. 얇은 셀 하나만으로 잎을 만들지 못하고, 같은 갈림길을 지난 다른 정류장·시각과 묶인다.
이것이 boosting식 풀링이다. 누구와 묶을지 미리 정하지 않고 잔여석·위치·시간의 경계로 정한다. 다만 나무 250개의 잎을 겹치면 최종 구역은 30건보다 훨씬 얇아질 수 있다. 규제가 있어도 운영 모델로 바로 쓰지 않는 이유다.
2026-08-06 07:28:58 KST, 3330번 롯데백화점.범계역 검증행의 실측 계산이다.
3. Q. 실제 한 건은 어떻게 흘렀나?
아래 사례는 직접 수집분이 아니라 보조 코퍼스의 검증행이다.
| 항목 | 실제 값 |
|---|---|
| 질의 | 2026-08-06 07:28:58 KST |
| 장소 | 3330번 롯데백화점.범계역, 축 순번 55 |
| 최근접 상류 버스 | 축거리 1, stateCd=0, 잔여석 0 |
| 상류 버스 좌석 변화 | 분당 -6.6165석 |
| 직전 통과 | 만석, 출발 후 잔여석 0 |
| 노선 응답 | 차량 22대, 만석 3대, 평균 잔여석 29.1364석 |
| 계획 배차 | 5분 |
| 실제 정답 | 이 버스도 출발 후 만석 |
학습분의 만석은 705/12,065건이었다. 시작 log-odds는 ln(705/11,360) = -2.7797, 만석확률은 .0584다.
첫 나무에서 이 행은 상류 잔여석 0 ≤ 1.5, 정류장 순번 55 ≤ 64를 차례로 통과했다. 학습률을 반영한 잎 보정값은 +.5084였다.
| 단계 | 만석확률 |
|---|---|
| 시작값 | .0584 |
| 나무 1개 뒤 | .0935 |
| 나무 50개 뒤 | .8095 |
| 나무 150개 뒤 | .9365 |
| 나무 250개 뒤 raw | .9114 |
| isotonic 보정 뒤 | .8577 |
사용자용 p_board=1-p_full | .1423 |
250개 잎 보정값의 합은 +5.1104였다. 최종 점수는 2.3307, raw 만석확률은 .9114가 됐다. 나무가 늘수록 확률이 한 방향으로만 움직이지는 않는다. 150개 뒤 .9365에서 마지막에는 .9114로 내려왔다.
train 내부 시간순 OOF 10,236건 중 만석 572건으로 맞춘 58개 임계점의 isotonic 보정기가 .9114를 .8577로 바꿨다. 실제 출발은 만석이었다. 한 건을 맞힌 사례일 뿐, 이 한 줄이 성능 증거는 아니다.
4. Q. 무엇을 넣고, 무엇을 뺐나?
최종 입력은 46열이다. 문자열은 노선과 원천 표시만 숫자로 바꿨다. 원천별로 따로 적합하므로 원천 표시는 한 적합 안에서 변별력이 없다.
| 묶음 | 실제 입력 | 넣은 이유 |
|---|---|---|
| 축·시각 | 노선, 정류장 순번, 로컬 분·시·30분대·요일·날짜 인덱스, 방향·회차, 축거리, 계획 배차, 직전 폴 간격 | 만석 위험의 1차 구조가 위치와 시간에 있었기 때문 |
| 상류 버스 | 존재, 축거리, 동률 수, 절대 잔여석, 상태, 좌석 변화율, 축 속도 | 곧 올 관측 버스의 현재 상태를 쓰기 위해 |
| 통과 버스 | 존재, 축거리, 동률 수, 절대 잔여석, 상태 | 방금 지나간 공급 상태를 남기기 위해 |
| 통과 이력 | 직전 통과의 잔여석·만석 여부·나이, 최근 통과 나이 구간, 관측 헤드웨이 구간, 공간 헤드웨이 | 점시각을 만들지 않고 과거 공급 간격을 쓰기 위해 |
| 노선 응답 요약 | 차량 수, 좌석값이 있는 차량 수, 만석 대수, 잔여석 평균·표준편차·최솟값·p10·중앙값·p90 | 한 버스 밖의 노선 전체 혼잡 국면을 잡기 위해 |
안 넣은 재료도 명확하다.
| 아직 못 써본 재료 | 이유 |
|---|---|
| 정원·점유율 | 라벨은 출발 후 잔여석 0 하나로 정해진다. 코퍼스 가명 차량과 외부 vehId의 교집합은 0/77이다. 관측 최대는 정원의 하한일 뿐이라 대리 정원도 쓰지 않는다. |
crowded 혼잡도 | 대상 노선 유형 11은 공식 제공 유형 13·15·23 밖이다. 직접 수집분에서 같은 잔여석에 다른 코드가 붙은 값이 26개라 잔여석만의 함수는 아니다. 다만 같은 (vehId, remainSeatCnt)에서는 충돌이 0셀이고 산식도 미확정이라 아직 넣지 않았다. |
queryTime·HTTP Date | 같은 노선 재호출 38쌍 중 queryTime이 7번 되감겼다. HTTP Date도 같은 시계였다. 시간·속도·헤드웨이는 로컬 응답 수신시각만 쓴다. |
| 도착 ETA | 공급자의 예측값이며 v1 목표가 아니다. 분·초 필드도 6슬롯 중 2슬롯이 단순 나눗셈과 달랐다. |
| 미래의 출발 후 잔여석 | 입력에 넣으면 정답 누수다. 출발 후 0/양수는 label_full을 만드는 데만 쓴다. |
-1의 0 치환 | -1은 결측이다. 0으로 바꾸면 가짜 만석이 된다. 결측으로 남긴다. |
| 모델 | ECE | Brier | log loss | AUC |
|---|---|---|---|---|
| 정류장×시간 기준선 raw | .0063 | .0327 | .1033 | .9613 |
| 구간 검열 isotonic | .0053 | .0268 | .0878 | .9751 |
| GB 상한 isotonic | .0053 | .0155 | .0524 | .9910 |
보조 코퍼스 validation 10,040건·만석 583건·정류소×날짜 644군집의 점추정이다.
5. 그림 명세
6. Q. 언제 무너지고, 프로토타입과 무엇이 다른가?
언제 실패하나?
- 직접 수집 학습 1,081건·만석 40건, 검증 443건·만석 14건으로 다시 적합해 AUC .9595를 냈다. 검증 만석이 14건이고 수집 날짜가 하루뿐이라 날짜 간 변동은 구간에 들어오지 않는다. 같은 날짜라 isotonic 보정 fold가 없어 이 실행의 예측은 raw다.
- 코퍼스 검증 10,040건 중 train에 없던 셀은 2건뿐이고 만석은 0건이었다. 새 노선·새 정류장·새 시간 셀 성능: 아직 없음.
- 잎 하나는 최소 30건이어도 250개 잎의 교집합은 훨씬 작아진다. 46개 입력에서 우연한 조합과 결측 패턴까지 외울 수 있다. 한 예측의 이유도 250개 경로를 합쳐야 나와 짧고 안정적인 운영 설명이 어렵다.
- 보조 코퍼스와 직접 수집은 원문 필드와 결측 구조가 다르다. 코퍼스에서 잘 먹힌 갈림길이 직접 원천에서 그대로 남는다는 근거가 없다.
- 최근접 차량은
next_observed_bus다. 90초 이하 구간에서도 숨은 제3차량 하한이 3330 17.24%, 1650 12.01%였다. 관측 열이 바뀌면 앞차·헤드웨이 특징도 함께 바뀐다. - isotonic은 58개 임계점의 계단이다. 보정 구간의 사건이 적으면 서로 다른 raw 점수가 같은 확률로 뭉친다.
- 같은 개발 validation을 구현과 보정 선택 과정에서 반복 확인했다. 현재 신뢰구간은 이 선택 불확실성을 담지 않는다. 코드와 보정기를 얼린 뒤 새 연속 미래일로 다시 재야 한다.
- 이름의 상한은 이론적 최대가 아니다. 이번 특징집합과 네 하이퍼파라미터 설정에서 얻은 최고 기록이다.
프로토타입과 뭐가 다른가?
| 관점 | 프로토타입 | Gradient boosting 상한 |
|---|---|---|
| 맞히는 것 | 화면 탑승률은 셀의 1-zeroCount/samples; 좌석 예보는 별도 계산 | 다음 관측 버스의 출발 후 label_full 하나 |
| 계산 | 현재 잔여석에서 정류장별 정규 수요를 빼며 0~80석 분포 전파 | 46개 입력을 250개 나무로 나눠 만석 log-odds 합산 |
| 얇은 셀 | history 셀 비만석률을 그대로 표시 | 잎당 최소 30건·L2 10으로 암묵 풀링 |
| 고정 규칙 | 2분/순번, 10석 여유폭, 90분 streak 등 | 데이터에서 분할 경계를 학습. 계획 배차와 로컬 시각은 입력으로만 사용 |
| 확률 보정 | 별도 보정층 없음 | train 내부 시간순 OOF 10,236건으로 isotonic 적합 |
| 쓰임 | 사용자 화면 기능 | 비교표 전용. 배포·채택 대상 아님 |
프로토타입의 좌석 예보를 더 복잡하게 만든 모델이 아니다. 질문부터 바뀌었다. 이 모델은 몇 석 남는지, 몇 분 기다리는지, 몇 대 보내는지 답하지 않는다. 운영 후보가 상한과 얼마나 벌어졌는지만 잰다.
재료 한눈에
아직 못 써본 것과 그 이유
- 정원·점유율 — 라벨은 출발 후 잔여석 0 하나로 정해진다. 코퍼스 가명 차량과 외부 vehId의 교집합은 0/77이다. 관측 최대는 정원의 하한일 뿐이라 대리 정원도 쓰지 않는다.
- crowded 혼잡도 — 대상 노선 유형 11은 공식 제공 유형 13·15·23 밖이다. 직접 수집분에서 같은 잔여석에 다른 코드가 붙은 값이 26개라 잔여석만의 함수는 아니지만, 같은 (vehId, 잔여석)에서는 충돌이 0셀이고 산식도 미확정이라 아직 넣지 않았다.
- queryTime·HTTP Date — 같은 노선 재호출 38쌍 중 queryTime이 7번 되감겼다. HTTP Date도 같은 시계였다. 시간·속도·헤드웨이는 로컬 응답 수신시각만 쓴다.
- 도착 ETA — 공급자의 예측값이며 v1 목표가 아니다. 분·초 필드도 6슬롯 중 2슬롯이 단순 나눗셈과 달랐다.
- 미래의 출발 후 잔여석 — 입력에 넣으면 정답 누수다. 출발 후 0/양수는 label_full을 만드는 데만 쓴다.
- -1의 0 치환 — -1은 결측이다. 0으로 바꾸면 가짜 만석이 된다. 결측으로 남긴다.
아래 성적은 남의 20일 코퍼스로 낸 것이다. 그 코퍼스는 차량 ID가 가명이라 정원·차종·저상 여부를 붙일 수 없었다(조인 0/77). 이 재료들은 테스트 자체를 못 했다.
우리 직접 수집분에는 원본 차량 ID가 남아 정원이 실제로 붙는다 — 고유차량 55/62대(88.7%). 다만 1회차 재대결에서 정원·차종을 넣어도 뚜렷한 개선은 확인되지 않았다. 표본이 얇아 판정이 안 될 뿐, 쓸모없다는 뜻은 아니다. 관측 최대 잔여석으로 정원을 추정하는 것은 계속 금지다 — 원본 ID를 써도 그 값은 관측할수록 올라가서 정원이 아니다.
직접 수집분으로 다시 잰 성적 1회차
우리 수집기가 직접 쌓은 데이터만 쓰고 코퍼스는 뺐다. 검증분 443행·만석 14건, 정류소×날짜 125군집 부트스트랩 5,000회.
| 지표 | 직접 수집분 (95% 구간) | 코퍼스 |
|---|---|---|
| 확률이 정직한가 | 0.0165[0.0073, 0.0297] | 0.0053 |
| 얼마나 빗나갔나 | 0.0144[0.0052, 0.0249] | 0.0155 |
| 확신하고 틀린 벌점 | 0.0731[0.0255, 0.1282] | 0.0524 |
| 만석을 골라내나 | 0.9595[0.9239, 0.9901] | 0.9910 |
표본이 얇아 21쌍 중 16쌍이 구분되지 않았다. 검증 날짜가 하루뿐이라 날짜 간 변동도 못 본다. 21쌍 중 16쌍을 못 가리고 validation 날짜가 1개라 전체 순위를 발표하지 않는다
코퍼스 순위가 뒤집힌 쌍은 0개다. 방향은 틀리지 않았고 미세한 차이만 미지로 남았다. ΔAUC 0.05를 가리려면 8 수집일, 날짜 변동까지 보려면 15일이 더 든다.
주의 — 이번 실행에서는 보정층이 작동하지 않았다. 검증 날짜가 하루뿐이라 적격 보정 fold가 없었다. 이름에 isotonic이 붙어 있어도 위 숫자는 보정 전 예측이다. 보정 효과는 반증된 것이 아니라 아직 재지 못한 것이다.
쓰는 재료 47개 — 출처 필드까지
| 재료 | 종류 | 어느 API의 어느 필드 |
|---|---|---|
목표 축 순번 (station_seq)stateCd=1이면 현재 stationSeq-1, stateCd=0·2이면 현재 stationSeq를 마지막 통과 순번으로 놓는다. 인접 적격 응답에서 이 값이 전진한 끝점을 통과 사건 순번으로 삼는다.주의 — 모델 축은 routestation 전체 순번이다. 학습 행은 서비스 정차점만 남겨도 축 간격에는 미정차·경유 지점이 들어간다. | 파생 | msgBody.busLocationList[].vehId, msgBody.busLocationList[].stationSeq, msgBody.busLocationList[].stateCd; routestation.staOrder; S1 station_seqGBIS 버스위치정보 getBusLocationListv2 + GBIS routestation 마스터 + S1 통과 사건 빌더 |
KST 하루 중 분 (query_minute_of_day)KST 시각에서 hour*60+minute을 계산한다.주의 — 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 |
KST 시 (query_hour)KST 응답 수신시각의 hour를 뽑는다.주의 — msgHeader.queryTime의 시각이 아니다. | 파생 | timing.response_received_at.kst, timing.response_received_at.utc; S1 E4 response_received_kst, response_received_utc; 코퍼스 collectedAt; S1 query_hour우리 수집기 응답 수신시각; 제한 코퍼스는 수집기가 붙인 collectedAt |
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 |
요일 (query_day_of_week)KST 날짜에 Python weekday()를 적용한다. 월요일 0, 일요일 6이다.주의 — 공휴일 표시는 없다. 요일만 구분한다. | 파생 | timing.response_received_at.kst, timing.response_received_at.utc; S1 E4 response_received_kst, response_received_utc; 코퍼스 collectedAt; S1 query_day_of_week우리 수집기 응답 수신시각; 제한 코퍼스는 수집기가 붙인 collectedAt |
날짜 순번 (query_day_index)KST 날짜에서 2026-07-19를 뺀 일수를 쓴다.주의 — 이 데이터셋의 고정 기준일에 묶인 값이다. 달력 날짜의 보편적 ID가 아니다. | 파생 | timing.response_received_at.kst, timing.response_received_at.utc; S1 E4 response_received_kst, response_received_utc; 코퍼스 collectedAt; S1 query_day_index우리 수집기 응답 수신시각; 제한 코퍼스는 수집기가 붙인 collectedAt |
계획 배차간격 (master_dispatch_interval_min)현재 구현은 평일 06:30~09:29·17:00~20:29에 peekAlloc, 그 밖 평일에 npeekAlloc을 고른다. 토요일은 satNpeekAlloc, 일요일은 sunNpeekAlloc을 고른다.주의 — 계획값이지 실제 헤드웨이가 아니다. 현재 코드의 피크 조건에 평일 검사가 들어가 토·일 *PeekAlloc 분기는 닿지 않는다. we*도 고르지 않는다. | 파생 | route.peekAlloc, route.npeekAlloc, route.satPeekAlloc, route.satNpeekAlloc, route.sunPeekAlloc, route.sunNpeekAlloc, route.wePeekAlloc, route.weNpeekAlloc; timing.response_received_at.kst, timing.response_received_at.utc; S1 master_dispatch_interval_minGBIS route 마스터 + 우리 수집기 응답 수신시각 + S1 특징 빌더 |
직전 적격 응답 간격 (previous_poll_gap_sec)같은 원천·수집 묶음·노선·KST 날짜·고정 수집창 안의 직전 적격 응답과 초 차이를 낸다. 0<gap<=90인 연결만 쓴다.주의 — 실패·정상 0대 호출은 학습 스냅샷이 아니다. 90초 초과, 날짜 경계, 수집창 경계에서는 연결을 끊는다. | 파생 | timing.response_received_at.kst, timing.response_received_at.utc; S1 E4 response_received_kst, response_received_utc; 코퍼스 collectedAt; S1 previous_poll_gap_sec우리 수집기 응답 수신시각 + S1 호출 연결 |
회차 순번 (turn_seq)대상 노선의 route.turnSeq를 그대로 붙인다.주의 — routestation에서 읽는 값이 아니다. | 응답 원본 | route.turnSeq; S1 turn_seqGBIS route 마스터 |
회차 전 여부 (is_outbound)station_seq<=turn_seq면 1, 아니면 0이다.주의 — 공식 upDown을 그대로 쓴 값이 아니다. 내부 순번축을 회차점에서 둘로 나눈 표시다. | 파생 | route.turnSeq, S1 station_seq; S1 is_outboundGBIS route 마스터 + S1 통과 사건 |
기점부터 축 순번거리 (axis_distance_from_origin_seq)station_seq-1을 계산한다.주의 — 표시용 서비스 정류장 수가 아니라 routestation 전체 순번 차이다. | 파생 | routestation.staOrder; S1 station_seq, axis_distance_from_origin_seqGBIS routestation 마스터 + S1 축 빌더 |
회차점까지 부호 있는 축 순번거리 (signed_axis_distance_to_turn_seq)turn_seq-station_seq를 계산한다.주의 — 회차점을 지나면 음수다. | 파생 | route.turnSeq, routestation.staOrder; S1 signed_axis_distance_to_turn_seqGBIS route 마스터 + GBIS routestation 마스터 + S1 축 빌더 |
기점부터 축 거리 km (axis_distance_from_origin_km)staOrder 순으로 인접한 x,y 좌표의 Haversine 거리를 누적한다.주의 — 도로 주행거리가 아니라 정류장 좌표 사이 대권거리 합이다. 현재 빌더는 station.x, station.y를 읽지 않는다. | 파생 | routestation.routeName, routestation.staOrder, routestation.x, routestation.y; S1 axis_distance_from_origin_kmGBIS routestation 마스터 + S1 축 빌더 |
| 회차점까지 축 거리 km (axis_distance_to_turn_km)목표 순번의 누적 Haversine 거리와 회차 순번의 누적 거리 차의 절댓값을 쓴다.주의 — 도로망 최단거리나 실시간 이동거리가 아니다. | 파생 | route.turnSeq; routestation.staOrder, routestation.x, routestation.y; S1 axis_distance_to_turn_kmGBIS route 마스터 + GBIS routestation 마스터 + S1 축 빌더 |
상류 버스 존재 (upstream_bus_exists)정규화 마지막 통과 순번이 목표 station_seq보다 작은 차량이 하나라도 있으면 1이다.주의 — API가 노선의 모든 차량을 포착한다는 뜻은 아니다. | 파생 | msgBody.busLocationList[].vehId, msgBody.busLocationList[].stationSeq, msgBody.busLocationList[].stateCd; S1 정규화 last_passed; S1 upstream_bus_existsGBIS 버스위치정보 getBusLocationListv2 + S1 스냅샷 특징 빌더 |
상류 버스 축거리 (upstream_axis_gap_seq)상류 후보 중 목표에 가장 가까운 차량을 고르고 target station_seq-last_passed를 계산한다.주의 — 동률은 출발 stateCd=2, 교차 0, 도착 1 순, 더 최신 운행, 차량키 사전순으로 푼다. 좌석값은 선택 기준이 아니다. | 파생 | msgBody.busLocationList[].vehId, msgBody.busLocationList[].stationSeq, msgBody.busLocationList[].stateCd; S1 정규화 last_passed; S1 upstream_axis_gap_seqGBIS 버스위치정보 getBusLocationListv2 + S1 스냅샷 특징 빌더 |
상류 최근접 동률 수 (upstream_tie_count)최소 upstream_axis_gap_seq에 놓인 후보 차량 수를 센다.주의 — 응답 전체 차량 수가 아니다. | 파생 | msgBody.busLocationList[].vehId, msgBody.busLocationList[].stationSeq, msgBody.busLocationList[].stateCd; S1 정규화 last_passed; S1 upstream_tie_countGBIS 버스위치정보 getBusLocationListv2 + S1 스냅샷 특징 빌더 |
상류 버스 잔여석 (upstream_remain_seats)축거리와 동률 규칙으로 고른 상류 차량의 remainSeatCnt를 붙인다.주의 — -1은 0으로 바꾸지 않고 결측으로 둔다. 이 값은 목표 차량의 미래 라벨이 아니다. | 파생 | msgBody.busLocationList[].remainSeatCnt, msgBody.busLocationList[].stationSeq, msgBody.busLocationList[].stateCd, msgBody.busLocationList[].vehId; S1 upstream_remain_seatsGBIS 버스위치정보 getBusLocationListv2 + S1 최근접 상류차 선택 |
상류 버스 상태 (upstream_state_cd)선택된 상류 차량의 원 stateCd를 붙인다.주의 — 로지스틱은 0·1·2를 원핫 표시로 바꾼다. 상태 의미와 축 위치 정규화는 별개다. | 파생 | msgBody.busLocationList[].stateCd, msgBody.busLocationList[].stationSeq, msgBody.busLocationList[].vehId; S1 upstream_state_cdGBIS 버스위치정보 getBusLocationListv2 + S1 최근접 상류차 선택 |
상류 버스 좌석 변화율 (upstream_seat_change_per_min)선택된 차량을 직전 적격 응답의 같은 차량과 잇고 (현재 좌석-직전 좌석)/(경과초/60)을 계산한다.주의 — 두 좌석값이 비음수이고 0<경과<=90초, 정규화 순번 변화가 0 또는 1일 때만 만든다. | 파생 | msgBody.busLocationList[].vehId, msgBody.busLocationList[].stationSeq, msgBody.busLocationList[].stateCd, msgBody.busLocationList[].remainSeatCnt; timing.response_received_at.kst, timing.response_received_at.utc; S1 upstream_seat_change_per_minGBIS 버스위치정보 getBusLocationListv2 + 우리 수집기 응답 수신시각 + S1 차량 연결 |
상류 버스 축 속도 (upstream_speed_seq_per_min)같은 차량의 정규화 순번 변화량을 (경과초/60)으로 나눈다.주의 — 0<경과<=90초이고 순번 변화가 0 또는 1일 때만 만든다. +2 이상 도약은 속도로 쓰지 않는다. | 파생 | msgBody.busLocationList[].vehId, msgBody.busLocationList[].stationSeq, msgBody.busLocationList[].stateCd; timing.response_received_at.kst, timing.response_received_at.utc; S1 upstream_speed_seq_per_minGBIS 버스위치정보 getBusLocationListv2 + 우리 수집기 응답 수신시각 + S1 차량 연결 |
이미 지난 버스 존재 (passed_bus_exists)정규화 마지막 통과 순번이 목표 station_seq 이상인 차량이 하나라도 있으면 1이다.주의 — 그 차량이 사용자가 탈 다음 차량이라는 뜻은 아니다. | 파생 | msgBody.busLocationList[].vehId, msgBody.busLocationList[].stationSeq, msgBody.busLocationList[].stateCd; S1 정규화 last_passed; S1 passed_bus_existsGBIS 버스위치정보 getBusLocationListv2 + S1 스냅샷 특징 빌더 |
지난 버스 축거리 (passed_axis_gap_seq)이미 지난 후보 중 목표에 가장 가까운 차량을 고르고 last_passed-target station_seq를 계산한다.주의 — 동률 규칙은 상류 선택과 같다. 좌석값은 선택 기준이 아니다. | 파생 | msgBody.busLocationList[].vehId, msgBody.busLocationList[].stationSeq, msgBody.busLocationList[].stateCd; S1 정규화 last_passed; S1 passed_axis_gap_seqGBIS 버스위치정보 getBusLocationListv2 + S1 스냅샷 특징 빌더 |
지난 버스 최근접 동률 수 (passed_tie_count)최소 passed_axis_gap_seq에 놓인 후보 차량 수를 센다.주의 — 응답 전체 차량 수가 아니다. | 파생 | msgBody.busLocationList[].vehId, msgBody.busLocationList[].stationSeq, msgBody.busLocationList[].stateCd; S1 정규화 last_passed; S1 passed_tie_countGBIS 버스위치정보 getBusLocationListv2 + S1 스냅샷 특징 빌더 |
지난 버스 잔여석 (passed_remain_seats)축거리와 동률 규칙으로 고른 지난 차량의 remainSeatCnt를 붙인다.주의 — -1은 결측이다. 목표 정류장을 지난 뒤의 관측이라도 목표 차량의 정답 라벨과는 다른 스냅샷 차량이다. | 파생 | msgBody.busLocationList[].remainSeatCnt, msgBody.busLocationList[].stationSeq, msgBody.busLocationList[].stateCd, msgBody.busLocationList[].vehId; S1 passed_remain_seatsGBIS 버스위치정보 getBusLocationListv2 + S1 최근접 지난 차 선택 |
지난 버스 상태 (passed_state_cd)선택된 지난 차량의 원 stateCd를 붙인다.주의 — 로지스틱은 0·1·2 원핫 표시로 바꾼다. | 파생 | msgBody.busLocationList[].stateCd, msgBody.busLocationList[].stationSeq, msgBody.busLocationList[].vehId; S1 passed_state_cdGBIS 버스위치정보 getBusLocationListv2 + S1 최근접 지난 차 선택 |
| 직전 통과 라벨 존재 (previous_passage_exists)같은 원천·노선·정류장에서 질의시각 전에 구간이 끝나고 라벨까지 관측된 가장 최근 통과가 있으면 1이다.주의 — 현재 행의 미래 라벨은 후보가 될 수 없다. 원천을 넘겨 연결하지 않는다. | 파생 | S1 passage_events.source, passage_events.route, passage_events.station_seq, passage_events.passage_interval_end, passage_events.label_observed_at, passage_events.label_status; 특징 previous_passage_existsS1 passage_events |
직전 통과 출발 후 잔여석 (previous_passage_remain_seats)인과 조건을 만족하는 같은 정류장의 가장 최근 통과에서 label_remain_seats를 가져온다.주의 — 현재 목표 차량의 출발 후 좌석이 아니다. | 파생 | S1 passage_events.label_remain_seats, passage_events.label_observed_at, passage_events.passage_interval_end; 특징 previous_passage_remain_seatsS1 passage_events |
직전 통과 만석 여부 (previous_passage_is_full)인과 조건을 만족하는 가장 최근 같은 정류장 통과의 label_full을 가져온다.주의 — 직전 API 응답 한 대와 같은 뜻이 아니다. 직전으로 확정된 통과 사건이다. | 파생 | S1 passage_events.label_full, passage_events.label_observed_at, passage_events.passage_interval_end; 특징 previous_passage_is_fullS1 passage_events |
직전 통과 라벨 나이 (previous_passage_label_age_sec)현재 질의 응답 수신시각에서 직전 통과 라벨 관측시각을 뺀다.주의 — msgHeader.queryTime으로 빼지 않는다. | 파생 | timing.response_received_at.kst, timing.response_received_at.utc; S1 E4 response_received_kst, response_received_utc; 코퍼스 collectedAt; S1 passage_events.label_observed_at, previous_passage_label_age_sec우리 수집기 응답 수신시각 + S1 passage_events |
| 최근 통과 나이 하한 (latest_passage_age_lower_sec)현재 질의시각에서 최근 통과 구간의 끝을 뺀다.주의 — 통과시각이 폴링 사이 구간이라 한 점의 정확한 나이가 아니다. | 파생 | timing.response_received_at.kst, timing.response_received_at.utc; S1 E4 response_received_kst, response_received_utc; 코퍼스 collectedAt; S1 passage_events.passage_interval_end, latest_passage_age_lower_sec우리 수집기 응답 수신시각 + S1 passage_events |
| 최근 통과 나이 상한 (latest_passage_age_upper_sec)현재 질의시각에서 최근 통과 구간의 시작을 뺀다.주의 — 하한과 함께 봐야 한다. | 파생 | timing.response_received_at.kst, timing.response_received_at.utc; S1 E4 response_received_kst, response_received_utc; 코퍼스 collectedAt; S1 passage_events.passage_interval_start, latest_passage_age_upper_sec우리 수집기 응답 수신시각 + S1 passage_events |
| 관측 헤드웨이 하한 (observed_headway_lower_sec)같은 원천·노선·정류장의 최근 통과 시작에서 그 전 통과 끝을 빼고 0 아래를 자른다.주의 — 정확 통과시각 차가 아니라 두 검열 구간이 허용하는 하한이다. | 파생 | S1 passage_events.passage_interval_start, passage_events.passage_interval_end; 특징 observed_headway_lower_secS1 passage_events의 연속 통과 구간 |
| 관측 헤드웨이 상한 (observed_headway_upper_sec)최근 통과 끝에서 그 전 통과 시작을 빼고 0 아래를 자른다.주의 — 계획 배차간격과 다른 관측 구간 상한이다. | 파생 | S1 passage_events.passage_interval_start, passage_events.passage_interval_end; 특징 observed_headway_upper_secS1 passage_events의 연속 통과 구간 |
응답 내 노선 차량 수 (route_vehicle_count)make_observation 정규화에 성공한 차량 행 수를 센다.주의 — 실제 운행 대수 보장이 아니다. 차량키·순번·상태가 잘못된 원행은 먼저 빠진다. 정상 0대와 실패도 학습 스냅샷 행이 아니다. | 파생 | msgBody.busLocationList[]; S1 E4 derived/observations.jsonl 행; 코퍼스 vehicles[]; S1 route_vehicle_countGBIS 버스위치정보 getBusLocationListv2 |
좌석값이 있는 차량 수 (route_known_seat_count)remainSeatCnt>=0인 차량 수를 센다.주의 — -1은 포함하지 않는다. | 파생 | msgBody.busLocationList[].remainSeatCnt; S1 route_known_seat_countGBIS 버스위치정보 getBusLocationListv2 |
응답 내 0석 차량 수 (route_full_bus_count)remainSeatCnt==0인 차량 수를 센다.주의 — 응답에 잡힌 차량만 센다. | 파생 | msgBody.busLocationList[].remainSeatCnt; S1 route_full_bus_countGBIS 버스위치정보 getBusLocationListv2 |
노선 응답 잔여석 평균 (route_seat_mean)응답 안의 비음수 remainSeatCnt에 산술평균을 적용한다.주의 — remainSeatCnt=-1은 제외한다. 유효 좌석값이 없으면 결측이다. | 파생 | msgBody.busLocationList[].remainSeatCnt; S1 route_seat_meanGBIS 버스위치정보 getBusLocationListv2 + S1 노선 응답 요약 |
노선 응답 잔여석 표준편차 (route_seat_std)응답 안의 비음수 remainSeatCnt에 모표준편차(ddof=0)을 적용한다.주의 — remainSeatCnt=-1은 제외한다. 유효 좌석값이 없으면 결측이다. | 파생 | msgBody.busLocationList[].remainSeatCnt; S1 route_seat_stdGBIS 버스위치정보 getBusLocationListv2 + S1 노선 응답 요약 |
노선 응답 잔여석 최솟값 (route_seat_min)응답 안의 비음수 remainSeatCnt에 최솟값을 적용한다.주의 — remainSeatCnt=-1은 제외한다. 유효 좌석값이 없으면 결측이다. | 파생 | msgBody.busLocationList[].remainSeatCnt; S1 route_seat_minGBIS 버스위치정보 getBusLocationListv2 + S1 노선 응답 요약 |
노선 응답 잔여석 10백분위 (route_seat_p10)응답 안의 비음수 remainSeatCnt에 numpy.quantile 기본 선형 보간의 0.10 분위수를 적용한다.주의 — remainSeatCnt=-1은 제외한다. 유효 좌석값이 없으면 결측이다. | 파생 | msgBody.busLocationList[].remainSeatCnt; S1 route_seat_p10GBIS 버스위치정보 getBusLocationListv2 + S1 노선 응답 요약 |
노선 응답 잔여석 중앙값 (route_seat_median)응답 안의 비음수 remainSeatCnt에 numpy.quantile 기본 선형 보간의 0.50 분위수를 적용한다.주의 — remainSeatCnt=-1은 제외한다. 유효 좌석값이 없으면 결측이다. | 파생 | msgBody.busLocationList[].remainSeatCnt; S1 route_seat_medianGBIS 버스위치정보 getBusLocationListv2 + S1 노선 응답 요약 |
노선 응답 잔여석 90백분위 (route_seat_p90)응답 안의 비음수 remainSeatCnt에 numpy.quantile 기본 선형 보간의 0.90 분위수를 적용한다.주의 — remainSeatCnt=-1은 제외한다. 유효 좌석값이 없으면 결측이다. | 파생 | msgBody.busLocationList[].remainSeatCnt; S1 route_seat_p90GBIS 버스위치정보 getBusLocationListv2 + S1 노선 응답 요약 |
스냅샷 공간 헤드웨이 (spatial_headway_axis_seq)선택된 지난 차량의 last_passed에서 선택된 상류 차량의 last_passed를 뺀다.주의 — 양쪽 차량이 모두 있어야 한다. 시간 헤드웨이가 아니라 전체 순번축의 간격이다. | 파생 | msgBody.busLocationList[].stationSeq, msgBody.busLocationList[].stateCd, msgBody.busLocationList[].vehId; S1 spatial_headway_axis_seqGBIS 버스위치정보 getBusLocationListv2 + S1 상류·지난 차 선택 |
노선 (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 마스터 |
원천 구분 (source_kind)직접 수집 행은 direct_e4, 제한 코퍼스 행은 corpus로 붙인다.주의 — GBIS 응답 필드가 아니다. GBM에는 범주형 예측 재료로 들어가고, 구간 검열에서는 같은 원천끼리 연결하는 데 쓴다. | 파생 | S1 source_kindS1 데이터셋 빌더 |
출발 후 만석 라벨 (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 통과 사건 빌더 |
지금은 안 쓰는 재료 6개
- 차량 정원·점유율 —
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 일치해 독립 시계가 아니다. 감사 원문만 보존하고 나이·속도·헤드웨이·분할에 쓰지 않는다. - 도착 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 후보 입력에서 뺐다. - 목표 차량 ID와 미래 출발 후 좌석·라벨 —
msgBody.busLocationList[].remainSeatCnt; S1label_remain_seats,label_full,target_vehicle_hashtarget_vehicle_hash는 차량 암기를 막기 위해 감사키로만 둔다. 출발 후 좌석과label_full은 목표 통과 뒤 생기는 미래 정답이라 학습의 y로만 쓰고 예측 행렬에는 넣지 않는다. remainSeatCnt=-1을 0석으로 대치 — GBIS getBusLocationListv2msgBody.busLocationList[].remainSeatCnt20일 코퍼스 차량행에서-1은 69건이다. 좌석 미제공이며 만석 0과 뜻이 다르므로 결측으로 둔다.
언제 실패하나
- 직접 수집 학습 1,081건·만석 40건, 검증 443건·만석 14건으로 다시 적합해 AUC .9595를 냈다. 검증 만석이 14건이고 수집 날짜가 하루뿐이라 날짜 간 변동은 구간에 들어오지 않는다. 같은 날짜라 isotonic 보정 fold가 없어 이 실행의 예측은 raw다.
- 코퍼스 검증 10,040건 중 train에 없던 셀은 2건뿐이고 만석은 0건이었다. 새 노선·새 정류장·새 시간 셀 성능: 아직 없음.
- 잎 하나는 최소 30건이어도 250개 잎의 교집합은 훨씬 작아진다. 46개 입력에서 우연한 조합과 결측 패턴까지 외울 수 있다. 한 예측의 이유도 250개 경로를 합쳐야 나와 짧고 안정적인 운영 설명이 어렵다.
- 보조 코퍼스와 직접 수집은 원문 필드와 결측 구조가 다르다. 코퍼스에서 잘 먹힌 갈림길이 직접 원천에서 그대로 남는다는 근거가 없다.
- 최근접 차량은 next_observed_bus다. 90초 이하 구간에서도 숨은 제3차량 하한이 3330 17.24%, 1650 12.01%였다. 관측 열이 바뀌면 앞차·헤드웨이 특징도 함께 바뀐다.
- isotonic은 58개 임계점의 계단이다. 보정 구간의 사건이 적으면 서로 다른 raw 점수가 같은 확률로 뭉친다.
- 같은 개발 validation을 구현과 보정 선택 과정에서 반복 확인했다. 현재 신뢰구간은 이 선택 불확실성을 담지 않는다. 코드와 보정기를 얼린 뒤 새 연속 미래일로 다시 재야 한다.
- 이름의 상한은 이론적 최대가 아니다. 이번 특징집합과 네 하이퍼파라미터 설정에서 얻은 최고 기록이다.
기존 프로토타입과 다른 점
- 프로토타입의 화면 탑승률은 셀의 1-zeroCount/samples였고 좌석 예보는 별도 계산이었다. GB 상한은 다음 관측 버스의 출발 후 label_full 하나를 맞힌다.
- 프로토타입은 현재 잔여석에서 정류장별 정규 수요를 빼며 0~80석 분포를 흘렸다. GB 상한은 46개 입력을 250개 나무로 나눠 만석 log-odds를 합친다.
- 프로토타입 history는 얇은 셀의 비만석률을 그대로 표시했다. GB 상한은 잎당 최소 30건과 L2 10으로 암묵 풀링한다.
- 프로토타입은 2분/순번, 10석 여유폭, 90분 streak 같은 고정 규칙을 썼다. GB 상한은 분할 경계를 데이터에서 학습한다.
- 프로토타입에는 별도 확률 보정층이 없었다. GB 상한은 train 내부 시간순 OOF 10,236건으로 isotonic을 맞췄다.
- 프로토타입은 사용자 화면 기능이었다. GB 상한은 비교표 전용이며 배포·채택 대상이 아니다.