1차 운영 선택 · 2026-08-19
오고 있는 버스 보기
차량 상태 조건부 좌석 분포 · seat_distribution · A18
한 줄 결론 — 지금 오고 있는 그 버스가 목표 정류장에 닿을 때 자리가 몇 석 남아 있을지를 확률로 답한다. 만석 확률은 그중 0석일 확률을 그대로 쓴 것이다. 다른 후보들이 쓰지 않는 실시간 잔여석을 쓰기 때문에, 잘하고 못하고를 떠나 쓰임 자체가 다르다.
2026-08-19 팀 검토에서 1차 운영 모델로 정했다. 그 전(08-17~08-19)에는 02번 「재료 다 넣기」가 그 자리였다. 02번을 버리는 것은 아니다 — 서빙에서 빠지고 평가 규약에는 기록으로 남는다. 같은 검토에서 02번을 포함한 기존 일곱의 일일 채점을 2026-08-19 평가일까지로 끊었다. 지난 점수는 지우지 않는다. "1차 운영"은 팀이 운영에 올려 보기로 정했다는 뜻이지 성능을 입증했다는 뜻이 아니다. 평가는 아직 잠정이다. 현재 LIVE 서빙은 v4 클라이언트 계약의 fail-closed 규칙으로 제공한다.
이 모델이 실제로 맞히는 것을 보려면 — 노선 지도 관전 화면에서 예보가 발행되고 도착 뒤 채점되는 것을 그대로 본다 →기존 일곱 후보는 정류장과 시간대로 예측한다. "이 정류장은 화요일 아침에 만석일 확률" 같은 형태다. 버스를 특정하지 않으므로 언제든 답할 수 있다.
A18 은 지금 노선 순번으로 몇 칸 앞에 와 있는 그 버스의 잔여석까지 쓴다. 이 칸은 raw 노선 대장의 순번 차이라 경유(미정차) 지점도 하나로 센다. 그래서 답이 더 정확한 대신, 어느 버스인지 알고 그 버스를 계속 따라가고 있어야만 답할 수 있다. 더 나은 모델이라기보다 다른 질문에 답하는 모델이니, 점수를 나란히 볼 때는 이 조건 차이를 같이 봐야 한다.
이 페이지에만 쓰는 말이 여럿 있다 — 지평 · 셀 · 칸 · 코호트 · 기후값 같은 것들이다. 뜻이 헷갈리면 중간의 「이 페이지가 쓰는 말」 표를 보면 된다.
이 모델이 나온 배경
앞서 만든 후보 하나가 아침 러시에서 현행 기본(A18 을 올리기 전까지 운영 자리에 있던 모델)보다 6~7% 나빴다 — CRPS 기준이고, 이 페이지에서 "몇 % 낫다/나쁘다"는 전부 CRPS 로 잰 값이다. 그래서 못 썼다. 왜 그랬는지 파 보니 세 가지가 나왔는데, 셋 다 모델이 아니라 모델을 판정하는 방식의 문제였다.
- 점수를 하나로 합치면 반대 방향이 서로 지워진다. 오전에 나빠지고 오후에 좋아지면 합계는 멀쩡해 보인다. 휴일과 평일을 섞어도 마찬가지다. 아침이면 아침, 평일이면 평일로 갈라서 봐야 제대로 보인다. 이 페이지 뒤쪽의 12정류장 종합 점수가 바로 그 함정에 걸린 사례다.
- 결과를 보기 전에 규칙을 정해 두는 것만으로는 부족하다 — 표본이 적으면 그 규칙도 못 믿는다. 어떤 재료를 남길지 미리 기준을 정해 놓고 적용했는데, 하루치 자료로 재니 여섯 개가 남고 여러 날 자료로 재니 하나만 남았다. 자료는 같은데 며칠분을 보느냐에 따라 답이 뒤집힌 것이다.
- 이론적으로 맞는 제약이라고 성능이 오르지는 않는다. 좌석이 0석과 정원 사이를 벗어날 수 없다는 것은 당연한 사실이다. 그런데 모델이 실제로 그 바깥에 얹어 놓는 확률을 확인해 보니 정확히 0이었다. 잘라 낼 것이 없으니 그 재료는 그냥 뺐다.
A18 은 그 셋을 고쳐서 나온 모델이다. 앞선 후보가 무너졌던 바로 그 아침 러시에서 현행 기본보다 21~23% 낫다.
Q. 이 모델은 무엇을 계산하나?
버스가 목표 정류장에 도착할 때 잔여좌석이 0석일 확률, 1석일 확률, 2석일 확률…을 전부 낸다. 하나의 숫자가 아니라 분포다.
만석 경보는 그중 0석일 확률 하나를 꺼내 쓴 것이다. 그래서 "만석이다/아니다"만 답하는 모델과 달리
"만석은 아닌데 두 자리밖에 안 남을 것 같다" 같은 말도 할 수 있다. 서비스는 빈자리 확률과 expectedSeats를 함께 보여 줄 수 있다.
종합 점수는 확률 지표만 합치지만, evaluation-v5부터 사설 지평 원장은 좌석 분포와 점추정도 CRPS·MAE·RMSE·±3석·편향으로 따로 채점한다.
Q. 왜 분포로 예측하나?
만석은 현재 통과 사건의 1.21%뿐이다. 이 표적만 따로 떼어 배우려 들면 배울 거리가 모자란다. 좌석 수 전체를 배우면 나머지 98.79%도 버리지 않고 쓸 수 있고, 만석은 그렇게 배운 분포의 끝자락에서 저절로 나온다.
8/23 완료일까지 격리 검사를 통과한 말뭉치는 통과 사건 114,945건이고 그중 만석 라벨이 1,394건이다. 지평 1~12로 펼친 채점행은 1,209,921건, 만석 라벨은 15,373건이다.
다만 0석은 다른 숫자와 성질이 다르다. 0석과 1석의 차이는 1석과 2석의 차이와 같지 않다 — 1석이면 탈 수 있고 0석이면 못 탄다. 그래서 만석 확률만큼은 분포 끝에서 가져다 쓰지 않고 "만석이었다/아니었다"라는 정답에서 곧바로 배운다. 이렇게 0석과 나머지를 따로 배워서 붙이는 방식을 허들 모형이라고 부른다.
Q. 원리가 무엇인가?
먼저 지평이라는 말부터 정하자. 이 페이지에서 지평은 목표 순번 − 차량 순번인 raw 노선 대장 순번 차이다. 지평 8이면 노선 순번으로 여덟 칸 앞에 있는 버스를 두고 "이 버스가 여기 올 때 자리가 몇 개 남을까"를 묻는 것이다. 시간이나 승차 정류장 개수가 아니며, 중간의 경유(미정차) 지점도 한 칸으로 센다. 앞으로 나오는 표의 "지평" 열은 전부 이 뜻이다.
이 모델의 구조는 지평을 바꿔 가며 재 보다가 알게 된 사실 하나에서 나왔다 — 멀리서 물을수록 답의 근거가 달라진다.
가장 단순한 방법은 도착했을 때의 좌석 수를 곧바로 맞히는 것이다. 그런데 그 값은 들쭉날쭉해서 맞히기 어렵다. 대신 지금 좌석에서 몇 석이나 변할지를 맞히면 훨씬 쉽다. 2분 뒤를 물을 때 변화량이 흔들리는 폭은 좌석 수 자체가 흔들리는 폭의 3.1%밖에 안 된다. 이미 알고 있는 값을 버리지 않는 쪽이 유리하다는 뜻이다.
그런데 멀리 가면 이 방법이 무너진다. 평일 아침에 8정류장을 지나는 동안 좌석이 평균 13.8석 움직인다. 지금 좌석이 1석 많을 때 도착 좌석이 몇 석 많아지는지를 재 보면 0.434석이다 — 지금 좌석의 영향이 절반도 남지 않는다.
그 변화를 만드는 것은 정류장이다. 어느 정류장에서 몇 명이 타느냐가 좌석을 줄인다. 그래서 정류장의 과거 이력을 같이 쓴다. 위 그림에서 8정류장 지평의 설명력이 차량만으로 0.179 인데 정류장을 더하면 0.442 로 오르는 것이 정류장이 채워 준 몫이다.
Q. 어떻게 계산하나?
큰 줄기만 먼저 본다. 실제 코드가 도는 순서는 조금 다르고, 그건 맨 아래 부록에 다섯 단계로 따로 적었다.
큰 줄기 네 토막
지금 좌석에서 출발
지평에 따라 신뢰도 조절
변할까
통과할 정류장들의
평소 수요를 합산
제약
좌석은 0~정원을
벗어날 수 없다
따로
정답에서 곧바로 배운
만석 확률을 합침
②의 "통과할 정류장들"이 핵심이다. 8개 정류장을 지난다면 그 8개 각각의 평소 수요를 더한다. 도착 정류장 하나만 보면 나머지 일곱을 무시하는 셈이다.
이 모델이 도는 두 노선
아래에 나오는 지평·커버율·회차 이야기는 모두 이 두 노선이 어떻게 생겼는지에 기대고 있다.
회차는 버스가 종점을 찍고 되돌아 나오는 지점이고,
커버율은 정류장에 서 있을 때 지평 안에 실제로 보여 줄 버스가 있던 비율이다(둘 다 아래에서 자세히 다룬다).
아래 값은 상류 기준정보 API
(getBusRouteInfoItemv2 · getBusRouteStationListv2)를 직접 불러 확인했다.
| 항목 | 1650 | 3330 |
|---|---|---|
routeId | 234000050 | 204000057 |
| 정류장 수 | 89 | 85 |
turnSeq (회차 순번) | 44 | 43 |
| 회차 정류장 | 안양역 · stationId 208000069 — 두 노선 공통 | |
| 기점 (순번 1) | 구리수택차고지 221000046 | 도촌동9단지앞 205000227 |
| 종점 (마지막 순번) | 구리수택차고지 221000047 | 도촌동9단지앞 205000226 |
| 첫차 / 막차 (상행) | 04:10 / 22:35 | 04:50 / 23:30 |
| 첫차 / 막차 (하행) | 05:25 / 23:55 | 05:00 / 23:30 |
| 배차 (첨두 / 비첨두) | 6분 / 15분 | 5분 / 15분 |
| 운수사 | 경기여객 | 대원버스 |
| 노선 종류 | 직행좌석형시내버스 — 입석이 없다 | |
stationId 가 다르고, 두 노선 모두 stationId 중복이 0건이라
정류장은 ID 하나로 유일하게 지목된다.Q. 정류장의 무엇을 기억하나?
(노선, 정류장, 시간대) 조합마다 두 값을 기억한다. 이 조합 하나를 아래에서 셀이라고 부른다.
- 평균 점유율 — 그 정류장에 도착할 때 평균 몇 % 차 있었는지
- 순수요 — 그 정류장을 지나는 동안 좌석이 평균 몇 % 줄어드는지. 탄 사람에서 내린 사람을 뺀 값이라 "순"이고, 내린 쪽이 많으면 음수가 된다
일부러 기억하지 않는 것도 있다. 과거 만석률은 쓰지 않는다. 셀의 87%는 만석을 한 번도 겪지 않아서, 그 셀의 만석률은 사실상 "그 셀을 몇 번 들여다봤나"만 말해 주기 때문이다(만석 0건 셀에서 상관계수 1.000). 그리고 관측 횟수는 승객 수요가 아니라 우리가 얼마나 자주 수집했나를 반영한다 — 수집 호출은 닷새 사이 8.5배로 늘었다.
평균 점유율과 순수요에는 이런 오염이 없다. 휴일에서 평일로 넘어갈 때도 훨씬 잘 버틴다 — 정류장을 값 순서로 줄 세운 다음 휴일 등수와 평일 등수가 얼마나 같은지 재 보면(순위상관, 1이면 완전히 같은 순서) 순수요 0.920, 점유율 0.805인데 만석률은 0.302다.
Q. 노선을 합쳐서 배우나?
아니다. 노선마다 따로 배운다. 두 노선이 계수를 나눠 쓰지 않는다.
만석 판정은 잔여석이 정확히 0이냐를 가르는 문제인데, 어느 정류장에서 얼마나 빨리 차는지가 노선마다 다르다. 두 노선을 합쳐 배우면 그 차이만큼 한쪽으로 치우친 답이 나오고, 이 치우침은 자료를 아무리 모아도 줄지 않는다. 반대로 따로 배울 때 생기는 흔들림은 자료가 쌓이면 줄어든다. 그래서 결국 노선별이 이긴다.
실제로도 그랬다. 3330 에서 합쳐 배운 쪽과의 격차가 학습 2일치 +0.190 에서 4일치 +0.029 로 6.6배 줄었다. 그리고 노선별로 배운 A18 은 현행 기본 모델보다 36칸 전부에서 CRPS 가 낮다. 여기서 칸은 채점을 잘게 쪼개는 단위(노선 2 × 지평 6 × 시간대 3 = 36칸)이고, CRPS 는 좌석 분포 예측의 오차를 좌석 수 단위로 잰 값이라 낮을수록 좋다. 36칸 모두, 자료를 여러 번 다시 뽑아 채점해도 격차가 0 아래로 내려가지 않는다 — 바꿔서 손해 보는 칸이 없다는 뜻이다.
재료는 어떻게 들어오나
출처는 실시간 차량 위치 API 하나다. 이 API 가 한 번에 주는 것은
vehId · stationSeq · remainSeatCnt · lowPlate ·
crowded · stateCd 뿐이고, 모델이 쓰는 재료는 전부 이 여섯 개를 가공해 만든 것이다.
수집 주기는 고정이 아니라 시간대마다 다르다
"15초마다 읽는다"는 정확한 말이 아니다. 수집기가 시간대에 따라 주기를 바꾼다
(adaptive-kst-v1.0.1). 만석이 몰리는 시간에 호출을 몰아 쓰고, 심야에는 성기게 두되 끊지는 않는다.
| 구획 | 시각 (KST) | 관측 간격 | 이유 |
|---|---|---|---|
| 첨두 | 07–09 · 17–20 | 15초 (1분 안에 4연발) | 정류장을 지나는 순간을 놓치면 정답을 만들 수 없다 |
| 늦은 저녁 | 20–23 | 15초 (1분 안에 4연발) | 만석 라벨의 절반이 이 구간에서 나온다 |
| 낮 · 새벽 · 자정 | 09–17 · 04–07 · 23–01 | 20초 (1분 안에 3연발) | 만석이 드물어 조금 아끼되 끊지는 않는다 |
| 심야 | 01–04 | 600초 | 운행 공백. 차량 0대 응답 자체가 검증에 필요하다 |
하루 예상 호출은 두 노선 합 8,556회이고, 어떤 경우에도 10,000회를 넘기지 않는다. 그래서 모아 둔 관측 자료의 밀도가 고르지 않다 — 같은 "8정류장 전"이라도 첨두에는 촘촘히 찍히고 주간에는 성기게 찍혀서, 조밀한 구간에서 6,144건이 나올 때 한산한 구간에서는 2,058건밖에 안 나온다. 게다가 수집 주기 자체가 학습 창 중간(08-18)에 촘촘해져서 낮 구간이 앞쪽 날짜는 60초, 뒤쪽은 20초다. 이 페이지의 점수는 그렇게 섞인 자료 위에서 나온 값이다. 성긴 구간만 떼어 다시 채점해 본 적은 없다.
상류 응답의 queryTime 은 1,294건 전부에서 우리 수신 시각보다
2.2~20.5초 뒤였다. 시계가 어긋나 있어 그대로 쓸 수 없다.
그래서 observedAt 은 우리 수집기가 상류 응답을 받은 시각으로 정의한다.
모델에 들어가는 31개 값이 어디서 오나
모델은 예측 한 건마다 숫자 31개를 입력으로 받는다. 이 31개 묶음을 설계행렬이라고 부른다. 출처는 차량에서 오는 것 · 정류장에서 오는 것 · 위치와 시간, 셋으로 나뉜다. 아래 세 표는 성격별로 묶은 요약이고, 31개 하나하나의 정의는 맨 아래 「부록 — 단계별 계산식」의 0단계에 있다.
차량에서 오는 것 — 예측 시점의 상태
| 현재 잔여석 ÷ 정원 | 지금 얼마나 비어 있나. remainSeatCnt ÷ 정원. 정원은 아래 「정원」 줄의 규칙으로 구한다 |
| 현재 만석 여부 | 이미 0석인가 |
| 낮은 좌석 구간 | 20석 이하를 더 촘촘히 본다 |
| 혼잡도 등급 | crowded — 상류가 0·1·2·3 네 등급으로 주는 값. 잔여석을 네 칸으로 뭉뚱그린 것이라 잔여석 위에 새로 얹어 주는 정보는 거의 없다 |
| 정원 | 그 차량이 보여 준 최대 잔여석 그대로.
remainSeatCnt 의 차량별 최대
전에는 lowPlate 값을 보고 45·40·68 석 중 하나를 붙였는데, 이 판정이 틀리는 차량이 있었다 —
lowPlate=0 인데 6,334번 관측하는 동안 40석을 한 번도 넘지 않은 차량이 셋, 49석에서 딱 멈추는 차량이 하나.
차량이 직접 보여 준 최대치를 쓰면 이런 판정을 할 일 자체가 없으니 틀릴 일도 없다.
이 규칙으로 가르면 정원별 표본이 크게 벌어진다. 이전 페이지의 정원별 행 수는 옛 분모에서 센 값이라 제거했다.
정원은 관측된 최대 잔여석으로 추정하므로 관측이 덜 쌓인 차량을 실제 정원처럼 비교하지 않는다.
잔여석이 정원보다 크다는 이유로 버려지는 행은 없다.
제작사 인증 좌석수는 이것과 다른 값이라 섞지 않는다. 전에 쓰던 47 은 어디에도 근거가 없던 숫자였다. |
| 상류 좌석 기울기 | 앞선 정류장들에서 좌석이 얼마나 빠르게 줄고 있었나 |
정류장에서 오는 것 — 그 지점의 이력
| 직전 도착 차량 | 그 차의 잔여석과 만석 여부 |
| 직전 연속 만석 | 최근 몇 대가 연속으로 만석이었나 |
| 셀 평균 점유율 | 같은 노선·시간대 안에서 평균이 0 이 되게 맞춘 값. 학습일 자료만 쓴다 |
| 셀 순수요 구간합 | 지나갈 정류장 전체의 합. 대기 인원 정보가 들어올 자리 |
| 셀 결측 지시자 | 이 정류장 이력이 없어 이웃에서 빌려 왔을 때 켜진다 |
위치와 시간
| 정류장 상대위치 | 정류장 순번을 노선 안에서 0~1 로 편 값. 그 값 하나를 매끄러운 곡선 여덟 조각으로 나눠 넣는다(정류장 번호를 등수가 아니라 "노선의 어디쯤"으로 읽게 하려는 것). 정답을 전혀 보지 않고 순번만으로 만든다 |
| 시간대 | 아침 07–09 · 저녁 17–20 · 그 외 |
| 지평 | 지평마다 모델을 따로 만든다. 1~12 전부와 16 까지 만들어 채점했고, 실제로 내보내는 것은 12까지다. 지평은 입력값이 아니라 모델을 가르는 기준이다 |
지금은 쓰지 않는 것
실제 걸린 시간은 버스가 도착한 뒤에야 알 수 있다. 이것을 입력으로 쓰면 미래를 미리 훔쳐보는 셈이라 못 쓴다. 배차 간격은 넣어 봤는데 오히려 나빠졌다 — 직전 도착 차량 정보와 겹치고, 3330 의 8정류장 아침에서 0.41석 손해였다. 도착 예정 API 는 아예 수집하지 않는다. 그 값을 완벽하게 안다고 가정해도 얻는 이득이 0.045석뿐이라 수집할 이유가 없었다. 그래서 이 모델도 API 도 "몇 분 후 도착"을 낼 수 없다 — v3 도 주지 않았고 v4 도 주지 않는다.
Q. 무엇을 못 보나?
센서가 잔여좌석 하나뿐이라는 사실에서 오는 한계가 있다. 두 노선은 직행좌석형이라 입석이 없다 — 이것은 확정 사실이다. 그래서 잔여석 0은 곧 승차 불가이고, 좌석이 줄어든 만큼이 실제로 탄 인원(내린 사람을 뺀 값)이라는 계산도 정확하다. 문제는 다른 데 있다.
| 못 보는 것 | 왜 |
|---|---|
| 도착 예정 시각 | 도착정보 API 를 수집하지 않는다. 지평은 경유 지점까지 포함한 raw 노선 순번 차이로만 준다 |
| 정류장 대기 인원 | 어떤 경로로도 들어오지 않는다. 만석이 새로 생기는 순간을 결정하는 값이다 |
| 만석일 때의 초과 수요 | 좌석이 0이면 관측값도 계속 0이다. 센서가 포화된다 |
| 승차와 하차의 분리 | 순변화만 보인다. 앞문 승차와 뒷문 하차가 동시에 일어난다 |
| 경유(미정차) 지점의 승차 | 승객이 탈 수 없는 지점이라 잴 것 자체가 없다.
예측 대상에서 뺀다 — stationId 가 277 로 시작하면 경유다 |
| 실제 정원의 근거 | 차량마다 그 차가 실제로 보여 준 최대 잔여석이다. 연구 자료와 운영 평가기가 같은 규칙을 쓴다(2026-08-19 양쪽 교정). 관측이 적어 아직 최대치를 못 보여 준 차량은 정원이 실제보다 작게 잡히고, 그러면 그 차량의 점유율이 실제보다 높게 나온다 — 이 수집 자료에서 가장 낮은 값이 36석(관측 138행)이다 |
좌석이 0이면 몇 명이 더 타려 했는지 관측값은 계속 0이다. 연속 만석 구간이 중앙값 6개, 최대 12개 정류장이고, 그동안 못 탄 사람은 전부 정류장에 남는다. 그 수를 셀 방법이 없다.
혼잡도 값으로 메워지지도 않는다 — 만석 구간 169개 중 값이 변한 것은 1개(0.6%)뿐이다.
탄 사람과 내린 사람을 따로 세는 것도 불가능하다. 15초마다 읽는 첨두 시간에는 버스가 정류장에 서 있는 동안 좌석을 세 번 이상 확인하는 경우가 63.1%나 된다. 그런데도 "내리고 나서 탔다"는 뜻인, 좌석이 늘었다가 다시 줄어드는 모양은 0.55%뿐이다. 앞문 승차와 뒷문 하차가 동시에 일어나기 때문이다. 한 구간의 좌석 변화 가운데 57.1%가 0인데, 이것이 "아무도 안 탔다"인지 "3명 타고 3명 내렸다"인지 가릴 방법이 없다.
이 수치는 첨두의 촘촘한 관측에서 나온 것이다. 60초로 벌어지는 주간 시간대에는 같은 보증이 없다 — 정류장에 선 것을 한 번밖에 못 보고 지나가는 경우가 늘고, 그만큼 정답 자체가 뭉개진다.
그래서 못 보는 것을 어떻게 메우나
| 못 보는 것 | 무엇으로 메우나 | 성격 | 얼마나 되나 |
|---|---|---|---|
| 도착 예정 시각 | 축을 시간이 아니라 raw 노선 순번 차이로 잡고, 지평마다 모델을 따로 적합한다 | 회피 | 완벽히 안다고 가정해도 이득 0.045석 |
| 정류장 대기 인원 | 셀 이력 occ_z·board_z 와 통과 구간의 순수요 합 L_segsum,
이력이 없으면 반경 4 이웃 폴백(miss 로 표시) |
대체 | 따로 재지 않았다 |
| 만석일 때의 초과 수요 | 연속 만석 대수·직전 차량 잔여석 세 열, 만석 확률을 따로 세우는 허들 구조,
당일 절편 보정 L_p0shift |
추정 | 보정 폭은 따로 재지 않았다. 남는 만석 구간이 중앙값 6·최대 12 정류장이고, 혼잡도로 메워지는 것은 169개 중 1개(0.6%)다 |
| 승차와 하차의 분리 | 쪼개지 않고 순변화만 모델링한다. 예측 대상이 도착 시 잔여석이라 음수 순수요도 정상값이다 | 재정의 | 쪼갤 수 없는 0 변화가 57.1%, 되돌아오는 모양이 0.55% |
| 실제 정원의 근거 | 정원을 열로 넣고(cap), 좌석 구간을 정원 대비 비율로 자르고,
가능영역 0 ≤ Y ≤ 정원 으로 사영한다(scatter_support) |
부분 완화 | 정원보다 큰 잔여석이라는 이유로 버려지는 행이 없다. 아직 최대치를 못 보여 준 차량의 오차는 따로 재지 않았다 |
도착 예정 시각·대기 인원·승차와 하차 셋은 축을 바꾸거나 관측 가능한 양으로 질문을 바꿔 제대로 다룬다. 초과 수요는 대리 신호로 추정하는 데 그치고, 정원은 부분적으로만 완화된다. 장치 하나하나가 점수를 얼마나 올렸는지는 따로 재지 않았고, 정확도로 말할 수 있는 것은 아래 「오늘 성적」·「지평 1~12 전부의 성적」에 실린 값이 전부다.
회차를 어떻게 다루나
두 노선은 왕복이다. 순번 1~85(3330)·1~89(1650)가 하나의 연속 경로이고
중간의 안양역이 회차 지점이다 — 상류 노선 기준정보의 turnSeq 로
1650은 44, 3330은 43이다. 상류 API 를 직접 불러 확인한 값이다.
v3 초판 예시의 turnSequence: 31 과 3330 의 routeId: 234000016 은 실제와 다른 값이라
2026-08-19 에 43·204000057 로 교정했다.
회차 너머의 버스도 예보한다
기록에서 운행 한 건을 복원해 보면 버스 한 대가 지나는 정류장이 중앙값으로 89개 중 86개(1650)·85개 중 81개(3330)다. 회차 지점에서 끊기지 않고 그대로 이어 간 운행이 94.0%·72.4%이고, 회차 앞뒤 관측 간격은 중앙값 0.3분이다 — 차고지에 서지 않고 그냥 지나간다는 뜻이다. 회차는 운행의 끝이 아니다. 그러니 회차 너머 정류장에도 그 버스는 분명히 오고 있고, 거기에 "도착 예정 버스 없음"이라고 쓰면 거짓말이 된다. 실제로 API 가 내보내는 도착 항목 가운데 10.0%(1650)·12.8%(3330)가 회차를 넘어서 오는 버스다.
다만 근거가 바뀐다
회차를 넘는 순간 차량의 현재 잔여석이 설명력을 잃는다. 아래 표는 지금 잔여석만으로 도착 잔여석을 얼마나 설명할 수 있는지를 R² 로 잰 것이다.
| 지평 | 회차 안 넘음 (R²) | 회차 넘음 (R²) |
|---|---|---|
| 2정류장 | 0.873 | 0.370 |
| 4정류장 | 0.645 | 0.034 |
| 8정류장 | 0.178 | 0.011 (상관이 음수 −0.105 로 뒤집힌다) |
회차를 넘는 행은 드물지만 만석이 0건인 구간은 아니다. 아래는 8/20까지의 5일 설계 진단 표본에서 지평 8은 5,247건 중 3건, 지평 12는 8,091건 중 72건이 만석이다. 회차 여부를 입력으로 쓰지 않으므로 이 구간은 별도 표본으로 계속 감시한다.
| 지평 | 구분 | 표본 | 만석 | 실제 만석률 |
|---|---|---|---|---|
| 8 | 회차 안 넘음 | 43,839 | 655 | 1.494% |
| 8 | 회차 넘음 | 5,247 | 3 | 0.057% |
| 12 | 회차 안 넘음 | 38,763 | 577 | 1.488% |
| 12 | 회차 넘음 | 8,091 | 72 | 0.890% |
회차 너머 예보를 만들어 내는 것은 차량 상태가 아니라 정류장 통계다. 답은 맞지만 근거가 다르다.
멀리 볼수록 회차를 넘는 표본 비중이 커진다. 지평 8은 10.69%, 지평 12는 17.27%다. 이 표는 표본 구성을 보여 주는 원장이고, 코호트 성능값은 같은 v6 산출물에서 별도로 계산해 읽는다.
위 회차 표는 8/20까지의 역사 진단이다 — 표본 합은 49,086(8정류장) · 46,854(12정류장)다. 8/23 최신 지평 표와 날짜 창이 다르므로 직접 맞대어 읽지 않는다. 이 절의 R² 표도 별개 산출이고 이번 최신화에서 다시 재지 않았다.
모델은 회차점을 입력으로 받지 않는다
turnSeq 는 모델의 입력이 아니다. 31개 값 어디에도 "회차를 넘는가"라는 항목이 없다.
그런데도 위 표처럼 맞는 이유는, 정류장 이력 재료가 (노선, 정류장, 시간대)별 통계를 갖고 있어서
앞 그림의 공백 구간을 이미 알고 있기 때문이다. 회차라는 개념을 따로 알려 주지 않아도
그 정류장들에는 낮은 만석 확률을 낸다.
turnSeq 를 쓰는 곳은 두 군데뿐이다 — 순번을 UP/DOWN 으로 가르는 것,
그리고 화면에 "회차 후 도착"을 띄울지 정하는 것. 뒤엣것은 API 가 주는 값이 아니라
클라이언트가 sequence − horizonStops 를 turnSequence 와 견줘 직접 계산한다.
이 값이 틀려도 확률은 틀리지 않는다. 틀리는 것은 방향 표시와 회차 표시뿐이다.
Q. 노선을 따라가면 무엇이 보이나?
Q. 지표를 왜 넷이나 보나?
만석이 표본의 1.04%뿐이라 분포 정확도 지표 하나로는 만석 경보의 품질을 못 잰다. 나머지 98.96%가 지표를 거의 다 차지해 버리기 때문이다.
이번 설계에서 분포 쪽 지표와 만석 쪽 지표가 정반대를 가리킨 재료가 세 개 나왔다. 좌석 분포를 맞히는 데는 쓸모가 없는데 만석 확률에는 가장 크게 기여한 재료가 있었고, 그 반대도 있었다. 그래서 네 지표를 함께 보기로 했고, 어느 하나만으로 채택 여부를 정하지 않는다는 규칙을 세웠다.
| 지표 | 재는 것 | 좋은 방향 |
|---|---|---|
| 브라이어 | 확률 예측이 얼마나 정확한가 | 낮게 |
| 로그 손실 | 틀린 것을 얼마나 확신했나 | 낮게 |
| 보정 | "30% 확률"이라 한 것 중 실제로 30%가 일어났나 | 0에 가깝게 |
| 만석 구분 | 만석인 것을 위쪽에 잘 세우나 | 높게 |
이 점수는 어디서 나왔나
여기 실린 값은 evaluation-v6 · composite-score-v2로 지평 1~12를
확장창 롤링 오리진 채점한 사설 산출물에서 왔다. 앞선 날짜로만 배우고 뒤 날짜만 채점하며,
확률 맞춤은 시간대별 보정 오차 calibration_stratified 하나로 낸다.
A18은 상류 관측이 붙은 행만 채점해 공개 일곱 모델과 표본이 다르므로 같은 표에서 등수를 매기지 않는다.
지평 원장은 매일 갱신되지만 private_internal_only라 공개 일일 평가 API에는 싣지 않는다.
이 페이지가 쓰는 말
| 말 | 뜻 |
|---|---|
| 칸 | 채점을 나누는 단위. 노선 2 × 지평 6 × 시간대 3 = 36칸이 기본이고, 분포 정확도는 노선 2 × 지평 6 × 지표 2 = 24칸, 아침 구간만 보면 노선 2 × 지평 6 = 12칸이다. "96 판정"은 이 칸들에 검정 종류를 곱해 나온 전체 검정 횟수다 |
| 재료 묶음 | 모델에 얹었다 뺐다 할 수 있는 재료 덩어리 하나. A18 에는 여섯 개가 얹혀 있다 — 정류장 이력 · 노선 안에서의 위치 · 지나갈 구간의 수요 합 · 그날 관측으로 만석 확률 보정 · 지평에 따른 기준점 조절 · 정원 대비 비율로 나눈 구간. 위 「어떻게 계산하나」의 네 단계는 계산 순서라서 이것과 세는 기준이 다르다 |
| 밴드 | 시간대 구획. 아침 07–09 · 저녁 17–20 · 그 외 |
| CRPS | 분포 예측의 오차. 만석 여부만 보는 브라이어와 달리 좌석 수 분포 전체를 채점하며 단위가 좌석(석)이다. 낮을수록 좋다 |
| 판별력 | 만석과 비만석을 가르는 능력. 값이 0.5 부근이면 무작위다. 이 페이지의 「만석 구분」(PR-AUC)과는 다른 지표(ROC-AUC)이므로 값의 크기를 견주지 마라 |
| 현행 기본 | A18 을 올리기 전까지 운영 자리에 있던 상호작용 로지스틱 회귀. 지금은 비교 기준선으로 쓴다. 차량 상태를 쓰지 않는다. 이 페이지에서 "현행 기본보다 N% 낫다"는 CRPS 기준이다 |
| j12 | 지평 12정류장판 A18. 평가기에 seat_distribution_j12 로 등록돼 있다 |
오늘 성적
아래 숫자는 노선 2개 · 2026-08-14~23 자료, 평가일 9일을 evaluation-v6·composite-score-v2로 다시 채점한 값이다.
확률 맞춤 눈금이 바뀌었으므로 이전 페이지에 적힌 v1 점수와 이어서 비교하지 않는다.
지평이 멀수록, 즉 더 앞에서 물을수록 어렵다. 아래 카드의 큰 숫자는 종합 점수(0~100)이고, 그 밑에 원래 지표값을 같이 적었다.
서빙 상한은 12정류장으로 확정했다. 화면에는 12정류장 앞까지의 버스를 담는다. 다만 이 페이지의 상세 수치와 시간대별 분해는 8정류장을 보고 기준으로 삼는다 — 앞선 설계 판정이 전부 8에서 이뤄졌고 다른 후보와 비교 가능한 축이 8이기 때문이다. 두 숫자는 역할이 다르므로 섞어 읽으면 안 된다.
88.5
브라이어 0.00399
만석 구분 0.856
82.7
브라이어 0.00577
만석 구분 0.727
75.6
브라이어 0.00769
만석 구분 0.575
보고 기준
70.0
브라이어 0.00916
만석 구분 0.468
서빙 상한 · 채택
원장은 「지평 1~12 전부의 성적」 표이고, /live 와 모델 현황판이 그 표를 그대로 옮긴다.
종합 점수는 0~100 이다. 50점이 기준선이고, 그 기준은 채점하는 날짜보다 이른 학습일의 만석률만 그대로 내미는 예측(기후값)이다. 100점은 완벽, 기준보다 손실이 두 배면 0점이다. 채점에 쓴 표본은 이 네 지평에서 9.32~10.78만 건, 그중 만석이 1,226~1,344건이다 (지평 2는 107,847건 중 1,344건, 지평 12는 93,183건 중 1,226건). 열두 지평 전체는 아래 표에 있다. 평가일은 9일이라 여전히 잠정이며 날짜에 따라 흔들릴 수 있다.
Q. 8정류장과 12정류장, 종합 점수만 보면 되나?
안 된다. 종합 점수는 다섯 항목의 가중 평균인데, 8에서 12로 가면 다섯 항목이 모두 떨어진다. 종합이 75.6에서 70.0으로 내려가는 것도 그 다섯을 합친 결과일 뿐, 어느 항목이 어느 쪽으로 움직였는지는 합쳐 놓은 숫자만 봐서는 알 수 없다.
| 구성 항목 | 가중 | 8정류장 | 12정류장 | 변화 |
|---|---|---|---|---|
| 확률 맞춤 | 20 | 82.0 | 73.2 | −8.8 |
| 확률 오차 | 35 | 69.6 | 64.8 | −4.8 |
| 만석 구분 | 15 | 78.5 | 73.1 | −5.4 |
| 놓침 비용 | 15 | 73.9 | 68.5 | −5.4 |
| 과신 벌점 | 15 | 80.1 | 76.2 | −3.9 |
| 종합 | 100 | 75.6 | 70.0 | −5.6 |
v1에서는 확률 맞춤만 반대로 올라 먼 지평의 손해를 가렸다. v2는 시간대별 편향을 먼저 재므로 다섯 항목이 같은 방향으로 움직인다. 8→12의 가중 기여는 확률 맞춤 −1.80점 · 확률 오차 −1.68점 · 만석 구분 −0.83점 · 놓침 비용 −0.81점 · 과신 벌점 −0.60점이다.
12를 고른 근거는 종합 점수가 아니라 커버율과 지평 기하다. 가중치는 여전히 유도한 값이 아니며, v1과 v2의 차이를 모델 성능 변화로 읽지 않는다. 같은 v2 안에서도 지평 사이 우열은 다섯 구성 항목이 모두 한쪽일 때만 말한다.
코호트로 쪼개면
합쳐 놓은 점수는 서로 반대인 구간을 지운다. 그래서 아침·저녁·평일·주말처럼 성격이 다른 묶음으로 갈라서 본다. 이렇게 가른 묶음 하나를 코호트라고 부른다.
v6는 얇은 코호트의 확률 맞춤을 숫자로 만들지 않는다.
코호트 안의 시간대 층 가운데 하나라도 30행 미만이면 calibration_stratified와 종합 점수를
null로 두고 이유를 남긴다. 전체 블록은 모든 시간대의 표본을 합쳐 재므로 이 관문과 역할이 다르다.
v1 코호트 점수는 새 눈금으로 다시 계산하기 전의 역사값이라 이 페이지에서 제거했다. 코호트끼리는 표본 구성과 기준선 분모가 다르므로 같은 v2 안에서도 숫자를 직접 빼서 우열을 말하지 않는다.
종합 점수는 어떤 재료로 만들어지나
다섯 항목은 아래 지표에서 그대로 나온다. 각 항목을 기준(기후값) 50점 · 완벽 100점 · 기준보다 손실 두 배면 0점으로 환산한 뒤 가중합한다. 항목 이름은 화면에 쓰는 우리말이고, 옆 칸의 영문 이름이 실제로 계산되는 값이다.
| 항목 | 원지표 | 좋은 방향 | 가중치 | 8정류장 원값 | → 환산 점수 |
|---|---|---|---|---|---|
| 확률 오차 | brier — 확률 예측의 평균 제곱오차 | 낮을수록 | 35 | 0.007688 | 69.6 |
| 확률 맞춤 | calibration_stratified — 시간대별 편향 가중평균 | 낮을수록 | 20 | 0.004197 | 82.0 |
| 만석 구분 | pr_auc — 만석을 위쪽에 세우는 능력 | 높을수록 | 15 | 0.574724 | 78.5 |
| 과신 벌점 | log_loss — 틀린 확신에 큰 벌점 | 낮을수록 | 15 | 0.027855 | 80.1 |
| 놓침 비용 | cost_4 — 정한 비용비의 오판 비용 | 낮을수록 | 15 | 0.026681 | 73.9 |
가중합하면 69.6×0.35 + 82.0×0.20 + 78.5×0.15 + 80.1×0.15 + 73.9×0.15 = 75.6 이다. 같은 이름이 두 가지 눈금으로 나오는 곳이 있다 — 성적 카드의 "만석 구분 0.575"는 원래 지표값(PR-AUC)이고, 위 표의 78.5는 그것을 점수로 환산한 값이다. 이름은 같아도 눈금이 다르니 헷갈리면 안 된다.
규약 출처는 score_policy.json(composite-score-v2)이며
기준 예측은 각 평가일보다 이른 학습일의 만석률만 쓰는 값이다.
규약 자체가 종합 점수를 두고 "설명과 비교를 돕는 보조값이며 자료·재현성 관문을 대신하지 않는다"고 밝혀 두었다.
8정류장 지평(중앙 16.8분) · 원지표 — 보고 기준
| 지표 | A18 | 기준(기후값) | 뜻 |
|---|---|---|---|
| 브라이어 | 0.007688 | 0.012628 | 확률 예측의 평균 제곱오차. 낮을수록 좋다 |
| 로그 손실 | 0.027855 | 0.069873 | 과신에 큰 벌점. 낮을수록 좋다 |
| 평균 보정 편향 | −0.000622 | −0.001704 | 진단용. v2 점수에는 넣지 않는다 |
| 시간대별 보정 오차 | 0.004197 | 0.011676 | v2 확률 맞춤의 재료. 낮을수록 좋다 |
| 누적 보정 오차 | 0.001117 | 0.004148 | 진단용. v2 점수에는 넣지 않는다 |
| 만석 구분(PR-AUC) | 0.574724 | 0.012026 | 만석 경보의 순위 품질. 높을수록 좋다 |
| ROC-AUC | 0.985353 | 0.453804 | 만석과 비만석을 가르는 능력 |
| 놓침 비용 | 0.026681 | 0.051067 | 정한 비용비에서의 오판 비용 |
절대 브라이어만 보고 좋고 나쁨을 정하면 안 된다. 만석이 드물수록 기준(기후값)의 브라이어도 같이 작아진다. 기준 대비 얼마나 앞서는지를 재는 브라이어 향상(BSS)이 그 눈금을 지운 값이고, 8정류장에서 39.1%다. 만석 구분과 브라이어 향상은 눈금이 흔들리지 않으므로 지평끼리 견줄 때는 이 둘을 본다.
기준(기후값)의 ROC-AUC 가 0.5 아래인 것은 모델이 나빠서가 아니라 기준 자체가 순위를 못 매기기 때문이다. 기후값은 하루 안에서는 모든 예측이 똑같은 값이라 같은 날 버스끼리는 줄을 세울 수가 없고, 남는 것은 날짜 사이의 순서뿐인데 그 순서가 실제와 반대로 나왔다. 그래서 0.5 아래로 내려간다.
지평을 어디서 끊나
raw 노선 순번 차이를 몇 칸까지 허용할지는 둘을 맞바꾸는 문제다 — 멀리 볼수록 답을 줄 수 있는 정류장이 늘고, 대신 만석 버스를 골라내는 능력은 깎인다. 앞의 값은 실제 운행 기록에서, 뒤의 값은 채점에서 나온다.
| 지평 | 중앙 소요 | 하루 전체 커버율 | 운행시간 커버율 | 만석 구분 | 무작위 대비 | 판정 |
|---|---|---|---|---|---|---|
| 8정류장 | 16.8분 | 78.0% | 83.7% | 0.575 | 45.0배 | 보고 기준 |
| 12정류장 | 26.7분 | 84.8% | 90.4% | 0.468 | 35.6배 | 서빙 상한 · 채택 |
| 16정류장 | 36.0분 | 85.7% | 91.0% | 0.333 | 24.0배 | 8/19 기각 이력 |
8/23 완료일 평가에서 하루 전체 8→12는 커버율 +6.73%p에 만석 구분 −18.6%다. 지평 16은 이번 평가 범위에 없으므로 최신 지평 12와 직접 비교하지 않고 기존 기각 이력으로만 남긴다. 지평 8·12의 커버율은 8/22까지 잰 별도 측정값이고, 무작위 대비는 최신 만석 구분(PR-AUC)을 같은 지평의 기저 만석률로 나눈 값이다.
이 판정에 종합 점수는 쓰지 않았다. 여기서 맞바꾸는 것은 답을 줄 수 있는 범위와 골라내는 능력뿐이다.
| 구간 | 커버율 증가 | 만석 구분 상대 손실 | 평가 상태 |
|---|---|---|---|
| 8→12 | +6.73%p | −18.6% | 성능 8/23 · 커버율 8/22 |
| 12→16 | +0.94%p | 8/19 이력 | 현재 계약 밖 · 재평가 안 함 |
지평 12는 운영 계약과 기하 커버리지의 상한이고, 이번 최신화는 그 선택을 다시 한 실험이 아니라 선택된 1~12의 성능 재검증이다. 무작위 대비 배수는 기저 만석률로 나눈 값이라 기저율이 다른 판끼리 견주면 안 되고, 같은 판 안에서 지평끼리 견줄 때만 쓴다.
커버율은 실제 운행 기록으로 계산했다 — 아무 순간이나 하나 잡아 노선의 승차 정류장을 훑고, 그 정류장 뒤 지평 안에 같은 운행의 버스가 실제로 있던 비율이다. 위 표는 하루 전체와 운행 시간대(06~23시)를 함께 적었다. 지평 12의 운행시간 커버율은 두 노선을 합쳐 90.4%다.
분모는 두 노선의 승차 정류장 143곳(1650 65 · 3330 78)이고, 분자는 상류
[S−지평, S−1] 구간에 같은 운행의 버스가 있던 칸이다. 상류 구간은 정차 여부를 가리지 않고 센다 —
거기 있는 버스도 곧 그 정류장에 오는 진짜 버스이기 때문이다.
버스가 한 대도 안 잡힌 순간도 분모에 들어가고 미커버로 센다.
노선 앞머리에서는 커버율을 더 올릴 수가 없다
지평 12 라도 순번 12 이하 정류장에서는 거슬러 올라가 볼 수 있는 정류장이 순번−1 개뿐이다.
(여기서 말하는 "정류장 몇 개"는 채점 단위인 칸과 아무 관계가 없다.)
그런 승차 정류장이 1650 은 8개 · 3330 은 12개로, 전체 143곳 중 14.0% 다.
실제로 그 구간의 커버율은 24.47%(1650)·41.85%(3330)까지 떨어지고, 나머지 구간은 98.92%·99.05% 다.
커버율을 더 올리는 길은 지평을 늘리는 것이 아니라 이 구간을 따로 푸는 것이다.
| 구간 | 정류장 비중 | 지평 12 커버율(운행 시간대) |
|---|---|---|
| 순번 13 이상 | 86.0% | 98.92% · 99.05% |
| 순번 12 이하 | 14.0% | 24.47% · 41.85% |
한 바퀴 돌아 오는 차량은 담지 않는다
순번 12 이하의 빈칸을 메우려면 이미 종점 쪽에 가 있는 차량이 한 바퀴 돌아 오는 것까지 세면 수치상 커버율은 오를 수 있다. 그러나 이 값들은 v6 창으로 다시 재지 않았으므로 현재 고정값으로 쓰지 않는다. 순번 축에서 이미 목표를 지난 차량은 그 전에 여러 차량이 먼저 도착할 수 있어 다음 버스라고 보장할 수 없다.
차고지에 대기 중인 실제 다음 차량은 차량위치 API에 아직 나타나지 않을 수 있다. 우리 눈에 보이는 것은 이미 운행 중인 차량뿐이므로 이 빈칸은 지평을 늘려 해결할 문제가 아니다.
그래서 한 바퀴 돌아 오는 차량을 담으면 커버율은 오르지만 엉뚱한 버스를 가리키게 된다. 그것을 "다음 버스"라고 보여 주는 셈이므로 담지 않는다. 이 구간은 차고지 발차 시각이 있어야 풀리는 별개 문제로 남겨 둔다.
지평은 시간이 아니라 raw 노선 순번 차이다
둘은 서로 바꿔 쓸 수 없다. 표에 적은 "중앙 소요"는 나중에 세어 본 중앙값일 뿐 환산식이 아니다.
여기서 horizonStops는 노선 대장의 순번 차이다. 중간의 경유 지점도 한 칸으로 세며 승차 가능한 정류장 수가 아니다.
같은 8정류장이 빠를 때 9.1분에서 느릴 때 23.0분까지 2.5배로 흩어지고(중간 절반이 12.0~19.0분),
거꾸로 5~10분 안에 도착하는 경우를 모아 보면 2·3·4·6·8정류장이 다 섞여 있다
(4정류장 38%, 3정류장 26%, 6정류장 23%). 길이 막히느냐 아니냐에 따라 달라지기 때문이다.
그런데도 순번 차이로 정하는 이유는, 걸린 시간은 도착한 뒤에야 알 수 있는 값이라
예측에 쓰면 미래를 훔쳐보게 되기 때문이다. 모델은 이 시간 차이를 입력으로 받지 않는다.
화면에 "몇 분 후"를 띄우려면 도착정보 API를 따로 불러야 하는데 그것은 지금 수집 대상이 아니다.
그래서 API 도 horizonStops(노선 순번으로 몇 칸 남았나)만 주고 도착 예정 시각은 주지 않는다.
지평 1~12 전부의 성적
클라이언트 계약이 1정류장부터 12정류장까지 예보를 약속하므로, 그 열두 지평을 모두 만들어
같은 규약으로 채점했다. 처음에는 1·2·3·4·6·8 만 있었고 5·7·9·10·11 은 표본조차 없었다 —
계약이 근거 없는 약속을 하고 있던 셈이라, 자료를 다시 만들어 메웠다.
| 지평 | 중앙 소요 | 종합 점수 | 브라이어 | 만석 구분 | 보정 편향 | 채점 표본 | 만석 표본 |
|---|---|---|---|---|---|---|---|
| 1 | 1.7분 | 91.9 | 0.00282 | 0.9114 | -0.0007 | 111,063 | 1,354 |
| 2 | 3.5분 | 88.5 | 0.00399 | 0.8562 | -0.0011 | 107,847 | 1,344 |
| 3 | 5.3분 | 84.7 | 0.00521 | 0.7647 | -0.0011 | 106,337 | 1,334 |
| 4 | 7.5분 | 82.7 | 0.00577 | 0.7270 | -0.0015 | 104,566 | 1,318 |
| 5 | 9.7분 | 80.9 | 0.00634 | 0.6840 | -0.0013 | 102,726 | 1,304 |
| 6 | 11.8분 | 78.8 | 0.00689 | 0.6467 | -0.0014 | 101,013 | 1,281 |
| 7 | 14.2분 | 77.4 | 0.00731 | 0.6095 | -0.0009 | 99,242 | 1,269 |
| 8 | 16.8분 | 75.6 | 0.00769 | 0.5747 | -0.0006 | 97,597 | 1,246 |
| 9 | 19.3분 | 73.8 | 0.00810 | 0.5371 | -0.0007 | 96,600 | 1,239 |
| 10 | 21.8분 | 72.7 | 0.00835 | 0.5179 | -0.0002 | 95,372 | 1,229 |
| 11 | 24.2분 | 70.6 | 0.00889 | 0.4855 | -0.0002 | 94,375 | 1,229 |
| 12 | 26.7분 | 70.0 | 0.00916 | 0.4678 | -0.0002 | 93,183 | 1,226 |
브라이어와 만석 구분은 열두 지평 내내 한 방향으로 움직인다 — 멀수록 어려워진다. 교정한 종합도 같은 방향이다. 표본이 지평마다 줄어드는 것은 목표 정류장 뒤로 그만큼의 관측 이력이 필요하기 때문이다. 서빙 상한 12 의 근거는 위 「지평을 어디서 끊나」에 있다.
놓침률(임계 0.5에서 놓치는 만석의 비율)은 8정류장 59.4% · 12정류장 67.2%이다. 채점 표본의 기저 만석률은 지평 1에서 1.22%, 지평 12에서 1.32%다.
Q. 언제 실패하나?
- 날짜 표본이 아직 얕다. 수집 자료는 8/14~8/23 열흘이고 실제 평가일은 8/15~8/23 아홉 날이다. 표본은 늘었지만 계절·시간표 변경을 포함한 장기 안정성을 말하기에는 부족하다.
- 만석이 새로 생기는 순간. 예보 시점에는 좌석이 있었지만 도착 뒤 0석이 된 사건은 과거 상태를 그대로 내미는 기준선으로 맞힐 수 없다. 이 사건 수는 창과 지평에 따라 바뀌므로 고정값을 본문에 박지 않고 사설 일일 원장에서 읽는다.
- 이력이 없는 정류장과 노선. 노선마다 따로 배우므로, 새 노선은 자기 이력이 쌓이기 전까지 예보할 수 없다. 그래서 신규 노선은 수집만 하고 예보를 내지 않는다.
- 운행 패턴이 크게 바뀔 때. 시간표 변경이나 계절 변화는 아직 겪어 보지 않았다.
Q. 어떻게 검증했나? — 2026-08-19 오프라인 설계 이력
아래는 v6 현재 성적표가 아니라 A18 채택 전에 수행한 역사 검증이다. 어떤 조건을 만족하면 채택할지를 결과를 보기 전에 문서로 못 박아 두고 그대로 적용했다.
아래 표와 네 가지 시험은 CRPS 로 잰 36칸·96 판정 위에 서 있고, 위 「지평 1~12 전부의 성적」 원장과는 별개 산출이다. 원장과 같은 대상 정의로 다시 돌리지 않았다 — 맨 아래 「다음 확인 사항」에 적어 두었다.
| 규칙 | 기준 | 실측 |
|---|---|---|
| 우연이라 보기 어려울 만큼 나빠진 칸 | 0건 / 96 판정 | 0건 |
| 분포 정확도 개선 | 24칸 중 20칸 이상 | 24/24, 전부 유의 |
| 아침 밴드 개선 | 12칸 전부 | 12/12 |
| 만석 확률 정확도 | 나빠진 칸 없음 | 0칸 |
| 결정성 | 같은 입력이면 같은 출력 | 두 노선 통과 |
여기에 "이래도 결과가 유지되나"를 확인하는 시험을 네 가지 더 걸었다.
- 미래를 훔쳐보는가 — 시험일 정오 이후에 도착한 사건의 정답을 전부 뒤집어도 정오 이전 시점의 예측이 정확히 0 만큼 변했다. 즉 미래를 안 본다. 반대로 과거 정답을 뒤집으면 0.788 변하므로, 시험 자체가 둔한 것은 아니다.
- 여러 번 재서 운 좋은 것만 골라 온 것인가 — 96번이나 검정한 만큼 우연히 잘 나올 확률을 감안해 합격선을 빡빡하게 잡아도, 분포 정확도가 24칸 중 23칸에서 살아남는다. 순전히 운으로 얻을 수 있는 겉보기 이득이 0.21석인데 실제로 관측된 이득은 2.07석으로 열 배다.
- 자료가 적어서 잘 보이는 것인가 — 학습을 하루에서 나흘로 늘려도 격차가 좁아지지 않는다. 정류장 이력 재료는 오히려 이득이 커졌다.
- 실제로는 알 수 없는 값을 몰래 쓰고 있는가 — 학습에 쓴 시간대 딱지가 전부 도착 시각 기준이었다(실측 100%). 그런데 예보를 낼 때는 도착 시각을 모른다. 그때 알 수 있는 것은 예보를 내는 시각의 시간대뿐이고, 둘은 12정류장 지평에서 10.11% 어긋난다 (2정류장 1.00% · 4정류장 2.72% · 8정류장 6.35%). 그래서 예측 시각 기준으로 바꿔 처음부터 다시 적합하고 다시 채점했다. 종합 점수의 차이는 2정류장 −0.1 · 4정류장 −0.2 · 8정류장 −0.3 · 12정류장 +0.1 이다. 가장 큰 손실이 0.3점이다. 우려는 실재했으나 크기가 무시할 만하다 — 시간대 꼬리표는 셀 통계의 묶음 키일 뿐이고, 경계를 넘는 3.65%가 이웃 셀로 옮겨가도 그 셀들의 통계가 크게 다르지 않기 때문이다. 12정류장에서 오히려 오른 것은 경계 부근 표본이 예측 시각 쪽에서 더 고르게 나뉘어서다. 보고하는 점수는 도착 시각 기준 그대로 둔다. 기존 7개 모델과 정답을 똑같이 정의해야 같은 표에서 비교할 수 있기 때문이다. 서빙 시 실제 성적은 예측 시각 기준 쪽에 가깝다.
이번에 확정한 것
- 서빙 상한은 12정류장. 하루 전체 커버율 84.8%, 운행 시간대 90.4%를 얻는 대신 만석 구분이 8정류장 0.558에서 12정류장 0.429로 내려가는 것을 받아들였다. 16은 기각했다. 근거는 커버율 1%p를 얻는 값이 8→12 구간에서 12→16 구간보다 하루 전체 6.3배, 운행시간 9.7배 싸다는 것이다.
- 한 바퀴 돌아 오는 차량은 도착 예정에 담지 않는다. 순번 축에서 이미 목표를 지난 차량을 다음 도착으로 되감으면 다른 운행을 가리킬 수 있기 때문이다. 과거 증분 수치는 v6로 다시 재지 않아 현재값으로 쓰지 않는다.
- 회차 순번은 1650=44 · 3330=43, 회차 정류장은 두 노선 모두 안양역(
208000069). 상류를 직접 불러 확인했고, 이전 문서의turnSequence: 31은 틀린 값이었다. - 회차 너머 버스도 예보에 담는다. 운행이 회차에서 끊기지 않고 0.3분 만에 지나간다는 것을 기록으로 확인했다.
- 노선마다 따로 배우고 계수를 공유하지 않는다. 36칸 전부에서 기존 기준선을 이겼다.
- 정원은 그 차량이 보여 준 최대 잔여석을 그대로 쓴다. 연구 자료와 운영 평가기가 같은 규칙을 쓴다.
고치기 전
lowPlate규칙으로 가르면 45·68·40석 세 갈래였다. 지금 표본에서는 열 갈래가 관측되고 가장 흔한 값이 44다 — 위 재료 표에 적어 두었다.
다음 확인 사항
- 역사 산출을 현재 원장과 섞지 않는다.
「지평 1~12 전부의 성적」·코호트 표·「지평을 어디서 끊나」·커버율이 원장이고, 나머지는 별개 산출이다.
같은 대상 정의로 다시 내지 않은 2026-08-19 오프라인 검증은 바로 위 역사 절에만 남긴다.
현재 수치의 단일 출처는
evaluation-v6사설 일일 지평 원장이다. - 노선 앞머리 정류장을 어떻게 할 것인가. 순번 12 이하 승차 정류장이 전체 143곳 중 14.0%(1650 8곳 · 3330 12곳)인데 그 구간 커버율이 24.47%(1650)·41.85%(3330) 밖에 안 된다. 다음 차량이 아직 차고지에 있어 차량위치 API에 잡히지 않는 경우다. 이 문제는 지평을 늘려도 안 풀리고, 한 바퀴 돌아 오는 차량을 세면 엉뚱한 버스를 가리킨다. 차고지 발차 시각이 있어야 풀린다. 지금은 그 정류장에서 빈 답을 낸다.
- 3330 의 일부 차량이 차고지가 아니라 회차지 부근(순번 42~43)에서 밤을 넘긴다. 30분 넘게 끊긴 구간 106건 중 56건이 300분을 넘고, 그 직전 순번이 전부 거기다. 운행이 언제 시작됐다고 볼지 판정하는 데 영향을 주는지 확인해야 한다.
- 회차 대기 시간이 시간대에 따라 달라진다 — 1650 기준 07시 106.7분에서 21시 46.0분까지 줄어든다. 지금 모델은 이 값을 쓰지 않는다.
- 수집 밀도 효과는 재검했다. 같은 통과 사건을 짝지은 하향 표집에서 지표 이동은 날짜 표본 잡음보다 작았다. 다만 600초 구간은 라벨 문턱을 넘어 사건 자체가 빠지므로 시간대별 검열 원장을 계속 본다.
- 날짜 표본이 여전히 얕다. 평가일은 5일, 평일은 2일뿐이다. 날짜별 지평 12 종합이 56.6~75.2로 흔들리므로 수준을 확정하거나 코호트 수치를 선정 근거로 쓰지 않는다.
- 서빙에 실을 계수 묶음이 아직 없다. 지금 숫자는 매번 새로 학습해서 다시 채점한 것이다. 고정된 계수 묶음이 없으면 배포도 재현도 되지 않는다.
- 회차 너머 예보를 계속 감시한다. 지평 12에서 회차를 넘는 표본은 8,091건·만석 72건이다. 시간대·노선별 층이 충분해질 때까지 전체 평균 하나로 안전을 단정하지 않는다.
- 현재 규격은
evaluation-v6·composite-score-v2다. 지평 1~12가 평가기 범위이고, 사설 일일 원장의 규격·점수정책·표본을 함께 확인한다. 과거 v1 점수와 이어 읽지 않는다. - 재료 두 개를 다시 판정해야 한다. 노선별로 나눠 배운 뒤로 지평에 따른 기준점 조절과 정원 대비 비율 구간, 이 둘의 기여가 거의 사라졌다. 빼도 성능이 유지되면 재료가 여섯 개에서 네 개로 줄어든다.
- 정류장 대기 인원. 만석이 새로 생기는 순간을 결정하는 것은 정류장에 서 있는 사람 수인데 그 값이 없다. 구간 수요 항목이 그 자리를 미리 잡아 둔 것이므로, 장기 평균 대신 실시간 추정치를 끼워 넣으면 구조는 그대로 두고 값만 바꿔 낄 수 있다.
부록 — 단계별 계산식
여기까지는 무엇을 왜 하는지의 설명이었다. 이 절은 실제 계산 순서를 식으로 그대로 적는다. 팀원이 코드를 열지 않고도 한 행의 예보가 어떻게 나오는지 손으로 따라갈 수 있어야 하고, 코드를 열 때는 어디를 볼지 알 수 있어야 한다. 그래서 단계마다 해당 함수 이름을 붙였다.
출처는 연구 구현 a18.py 와 공용 함수 occam/core.py 다.
아래 식은 그 코드를 읽어 옮긴 것이고, 문장으로 요약하지 않고 계산 순서 그대로 적었다.
서빙에 실릴 고정 계수 묶음은 아직 없다 — 위 「다음 확인 사항」에 적어 둔 그대로다.
한 행의 예보는 다섯 단계다. ① 만석 확률을 따로 낸다 → ② 도착 좌석의 기준점을 잡는다 → ③ 기준점에서 얼마나 어긋날지의 분포를 낸다 → ④ 그날 이미 관측된 도착으로 만석 확률을 보정한다 → ⑤ 둘을 합쳐 하나의 분포로 만든다.
기호
식에 나오는 기호를 전부 모았다. 이 표가 없으면 검산이 안 된다.
| 기호 | 뜻 | 범위 · 비고 |
|---|---|---|
| Y | 맞히려는 값 — 목표 정류장 도착 시점의 잔여석 | 0 … cap. 자료의 arr_seat |
| v | 예측 시점의 잔여석 (지금 그 버스에 남은 자리) | 자료의 v_seat |
| cap | 정원 — 그 차량이 보여 준 최대 잔여석 | 앵커(2단계)와 셀 통계에서만 max(cap, 1) 로 바닥을 둔다. 설계행렬 5·12번 열은 바닥 없이 그대로 나눈다 |
| cap_eff | 격자에 쓰는 유효 정원 | min(max(cap, v), 70). v > cap 인 행이 자료에 있다 |
| m | 기준점(앵커) — 2단계가 내는 정수 좌석수 | 0 … cap |
| R | 변화량 — 기준점에서 도착까지 얼마나 줄었나. R = m − Y | −40 … 50 (정수 격자) |
| p0 | 만석 확률 P(Y = 0) | 1단계가 내고 4단계가 보정한다 |
| x | 그 행의 설계행렬 한 줄 — 31개 숫자 | 0단계 |
| X, X_p | 여러 행의 x 를 쌓은 행렬. X_p 는 그중 Y > 0 인 학습 행만 (3단계 전용) | (행 수) × 31 |
| w | 로지스틱 회귀 계수 벡터 | 31차원 |
| j | 지평 — 목표 순번과 차량 순번의 차이. 경유 지점도 한 칸이다 | 1 … 12. 승차 정류장 수가 아니며 지평마다 모델이 다르다 |
| pz, pp, pn | R의 부호 확률 — 각각 R = 0 · R > 0 · R < 0 | 세 값의 합이 1 |
| h⁺, h⁻ | 물리 여유폭 — 줄어들 수 있는 폭과 늘어날 수 있는 폭 | h⁺ = max(m,1) · h⁻ = max(cap_eff−m,1). 학습 행에서는 h⁻ 에 cap_eff 대신 max(cap,1) 을 쓴다 |
| T_b | 상대 구간 경계 — 행마다 다르다 | b = 0 … 8 |
| M[b] | |R| 이 구간 b 에 들어갈 확률 | 9개의 합이 1 |
| λ6 | 불가능해진 구간의 질량을 경계에 접는 비율 | 0 … 1, 학습자료로만 고른다 |
| u | 정류장 상대위치 — 노선 안에서 0~1 | station ÷ 노선 최대 순번 |
| ε | L1 회귀 가중치의 하한 — 좌석 양자화 폭의 절반 | 0.5 ÷ mean(cap) |
| c, a | 2단계 기준점 회귀의 절편과 기울기 — a 는 「지금 좌석을 얼마나 믿는가」 | a 는 j1 0.995 → j8 0.386 |
| n0, c | 4단계 사전확률 보정의 사전 표본수와 절편 이동량 | n0 = 200 · |c| ≤ 3. 2단계의 절편 c 와 이름만 같고 다른 값이다 |
| n_obs | 4단계에서 세는 그날 이미 도착이 끝난 사건 수 | 코드는 이것도 m 이라 부른다 — 기준점 m 과 다른 값이다 |
상수
코드에 박혀 있는 값이다. 전부 결과를 보기 전에 정했다.
| 이름 | 값 | 무엇인가 |
|---|---|---|
GRID | 71 | 좌석 격자 0…70 |
DMIN, DMAX | −40, 50 | 변화량 R 격자의 양끝 |
DSPAN | 91 | R 격자 칸 수 (= DMAX − DMIN + 1) |
REL_EDGES | 0 · .03 · .07 · .12 · .20 · .32 · .48 · .70 · 1.00 | 상대 구간 경계 9개 → 구간 9개 |
RIDGE | 1.0 | 모든 로지스틱 회귀의 정규화 세기 |
NK_SPLINE | 8 | 정류장 위치 기저 개수 |
MAXST | 1650 = 88 · 3330 = 84 | u 를 만들 때 나누는 노선별 최대 순번 |
NB_RADIUS | 4 | 이웃 정류장 폴백 반경 (1/d² 가중) |
SHIFT_N0 | 200 | 4단계 사전확률의 가상 표본수 |
SHIFT_MINN | 50 | 4단계를 켜는 최소 관측 건수 |
SHIFT_CAP | 3.0 | 4단계 절편 이동량의 상한 (로짓 단위) |
MASK_TARGET | 50 (하한 0.05 · 상한 0.5) | 학습 때 정류장 통계를 일부러 가리는 비율의 목표 |
LGRID6 | 0, 0.05, …, 1.00 | λ6 후보 격자 |
SEED | 20260818 | 난수 씨앗. +202 는 마스킹, +303 은 λ6 겹 |
예보를 만드는 경로에서는 난수를 쓰지 않는다 — 이 씨앗으로 실제 난수를 뽑는 곳은
신뢰구간을 내는 군집 부트스트랩뿐이다. 겹 나누기와 마스킹은 행 식별자를 해시해서 정하므로
(blake2b(오프셋 | route|station|horizon|t_pred)) 학습 행의 순서가 바뀌어도 결과가 같다.
그래서 「같은 입력이면 같은 출력」이 성립한다.
0단계 — 설계행렬 31열 (build_design)
모든 회귀가 같은 31개 숫자를 입력으로 받는다. 단계마다 라벨만 다르고 x 는 하나다. 위 「모델에 들어가는 31개 값이 어디서 오나」의 세 표는 성격별 요약이고, 여기가 열 하나하나의 정의다.
기본 20열 — base_feats
| # | 열 | 정의 |
|---|---|---|
| 1 | 상수 | 1 |
| 2 | 아침 | 1 if time_band = morning else 0 |
| 3 | 저녁 | 1 if time_band = evening else 0 |
| 4 | 처음 보는 시간대 | 1 if time_band 이 학습에 없던 값 else 0 |
| 5 | 지금 잔여석 비율 | v / cap |
| 6 | 지금 만석인가 | 1 if v = 0 else 0 |
| 7 | 낮은 좌석 구간 | min(v, 20) / 20 |
| 8 | 혼잡도 0 | 1 if crowded = 0 else 0 |
| 9 | 혼잡도 1 | 1 if crowded = 1 else 0 |
| 10 | 혼잡도 2 | 1 if crowded = 2 else 0 |
| 11 | 혼잡도 3 | 1 if crowded = 3 else 0 |
| 12 | 정원 | cap / 68 |
| 13 | 상류 좌석 기울기 | slope (없으면 0) |
| 14 | 기울기 결측 | 1 if slope 없음 else 0 |
| 15 | 연속 만석 횟수 | min(streak_full, 5) / 5 |
| 16 | 직전 차량 만석 | prev_full (0 또는 1) |
| 17 | 직전 차량 잔여석 | prev_seat / 60 (없으면 0) |
| 18 | 직전 차량 결측 | 1 if prev_seat 없음 else 0 |
| 19 | 노선 | 1 if route = 3330 else 0 |
| 20 | 정류장 순번 | station / 60 |
19번은 노선별로 나눠 배우는 지금 구성에서는 한 적합 안에서 상수다 — 절편에 흡수될 뿐 정보를 더하지 않는다. 열 수는 31 그대로 두었다. 합쳐 배우던 시절의 열이 남아 있는 것이다.
정류장 위치 8열 — spline_cols
u = station / MAXST[route] 1650 → 88, 3330 → 84 k_i = i / 7 i = 0 … 7 0~1 에 고르게 놓은 매듭 8개 21 … 28 번 열: max( 0 , 1 − |u − k_i| × 7 ) 삼각 기저
라벨을 전혀 쓰지 않는 열이다. 정류장 번호를 순서가 있는 위치로 바꿔 주는 역할만 한다.
정류장 통계 3열 — cell_cols
| # | 열 | 정의 |
|---|---|---|
| 29 | occ_z | 그 (노선, 정류장, 시간대) 의 평균 점유율을 z 화한 값 |
| 30 | board_z | 같은 키의 순수요 z 값. 지평 j 만큼 뒤로 이어지는 구간의 합 |
| 31 | miss | 1 if occ_z 를 자기 셀에서 못 얻어 이웃에서 빌려 왔음 else 0 |
셀·날짜 단위 (지평 1 행만 씀)
occ(셀,날) = mean( 1 − Y / max(cap,1) )
board(셀,날) = mean( v − Y ) / mean( max(cap,1) )
셀 통계 = 날짜별 값의 단순 평균 ← 폴링 밀도가 새지 않게 날짜 균등가중
z 화 = ( 셀 통계 − μ ) / ( σ + 1e-9 ) μ, σ 는 (노선, 시간대) 안에서
구간합 (30번 열)
board_z = ( Σ z_board[route, st, band] ) / √(개수)
st = station−j+1 … station
개수 = 그 구간에서 값이 실제로 있었던 정류장 수 (j 가 아니다).
하나도 없으면 이웃 폴백으로, 그것도 없으면 0.
폴백 자기 셀이 없으면 같은 (노선, 시간대) 의 이웃 정류장에서
0 < |Δstation| ≤ 4 · 가중 1/d² 로 보간한다 (자기 자신은 뺀다).
그것도 없으면 0.
31번 miss 는 29번(occ_z)이 폴백됐는지만 가리킨다. 30번의 폴백은 따로 표시하지 않는다.
학습 때는 일부 행의 자기 셀 통계를 일부러 가려(마스킹) 폴백 분기를 실제로 배우게 한다 —
가리는 비율은 clip(50 / 학습 만석 건수, 0.05, 0.5) 이고, 어느 행을 가릴지는 행 식별자 해시로 정한다.
1단계 — 만석 확률 p0 (fit_irls)
잔여석 0 은 평범한 한 칸이 아니라 경보의 대상이다. 그래서 분포에서 떼어내 「만석인가 아닌가」만 보는 회귀를 따로 돌린다. 이 확률이 그대로 5단계에서 0 칸의 값이 된다.
학습 라벨 y0 = 1 (Y = 0 인 행)
y0 = 0 (Y > 0 인 행)
계수 w0 = IRLS( X_학습 , y0 , ridge = 1.0 )
예보 p0 = sigmoid( x · w0 )
sigmoid(z) = 1 / ( 1 + exp( −clip(z, −30, 30) ) )
IRLS 한 걸음 — 최대 60회, max|Δw| < 1e-10 이면 멈춘다
p = sigmoid( X w )
W = p(1 − p) + 1e-9
H = Xᵀ W X + ridge · I
g = Xᵀ (y − p) − ridge · w
w ← w + H⁻¹ g
벌점 ridge·I 는 상수열에도 걸린다. 만석이 드문 자료라 절편이 자유로우면
확률이 쉽게 튀는데, 이 구성은 그것을 막는 대신 만석률을 조금 낮게 잡는 쪽으로 치우친다.
그 치우침을 되돌리는 것이 4단계다.
2단계 — 기준점 m (fit_anchor · fit_l1 · anchor_loc)
"지금 20석 남았으니 도착해도 20석 근처"가 출발점이다. 다만 가까운 지평에서만 맞는 말이고 멀어질수록 지금 좌석의 힘이 빠진다. 그래서 「지금 좌석을 얼마나 믿을지」인 기울기 a 를 자료에서 배운다. 평균이 아니라 중위수를 맞춘다 — 만석 근처의 꼬리가 기준점을 끌고 가지 않게 하기 위해서다.
학습 — L1(중위수) 회귀, IRLS 로 최대 40회
목표 min Σ | Y_i/cap_i − ( c + a · v_i/cap_i ) |
가중 ω_i = 1 / max( |r_i| , ε ), ε = 0.5 / mean(cap)
r_i = Y_i/cap_i − ( c + a · v_i/cap_i ) 직전 걸음의 잔차
갱신 (c, a) ← 가중최소제곱( [1, v/cap] , Y/cap , ω )
첫 값은 보통최소제곱. max|Δ(c,a)| < 1e-9 이면 멈춘다
예측
f = c + a · ( v / cap )
m = round( clip( f · cap , 0 , cap ) ) ← 정수 좌석수
a 는 지평이 멀어질수록 저절로 작아진다 — 1정류장 전 0.995 에서 8정류장 전 0.386 이다.
이 재료를 빼면 m = v 로 굳는다 (a = 1 강제).
3단계 — 변화량 R 의 분포
기준점만으로는 "얼마나 확실한가"를 말할 수 없다. 그래서 기준점에서 얼마나 어긋날지를 분포로 낸다. 먼저 어느 쪽으로 어긋나는지(부호), 다음에 얼마나 크게 어긋나는지(크기), 마지막에 그 크기를 격자 위에 어떻게 펴는지(분산) 순이다.
R = round( m − Y ) 학습에서는 Y > 0 인 행만 쓴다
(Y = 0 은 1단계가 맡으므로 여기서 빼야 이중계산이 없다)
3a. 부호 — _sign_probs
pz~ = sigmoid( x · IRLS( X_p , 1[R = 0] ) ) pp~ = sigmoid( x · IRLS( X_p , 1[R > 0] ) ) pn~ = max( 1 − pz~ − pp~ , 1e-6 ) ← 따로 적합하지 않고 나머지로 둔다 t = pz~ + pp~ + pn~ pz , pp , pn = pz~/t , pp~/t , pn~/t
세 갈래 중 둘만 회귀로 적합한다. 음의 방향(좌석이 늘어나는 쪽)은 남은 몫으로 얻고 1e-6 로 바닥을 깐 뒤 셋을 합이 1 이 되게 나눈다. 코드를 볼 때 여기서 세 번째 회귀를 찾으면 없다.
3b. 크기 — _mag_matrix_rel · rel_thresholds
같은 "5석 감소"라도 40석 남은 차와 6석 남은 차에서 뜻이 다르다. 그래서 구간을 절대 좌석수가 아니라 남은 여유폭 대비 비율로 끊는다. 구간 경계가 행마다 달라진다.
여유폭 h⁺ = max( m , 1 ) 좌석이 줄 수 있는 폭
h⁻ = max( cap_eff − m , 1 ) 좌석이 늘 수 있는 폭
경계 T_b = floor( REL_EDGES[b] × h ), b = 0 … 8
구간 b [ max(T_b + 1, 1) , min(T_(b+1), 50) ] b = 0 … 7
구간 8 [ T_8 + 1 , 50 ] T_8 = h — 여유폭을 넘는 꼬리
학습 배정 b(i) = clip( Σ_b 1[ T_b < |R_i| ] − 1 , 0 , 8 )
구간 확률 M~[b] = sigmoid( x · IRLS( X_p[해당 부호] , 1[ b(i) = b ] ) )
M[b] = M~[b] / Σ_b' M~[b']
경계식의 h 는 그 행의 부호에 맞는 여유폭이다 — R > 0 쪽을 계산할 때는 h⁺,
R < 0 쪽을 계산할 때는 h⁻ 를 넣는다. 그래서 같은 REL_EDGES 를 써도 두 방향의 경계가 서로 다르게 나온다.
두 개의 방어가 있다. 그 부호의 학습 행이 50건 미만이면 크기 모형을 만들지 않고 그 부호의 질량을 통째로 R = 0 으로 보낸다. 어떤 구간의 학습 사건이 10건 미만이면 그 구간 열을 1e-6 상수로 두어 사실상 비운다.
3c. 분산 — delta_pmf_rel · support_bounds
구간 안에서는 어느 값이 더 그럴듯한지 배우지 않는다. 균등하게 편다. 대신 좌석이 0 아래로 내려가거나 정원을 넘을 수는 없으므로, 물리적으로 불가능한 쪽은 잘라 낸다.
lo_b = max( T_b + 1 , 1 )
hi_b = min( T_(b+1) , 50 , lim(부호) )
P( |R| = k ) = (부호 확률) × M[b] / ( hi_b − lo_b + 1 ), lo_b ≤ k ≤ hi_b
물리 상한
lim(+) = max( m , 0 ) R ≤ m ⇔ Y ≥ 0
lim(−) = max( cap_eff − m , 0 ) −R ≤ cap_eff − m ⇔ Y ≤ cap_eff
잘라 낸 뒤 hi_b < lo_b 가 되어 구간 전체가 사라지는 경우가 있다.
음의 부호에서 그렇게 남은 질량만 「고아 질량」으로 따로 모아 5단계에서 경계 칸에 접는다.
양의 부호에서 사라진 질량은 담지 않고 마지막 재규격화에 흡수된다.
그리고 R 격자(−40…50) 아래로 밀려난 값은 가장자리 칸에 눌러 담는다.
4단계 — 사전확률 보정 (online_shift)
1단계의 p0 는 학습 기간의 만석률에 맞춰져 있다. 오늘이 그날들보다 붐비면 통째로 낮게 나온다. 그래서 예측 시점까지 이미 도착이 끝난 그날의 사건만 모아, 예보 평균과 실제 만석률의 차이만큼 절편을 민다. 미래는 보지 않는다 — 예측 시점에 지난 도착은 실제로 관측되는 값이다.
예측 시각 t 이전에 도착이 끝난 그날 사건 n_obs 건에 대해
p̄ = ( 1/n_obs ) Σ p0_k 그 사건들의 예보 평균
target = ( Σ y0_k + n0 · p̄ ) / ( n_obs + n0 ) n0 = 200
c = clip( logit(target) − logit(p̄) , −3 , +3 )
p0' = sigmoid( logit(p0) + c )
logit(p) = log( p / (1 − p) ), p 는 [1e-6, 1−1e-6] 로 자른다
n_obs < 50 이면 보정하지 않는다 (p0 를 그대로 쓴다)
n0 = 200 은 「관측 200건만큼의 무게로 원래 예보를 믿는다」는 뜻이다.
아침 첫 차에서는 n_obs 가 작아 보정이 거의 안 걸리고, 하루가 갈수록 그날의 실측이 무거워진다.
코드는 이 건수도 m 이라는 이름으로 쓴다 — 2단계의 기준점 m 과 이름만 같고 다른 값이다.
5단계 — 합치기 (scatter_support · finish_hurdle)
여기서 R 의 분포를 좌석의 분포로 되돌리고, 만석 확률을 0 칸에 얹는다. 1…cap 의 질량은 (1 − p0) 가 되도록 다시 재고, 0 칸은 4단계가 낸 값을 그대로 쓴다.
Y = m − R seat[Y] += P(R) 0 ≤ Y ≤ cap_eff 인 항만 담는다 seat[cap_eff] += λ6 × (고아 질량) 3c 에서 사라진 음의 부호 질량 P( Y = 0 ) = p0 P( Y = k ) = ( 1 − p0 ) × seat[k] / Σ_(j ≥ 1) seat[j] k = 1 … cap_eff
λ6 은 학습자료만으로 고른다 (choose_lam6) — 학습을 2겹으로 나눠
0 부터 1.00 까지 0.05 간격으로 CRPS 를 재고 가장 낮은 값을 쓴다. 시험자료는 보지 않는다.
겹을 나누는 기준도 행 식별자 해시라 학습 행의 순서에 영향받지 않는다.
적합 단위
계수는 (노선 × 지평) 마다 따로 배운다. 두 노선이 계수를 공유하지 않고, 8정류장 전 모델과 4정류장 전 모델도 별개다. 지평은 특징이 아니라 모델을 나누는 축이다.
- 노선을 나누는 것은 위 함수들 바깥이다.
a18.py안의 자료 나누기는 지평과 날짜만 가른다 — 학습 행을 노선으로 거르는 곳은a18v4/perroute_eval.py이고, 위 계산은 그렇게 걸러진 행을 받아 그대로 돈다. - 정류장 통계는 교차적합한다. 학습 행에는 그 행의 날짜를 뺀 통계를 준다(하루씩 빼고 만든다는 뜻으로 LODO, leave-one-day-out). 자기 날짜가 자기 특징에 들어가면 학습에서만 잘 맞는 열이 된다.
- 정류장 통계의 원자료는 지평 1 행만 쓴다. 같은 통과 사건을 지평마다 다시 세지 않기 위해서다.
- 시험 행에는 학습 날짜 전체의 통계를 준다. 시험일의 자료는 통계에 들어가지 않는다.
- 학습 날짜가 하루뿐이면 정류장 통계 3열을 아예 끈다 — 날짜를 빼는 교차적합이 성립하지 않기 때문이다. 그 경우에만 열이 31 이 아니라 28 이 된다.
구현 대조표
코드를 열었을 때 어디를 볼지의 표다.
| 단계 | 함수 | 파일 |
|---|---|---|
| 0 · 설계행렬 | build_design · spline_cols · cell_cols | a18.py |
| 0 · 기본 20열 | base_feats | occam/core.py |
| 0 · 셀 통계 | build_ctx · build_cell_stats · zmap · neighbor_map | a18.py |
| 1 · 만석 확률 | _fit_core 앞부분 · fit_irls | a18.py · occam/core.py |
| 2 · 기준점 | fit_anchor · fit_l1 · anchor_loc | a18.py |
| 3a · 부호 | _sign_probs | a18.py |
| 3b · 크기 | _mag_matrix_rel · rel_thresholds · rel_bin_index · rel_bin_ranges | a18.py |
| 3c · 분산 | delta_pmf_rel · support_bounds | a18.py |
| 4 · 사전확률 보정 | online_shift | a18.py |
| 5 · 합치기 | scatter_support · finish_hurdle | a18.py |
| 5 · λ6 선택 | choose_lam6 | a18.py |
| 전체 · 한 행의 흐름 | _fit_core | a18.py |
| 노선별로 나눠 적합 | main 의 행 거르기 | a18v4/perroute_eval.py |
| 채점 | crps_array | occam/core.py |