재료

무슨 재료를 어떻게 쓰나

재료 대조

프로토타입은 이 버스의 지금 좌석에서 시작했다. 새 후보는 이 상황을 먼저 보고, 세 후보만 실시간 좌석을 더한다.

Q. 프로토타입에서 profile과 실시간 좌석은 어디서 만나나?

둘은 학습할 때가 아니라 예보할 때 만난다.

과거 스냅샷은 먼저 profile이 된다. 같은 버스의 두 관측 사이 좌석 차이를 구간 순수요로 놓고, 이를 정류장×30분×평일/주말 셀의 평균 μ와 표준편차 σ로 푼다. 여기서 수요는 승차에서 하차를 뺀 값이다. μ가 음수면 좌석이 늘어나는 구간이다.

화면이 예보를 요청하면 forecast.tsremainingSeats를 한 점짜리 좌석 분포로 만든다. 2026-08-05 범계역의 실제 관측 29석이라면 29에 확률 1을 놓는다. 그 뒤 현재 정류장부터 하류 정류장까지 profile의 순수요 분포를 하나씩 적용한다.

다음 좌석 = 지금 좌석 - 그 정류장의 순수요를 확률분포 전체에 반복하는 셈이다. 통장 잔액 29에서 앞으로 나갈 금액을 항목마다 빼는 가계부와 같다. 다만 지출액이 매번 같지 않아 μ·σ로 흔들림까지 흘려보낸다. 실시간 좌석이 -1이면 예보는 빈 배열로 끝난다.

따라서 시작점은 실시간 좌석이고, 정류장마다 빼는 값은 과거 통계다. 같은 차량의 분포를 노선 아래로 계속 옮긴다. 이것이 “이 버스”를 따라간다는 뜻이다.

프로토타입은 이 버스, 새 후보는 이 상황
과거 관측 → profile μ·σ이 버스의 지금 잔여석·위치정류장마다 순수요 분포 차감이 버스의 예상 잔여석
출발 후 만석 라벨로 학습노선·정류장·시각 + 선택적 실시간상황 점수화이 상황의 만석확률

프로토타입은 현재 잔여석에서 출발해 정류장별 과거 순수요를 뺀다. 새 후보는 시공간과 선택적 실시간 신호를 바로 만석확률로 바꾼다.

Q. 새 후보도 profile에서 수요를 꺼내 좌석을 빼나?

아니다. 새 후보 어느 것도 프로토타입의 수요 profile(μ·σ)을 입력으로 받지 않는다.

새 데이터셋의 정답은 한 정류장을 지난 뒤 잔여석이 0이었는가다. 후보는 순수요와 도착 좌석 수를 중간에 복원하지 않고 P(출발 후 만석 | 예측 시점까지의 재료)를 바로 배운다. 사용자에게는 그 반대인 탈 확률을 보여 준다.

프로토타입의 상태는 특정 차량의 잔여석 분포다. 주된 답도 “이 버스가 저 정류장에 갈 때 몇 석 남나”였다. 내부에서 1-P(0석)도 계산했지만 같은 차량 분포에서 파생된 값이다. 새 후보의 상태는 차량 ID 없는 한 시점의 상황이다. 정류장, 시각, 계획 배차, 필요하면 관측된 상류 버스 상태를 넣고 만석 사건의 확률 하나를 낸다.

답의 문법도 갈린다. 프로토타입은 “이 버스 29석”이고 새 후보는 “이 상황 만석 42.0%”다. 29석은 위 실제 차량 관측이다. 42.0%는 코퍼스 validation의 logistic_interactions_raw 한 행에서 나온 실측 예측값 0.420088을 반올림했다.

학습 재료와 서빙 재료
재료학습서빙쓰는 후보
노선·정류장·로컬 시간과거 행현재 질의6개 기본 후보
계획 배차정적 마스터사전 적재GAM·로지스틱·GBM
출발 후 만석 라벨정답미래라 없음기본 후보·보정 학습
상류·통과 버스 좌석·상태·거리과거 질의 시점 값현재 응답로지스틱·구간 검열·GBM
미관측 통과의 양끝 상태전이 규칙 학습학습된 규칙만구간 검열
기본 모델 확률OOF 확률+정답방금 낸 확률보정 후처리
정원·점유율코퍼스 조인 0/77, 직접분 88.7%vehId로 조인1회차 ablation만
crowded미사용. 같은 잔여석에 다른 코드 26개계약 밖 값아직 아무도 안 씀

미래에 생기는 라벨은 학습에만 있다. 세 실시간 후보는 학습의 과거 질의값과 서빙의 현재 응답을 같은 자리에 넣는다.

Q. 후보마다 지금 좌석을 어디까지 보나?

후보학습에서 보는 것서빙에서 보는 것실시간 좌석
경험적 베이즈노선×정류장×30분의 출발 후 만석 기록노선·정류장·30분대안 봄
계층 로지스틱노선·정류장·30분대와 그 조합의 만석 기록노선·정류장·30분대안 봄
GAM/스플라인노선·정류장 순번·분·요일·계획 배차와 만석 기록같은 정적·달력 재료안 봄
로지스틱+상호작용위 재료에 과거 통과, 상류·통과 버스, 노선 응답 요약을 붙임잔여석·상태·축거리·좌석 변화·헤드웨이
구간 검열셀 만석 기록과 못 본 통과의 양끝 상태 800쌍·중간 통과 821건상류 버스가 0석인지와 목표까지 축거리
Gradient boosting허용된 시공간·배차·상류·통과·노선 요약 전부같은 가용 특징봄. 상한 측정 전용
보정 후처리기본 모델의 시간순 OOF 확률과 정답기본 모델이 방금 낸 확률원자료는 안 봄

학습과 서빙에는 비대칭이 있다. 출발 후 만석 라벨은 예측 시점 뒤에 생기므로 학습에만 있다. 반대로 서빙의 상류 버스 좌석은 지금 응답에서 얻고, 학습에서는 과거 질의 시점에 저장된 같은 종류의 값으로 맞춘다. 구간 검열의 못 본 중간 통과는 전이 규칙을 배울 때만 쓴다. 서빙 때는 학습된 규칙에 현재 상류 상태와 거리를 넣는다. 보정기는 학습 때 OOF 확률 10,236건·만석 572건으로 눈금을 맞췄고, 서빙 때는 기본 확률만 받는다.

정원과 점유율은 넣지 않는다. 보조 코퍼스의 가명 차량 77대는 외부 정원과 조인이 0대였다. 서빙 응답의 원 vehId로는 정원이 붙는다. 직접 수집분에서는 학습 행 1,345/1,524(88.3%)에도 붙었다. 다만 정원·차종을 추가한 대조에서 네 지표가 한 방향으로 좋아지지 않아 여섯 후보의 공통 특징으로는 두지 않는다. 라벨에도 정원이 필요 없다. crowded도 뺀다. 대상은 노선유형 11인데 공식 제공 대상은 13·15·23이고, 180초 이하 인접 관측 81,147쌍에서 잔여석은 그대로인데 crowded만 바뀐 경우가 0건이었다.

Q. 실시간 좌석 없이 어떻게 AUC가 .96이나 나오나?

정류장과 시간이 이미 만석 위험의 대부분을 가르기 때문이다.

아무것도 안 본 평균은 AUC .5000이었다. 직전 통과 버스 상태만 보면 .7187이었다. 노선×정류장×30분만 보면 .9613이었다. 실시간 좌석을 안 쓰는 세 후보도 경험적 베이즈 .9613, 계층 로지스틱 .9632, GAM .9647을 냈다.

이 수치는 새 정류장을 잘 맞혔다는 뜻이 아니다. validation 10,040행 중 10,038행이 train에서 이미 본 셀이었다. validation 832셀 중 831셀도 겹쳤다. 정류장만 써도 AUC .9336이었고, 같은 셀의 train·validation 만석률 상관은 .9294였다. 같은 노선의 같은 정류장과 시간대가 며칠 뒤에도 비슷한 위험 순서를 보인 것이다.

누수만으로 나온 숫자도 아니다. train 라벨을 1,000번 섞어 같은 셀 조회를 다시 맞추자 AUC 평균은 .4970으로 무너졌다. 대신 범위가 좁다. 새 셀 validation은 2행·만석 0건뿐이다. 새 노선·새 정류장·새 시간대 성적은 아직 재지 못했다.

셀 자체는 강하지만 얇다. train 833셀 중 281셀(33.73%)은 표본 10건 이하고, 719셀(86.31%)은 만석을 한 번도 못 봤다. 그래서 후보들의 실제 차이는 실시간 사용 여부 하나가 아니라 이 얇은 셀을 얼마나 덜 과신하느냐에도 있다.

실시간 없이 .96, 실시간을 더하면 .97~.99
평균만0.5000
직전 버스만0.7187
정류장·시간0.9613
경험적 베이즈0.9613
계층 로지스틱0.9632
GAM0.9647
구간 검열0.9748
로지스틱+상호작용0.9792
GBM 상한0.9923

20일 보조 코퍼스 validation 10,040건·만석 583건의 raw AUC다. 직접 수집 성적은 아직 없다.

Q. 실시간 좌석을 쓴 후보는 실제로 얼마나 나았나?

20일 보조 코퍼스의 시간순 validation 10,040건·만석 583건에서 raw AUC를 쟀다. 정류장·시간 기준선 .9613과 비교했다.

후보실시간 좌석raw AUC기준선과 차이
경험적 베이즈아니오.9613+.0000
계층 로지스틱아니오.9632+.0019
GAM/스플라인아니오.9647+.0034
구간 검열.9748+.0135
로지스틱+상호작용.9792+.0179
Gradient boosting.9923+.0310

실시간을 쓰는 세 후보가 더 높았다. 다만 이 차이 전부를 실시간 좌석의 몫으로 읽으면 안 된다. 로지스틱은 상호작용을, GBM은 나무 구조를 함께 바꿨다. 실시간 특징만 켰다 끈 공통 ablation 성적은 아직 재지 못했다.

구간 검열은 비교가 한 단계 더 가깝다. 같은 경험적 베이즈 셀 위험에 검열 전이와 현재 상류 상태를 섞자 raw AUC가 .0135 올랐고, 군집 bootstrap 95% 구간은 +.0098~+.0176이었다. isotonic 보정 뒤 AUC는 .9751이었다. 층화 기준선과의 paired 차이는 +.0138 [+.0103,+.0179]였다.

GBM .9923은 운영 성적표가 아니라 가용 특징으로 낼 수 있는 상한이다. 직접 수집 validation은 443건·만석 14건이고 날짜가 하루뿐이다. AUC 21쌍 중 16쌍을 못 가려 위 순서가 실제 운영에서도 유지되는지는 아직 모른다.

Q. 그래서 무엇을 얻고 무엇을 잃나?

실시간 좌석을 안 보는 후보는 버스 위치 응답이 비어도 노선·정류장·시각만 있으면 답을 낸다. “그 버스가 안 보여도 이 상황은 평소 얼마나 위험한가”에 답한다. 대신 눈앞의 상류 버스가 지금 0석이라는 사실도 무시한다. 역사적으로 안전한 셀이라면 현재 만석 신호를 보고도 낮은 위험을 낼 수 있다.

실시간 좌석을 보는 후보는 그 반대다. 상류 버스의 0석, 상태, 목표까지 거리를 확률에 반영한다. 버스가 안 보이면 신호가 사라진다. 구간 검열 모형은 이때 경험적 베이즈 셀 확률로 돌아가고, 로지스틱은 존재 표시와 결측 대치를 쓴다. API가 실제 다음 차량을 모두 포착한다는 보장도 없다. 90초 이하 구간에서도 숨은 제3차량 하한은 3330 17.24%, 1650 12.01%였다.

프로토타입은 “이 버스”의 좌석을 앞으로 밀었다. 새 후보는 “이 상황”의 만석 확률을 매겼다. 실시간 미사용형은 끊기지 않는 대신 지금 버스에 둔감하다. 실시간 사용형은 지금을 보지만 관측 누락에 기대고, 아직 직접 만석 검증을 통과하지 못했다.

재료 한눈에

노선·정류장·순번·로컬 시간·요일 — 6개 기본 후보의 시공간 뼈대다.계획 배차 — GAM·로지스틱+상호작용·GBM만 쓴다. 실제 배차가 아니라 정적 계획이다.출발 후 만석 라벨 — 모든 후보의 학습 정답이다. 서빙 입력은 아니다.상류·통과 버스의 잔여석·상태·축거리·변화와 노선 응답 요약 — 실시간 사용 후보만 쓴다.미관측 통과의 양끝 상태와 구간 길이 — 구간 검열 모형의 학습 보조재료다.기본 모델의 OOF 확률 — 보정 후처리의 학습 입력이다.정원·점유율crowded도착 ETA·queryTime 파생값

아직 못 써본 것과 그 이유

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

아래 성적은 남의 20일 코퍼스로 낸 것이다. 그 코퍼스는 차량 ID가 가명이라 정원·차종·저상 여부를 붙일 수 없었다(조인 0/77). 이 재료들은 테스트 자체를 못 했다.

우리 직접 수집분에는 원본 차량 ID가 남아 정원이 실제로 붙는다 — 고유차량 55/62대(88.7%). 다만 1회차 재대결에서 정원·차종을 넣어도 뚜렷한 개선은 확인되지 않았다. 표본이 얇아 판정이 안 될 뿐, 쓸모없다는 뜻은 아니다. 관측 최대 잔여석으로 정원을 추정하는 것은 계속 금지다 — 원본 ID를 써도 그 값은 관측할수록 올라가서 정원이 아니다.

쓰는 재료 70개 — 출처 필드까지

재료종류어느 API의 어느 필드
프로토타입 현재 잔여석 (remainingSeats)API 잔여석 이름만 바꿔 현재 차량 좌석분포의 점질량 시작값으로 넣는다.주의 — 음수값은 profile 학습에서 빠진다. 라이브 값이 -1이면 예보는 빈 배열을 낸다.응답 원본msgBody.busLocationList[].remainSeatCnt → 코퍼스 vehicles[].remainingSeatsVehicleForecastRequest.remainingSeats20일 코퍼스 수집기; 원천은 GBIS getBusLocationListv2
프로토타입 현재 위치 순번 (currentSequence)같은 차량의 관측쌍과 현재·하류 위치를 순번으로 정렬한다.주의 — S1의 stateCd 정규화 통과축과 다르다. 프로토타입은 원 순번을 쓴다.응답 원본msgBody.busLocationList[].stationSeq → 코퍼스 vehicles[].currentStopSequenceVehicleForecastRequest.currentSequence20일 코퍼스 수집기; 원천은 GBIS getBusLocationListv2
프로토타입 차량키 (id)가명키로 같은 차량의 연속 관측을 잇는다.주의 — identitySourcehashVersion은 코퍼스 102,197행 모두 없다. 외부 차량 마스터와 조인하지 않는다.파생msgBody.busLocationList[].plateNo 우선, 없으면 msgBody.busLocationList[].vehId → HMAC-SHA256 앞 16 hex → 코퍼스 vehicles[].id20일 코퍼스 수집기
프로토타입 수집시각 (collectedAt)collectedAt을 KST 날짜·평일/주말·30분 셀과 관측 순서에 쓴다.주의 — collectedAt은 본문 수신·JSON 파싱 뒤 찍힌 시각이다. apiQueryTime은 서로 다른 서버 시계라 경과시간에 쓰지 않는다.응답 원본코퍼스 collectedAt; 원 API 감사값 apiQueryTimemsgHeader.queryTime20일 코퍼스 수집기
프로토타입 순수요 평균 (profile μ)같은 운행의 두 비만석 관측 좌석차를 그 사이 정류장 순수요 합으로 놓고 ridge=1 구간합 최소제곱을 푼다. 셀은 끝 관측 collectedAt의 KST 평일/주말×30분대다.주의 — 끝점 어느 쪽이든 0석이면 평균 적합에서 버린다. 현재 새 후보는 이 profile을 입력으로 읽지 않는다.파생코퍼스 vehicles[].id, vehicles[].currentStopSequence, vehicles[].remainingSeats, collectedAt; 프로토타입 ProfileCell.weight, demandSum프로토타입 profile.ts의 deconvolution
프로토타입 순수요 표준편차 (profile σ)평균 적합 뒤 구간 잔차제곱을 다시 구간합 문제로 풀고 sqrt(max(B/w-μ²,1))을 쓴다. 저신뢰면 1을 더한다.주의 — 표본이 없으면 μ=0, σ=3이다. 정류장 충격 사이 공분산은 0으로 둔다.파생코퍼스 vehicles[].id, vehicles[].currentStopSequence, vehicles[].remainingSeats, collectedAt; 프로토타입 ProfileCell.weight, demandSum, demandSquaredSum, censoredWeight프로토타입 profile.ts의 deconvolution
프로토타입 현재·하류 정류장 배열수집기가 노선 정류장 응답을 캐시하고 순번으로 정렬한다. 화면은 현재 차량과 같은 회차 방향의 하류 정류장만 stops로 넘긴다.주의 — S1의 routestation 파일 경로와 다르다. 프로토타입은 getBusRouteStationListv2 캐시를 읽는다.파생msgBody.busRouteStationList[].stationSeq, msgBody.busRouteStationList[].stationName, msgBody.busRouteStationList[].stationId, msgBody.busRouteStationList[].turnSeq, msgBody.busRouteStationList[].turnYn, msgBody.busRouteStationList[].x, msgBody.busRouteStationList[].yRouteCache.stops[].sequence, name, id, directionSequence, isTurnStop, longitude, latitudeVehicleForecastRequest.stops[].sequence, nameGBIS 노선정보 getBusRouteStationListv2 + 프로토타입 수집기 정류장 캐시
프로토타입 예보 기준시각 (forecastAt)seoulBucketAt(forecastAt)으로 현재 KST 셀을 고르고, 하류 horizon마다 예상 이동시간을 더해 다음 셀을 고른다.주의 — GBIS msgHeader.queryTime도 코퍼스 collectedAt도 아니다. 화면 예보는 브라우저 현재시각을 쓴다.응답 원본화면 apps/web/src/app.ts Date.now() 또는 배치 build-data.ts generatedAtVehicleForecastRequest.forecastAt프로토타입 화면·배치 호출기
프로토타입 정류장당 이동시간 상수값 2를 하드코딩하고 estimatedTravelTimeMs(horizon)=horizon*2*60_000으로 계산한다.주의 — 실측으로 적합한 값이 아니다. 재검증의 순번당 추정은 3330 2.176분 [2.163,2.189], 1650 2.668분 [2.650,2.686]이었다.응답 원본estimatedMinutesPerStop프로토타입 packages/domain/src/travel-time.ts
프로토타입 좌석 상태 상한요청에 capacity가 없으면 하드코딩한 80을 쓰고 좌석분포 상태를 0~80으로 자른다.주의 — 실제 차량 정원이 아니라 계산 배열 상한이다. 코퍼스 유효 잔여석 실측 범위는 0~68이다. 새 후보의 정원·점유율 특징으로 옮기지 않는다.응답 원본VehicleForecastRequest.capacity; 기본값 defaultSeatCapacity프로토타입 forecast.ts + profile.ts
노선 (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 통과 사건 빌더
노선×정류장 효과 (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계층 로지스틱 내부 조합
목표 축 순번 (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
요일 (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
계획 배차간격 (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_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_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_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
상류 버스 잔여석 (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_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_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_remain_seats)축거리와 동률 규칙으로 고른 지난 차량의 remainSeatCnt를 붙인다.주의 — -1은 결측이다. 목표 정류장을 지난 뒤의 관측이라도 목표 차량의 정답 라벨과는 다른 스냅샷 차량이다.파생msgBody.busLocationList[].remainSeatCnt, msgBody.busLocationList[].stationSeq, msgBody.busLocationList[].stateCd, msgBody.busLocationList[].vehId; S1 passed_remain_seatsGBIS 버스위치정보 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 스냅샷 특징 빌더
응답 내 노선 차량 수 (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_min)응답 안의 비음수 remainSeatCnt에 최솟값을 적용한다.주의 — remainSeatCnt=-1은 제외한다. 유효 좌석값이 없으면 결측이다.파생msgBody.busLocationList[].remainSeatCnt; S1 route_seat_minGBIS 버스위치정보 getBusLocationListv2 + S1 노선 응답 요약
노선 응답 잔여석 중앙값 (route_seat_median)응답 안의 비음수 remainSeatCntnumpy.quantile 기본 선형 보간의 0.50 분위수를 적용한다.주의 — remainSeatCnt=-1은 제외한다. 유효 좌석값이 없으면 결측이다.파생msgBody.busLocationList[].remainSeatCnt; S1 route_seat_medianGBIS 버스위치정보 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 상류·지난 차 선택
관측 헤드웨이 하한 (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의 연속 통과 구간
회차 전 여부 (is_outbound)station_seq<=turn_seq면 1, 아니면 0이다.주의 — 공식 upDown을 그대로 쓴 값이 아니다. 내부 순번축을 회차점에서 둘로 나눈 표시다.파생route.turnSeq, S1 station_seq; S1 is_outboundGBIS route 마스터 + 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
상류 버스 존재 (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 스냅샷 특징 빌더
이미 지난 버스 존재 (passed_bus_exists)정규화 마지막 통과 순번이 목표 station_seq 이상인 차량이 하나라도 있으면 1이다.주의 — 그 차량이 사용자가 탈 다음 차량이라는 뜻은 아니다.파생msgBody.busLocationList[].vehId, msgBody.busLocationList[].stationSeq, msgBody.busLocationList[].stateCd; S1 정규화 last_passed; S1 passed_bus_existsGBIS 버스위치정보 getBusLocationListv2 + S1 스냅샷 특징 빌더
상류 버스 상태 (upstream_state_cd)선택된 상류 차량의 원 stateCd를 붙인다.주의 — 로지스틱은 0·1·2를 원핫 표시로 바꾼다. 상태 의미와 축 위치 정규화는 별개다.파생msgBody.busLocationList[].stateCd, msgBody.busLocationList[].stationSeq, msgBody.busLocationList[].vehId; S1 upstream_state_cdGBIS 버스위치정보 getBusLocationListv2 + S1 최근접 상류차 선택
지난 버스 상태 (passed_state_cd)선택된 지난 차량의 원 stateCd를 붙인다.주의 — 로지스틱은 0·1·2 원핫 표시로 바꾼다.파생msgBody.busLocationList[].stateCd, msgBody.busLocationList[].stationSeq, msgBody.busLocationList[].vehId; S1 passed_state_cdGBIS 버스위치정보 getBusLocationListv2 + S1 최근접 지난 차 선택
훈련 셀 사전 로그오즈 (train_only_cell_prior_logit)노선×정류장×30분 셀의 만석률을 전역 사전으로 축소해 로그오즈로 바꾼다. 훈련 행은 자기 라벨을 뺀 leave-one-out 집계를 쓴다.주의 — 예측·validation에는 훈련 전체 집계만 적용한다. 원 API 필드가 아니며 현재 행 라벨을 섞지 않는다.파생S1 route, station_id, query_half_hour, label_full; 모델 내부 train_only_cell_prior_logit로지스틱+상호작용 내부 경험적 베이즈 집계
원천 구분 (source_kind)직접 수집 행은 direct_e4, 제한 코퍼스 행은 corpus로 붙인다.주의 — GBIS 응답 필드가 아니다. GBM에는 범주형 예측 재료로 들어가고, 구간 검열에서는 같은 원천끼리 연결하는 데 쓴다.파생S1 source_kindS1 데이터셋 빌더
날짜 순번 (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
같은 차량·운행 연결키같은 원천·노선·구간·가명 차량을 묶고 순번 회귀·긴 공백에서 운행을 나눠 journey_index를 붙인다.주의 — 코퍼스 차량키는 plateNo 우선 HMAC 앞 16 hex이며 외부 원 식별자로 되돌릴 수 없다. 원천을 넘겨 같은 차량으로 보지 않는다.파생msgBody.busLocationList[].vehId, msgBody.busLocationList[].plateNo; 직접 S1 target_vehicle_hash, 코퍼스 vehicles[].id; S1 source, route, segment_id, vehicle_hash, journey_indexGBIS 버스위치정보 getBusLocationListv2 + 수집기 가명화 + S1 운행 분할
미관측 중간 통과 구간인접 적격 응답의 정규화 순번이 +2 이상이면 끝점 사건만 만들고 각 중간 순번을 reason=sequence_jump_intermediate_passage_unobserved로 기록한다.주의 — 선형 보간한 가짜 통과시각을 만들지 않는다. 불가능 이동 사유는 이 모형의 전이쌍에서 제외한다.파생S1 censored_passages.reason, censored_passages.source, censored_passages.route, censored_passages.segment_id, censored_passages.vehicle_hash, censored_passages.interval_start, censored_passages.interval_end, censored_passages.station_seq, censored_passages.normalized_sequence_jumpS1 censored_passages
서비스 정차점 표시stationName(미정차)(경유)가 없으면 1로 둔다.주의 — 현재 S1 빌더는 station.stationName이 아니라 routestation.stationName을 읽는다.파생routestation.stationName; S1 is_service_stopGBIS routestation 마스터 + S1 정류장 필터
검열 구간 양끝 만석 상태검열 구간 끝점의 서비스 정차 통과 라벨과, 같은 차량·운행에서 90초 안에 앞서 확정된 서비스 정차 라벨을 연결한다.주의 — label_status=ok, is_service_stop=1만 쓴다. 같은 원천과 인과 시각 조건을 지킨다.파생S1 censored_passages.source, censored_passages.route, censored_passages.segment_id, censored_passages.vehicle_hash, censored_passages.interval_start, censored_passages.interval_end, censored_passages.station_seq; passage_events.source, passage_events.route, passage_events.segment_id, passage_events.vehicle_hash, passage_events.label_status, passage_events.is_service_stop, passage_events.label_full, passage_events.passage_interval_start, passage_events.passage_interval_end, passage_events.journey_index, passage_events.station_seq; 모델 내부 start_state, end_stateS1 censored_passages + S1 passage_events
전이 횟수와 숨은 통과 수같은 검열 구간의 중간 순번 행 수를 latent_passage_count로 세고 transition_count=latent_passage_count+1로 둔다.주의 — 한 구간의 중간 순번 행을 서로 독립 표본으로 세지 않는다.파생S1 censored_passages.normalized_sequence_jump; 모델 내부 latent_passage_count, transition_countS1 censored_passages + 구간 검열 모형 내부
검열 전이 그룹route | am·pm | b((station_seq-1)//10)으로 묶어 0↔1 전이율을 추정한다. 12:00 전은 am이다.주의 — API의 공식 시간대나 방향 코드가 아니다. 10순번 폭은 구현 규칙이다.파생S1 route, query_minute_of_day, station_seq; 모델 내부 transition_group구간 검열 모형 내부
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
직전 적격 응답 간격 (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 마스터
기점부터 축 순번거리 (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_tie_count)최소 upstream_axis_gap_seq에 놓인 후보 차량 수를 센다.주의 — 응답 전체 차량 수가 아니다.파생msgBody.busLocationList[].vehId, msgBody.busLocationList[].stationSeq, msgBody.busLocationList[].stateCd; S1 정규화 last_passed; S1 upstream_tie_countGBIS 버스위치정보 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 스냅샷 특징 빌더
최근 통과 나이 하한 (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
노선 응답 잔여석 표준편차 (route_seat_std)응답 안의 비음수 remainSeatCnt에 모표준편차(ddof=0)을 적용한다.주의 — remainSeatCnt=-1은 제외한다. 유효 좌석값이 없으면 결측이다.파생msgBody.busLocationList[].remainSeatCnt; S1 route_seat_stdGBIS 버스위치정보 getBusLocationListv2 + S1 노선 응답 요약
노선 응답 잔여석 10백분위 (route_seat_p10)응답 안의 비음수 remainSeatCntnumpy.quantile 기본 선형 보간의 0.10 분위수를 적용한다.주의 — remainSeatCnt=-1은 제외한다. 유효 좌석값이 없으면 결측이다.파생msgBody.busLocationList[].remainSeatCnt; S1 route_seat_p10GBIS 버스위치정보 getBusLocationListv2 + S1 노선 응답 요약
노선 응답 잔여석 90백분위 (route_seat_p90)응답 안의 비음수 remainSeatCntnumpy.quantile 기본 선형 보간의 0.90 분위수를 적용한다.주의 — remainSeatCnt=-1은 제외한다. 유효 좌석값이 없으면 결측이다.파생msgBody.busLocationList[].remainSeatCnt; S1 route_seat_p90GBIS 버스위치정보 getBusLocationListv2 + S1 노선 응답 요약
기본 모델 원확률 (p_full)기본 후보가 낸 보정 전 만석확률을 그대로 보정기의 한 개 입력값으로 넣는다.주의 — 직접 API 필드가 아니다. 학습용 값은 시간순 OOF 예측이고 서빙용 값은 최종 적합 모델의 새 예측이다.파생후보 출력 p_full; 보정기 입력 p_full선택된 기본 후보의 predict() 출력
훈련 만석률과 클래스 가중치전체 train의 label_full 평균을 목표 만석률로 두고, OOF 보정 구간의 가중 만석률이 그 값과 같아지도록 양성·음성 표본을 재가중한다.주의 — validation 라벨은 이 계산에 넣지 않는다.파생S1 label_full; candidate_lib.fit_calibrators(target_prevalence); 모델 내부 calibration_prevalence, positive_weight, negative_weight, sample_weight보정 후처리 내부

지금은 안 쓰는 재료 4개

언제 실패하나

기존 프로토타입과 다른 점