한화 신입사원 AI 솔루션 챌린지 · 과제 03 고객 페인포인트 해결

불꽃배웅 — 서울세계불꽃축제 귀가 출구 내비 · 최종 보고

눈부신 밤의 끝, 집으로 가는 길까지. · 2026-09-08 기준 (9/5 실전 결과 반영) · 작성: 박재형 · 쉬운 버전(5분) · 피치 덱(12장) · 완성된 대시보드 보기 · 코드 저장소 · 행사일 2026-09-05(토) 실전 운전 완료
차례
  1. 문제 제기 — 귀가가 무너진다
  2. 해결 방안 — 상태가 아니라 행동을 알려준다
  3. 설계 방법 — 과거 실측을 기준점 삼은 예측 모델
  4. 아키텍처 — 전체가 어떻게 맞물리나
  5. 자체 적대적 검증 — 우리 것을 먼저 깨 봤다
  6. 실전 결과 — 2026-09-05 당일
  7. 용어 사전
  8. 참고문헌·데이터 출처
다섯 줄 요약.
① 문제 — 불꽃축제가 끝나면 100만 명이 동시에 귀가하는데, 지도앱은 그날의 역 통제(여의나루 무정차)를 몰라서 막힌 길로 안내한다.
② 해법 — "지금 어디로 나가면 덜 막히나"를 3초 안에 보여주는 웹페이지. 출구 7개의 시간대별 혼잡을 미리 예측한다.
③ 방법 — 2024·2025년 축제일에 사람들이 실제로 어느 역에서 몇 시에 탔는지(실측)를 뼈대로 쓰고, 당일 관측으로 규모만 보정한다.
④ 검증 — 과거로 돌아가 맞혀보는 백테스트로 확인: 혼잡 등급 적중 76~100%. 남는 불확실성은 숨기지 않고 "범위"로 표시한다.
⑤ 실전 — 9/5 본번에서 12시간 동안 5분마다 예측을 발행했다(287개). 유출 피크 시각 22시를 맞혔고(형태 상관 0.66), 같은 도시데이터의 서울시 12시간 예측은 피크를 9배 낮게 봤다. 놓친 것은 23시 꼬리(실측은 피크의 92%, 예측 41%) — 그대로 적었다(§6). 적대적 검증은 7회차 39건까지 갔다(§5).

읽는 법 — 배경지식 없이 처음 읽는다면 그림 중심의 쉬운 버전(5분)부터 보면 좋다. 바쁘면 위 다섯 줄과 §1~2만 읽어도 흐름이 잡힌다. §3은 예측을 만들고 검증한 방법, §4는 시스템이 돌아가는 구조, §5는 우리 스스로 찾은 결함과 그 처리, §6은 9/5 실전 결과와 채점이다. 낯선 용어는 모두 §7 용어 사전에 풀어 두었고, 차트는 점에 마우스를 올리면 값이 나온다.

1문제 제기 — 귀가가 무너진다

서울세계불꽃축제는 매년 여의도 한강공원 일대에 약 100만 명이 모이는, 한화가 주최하는 국내 최대 규모 불꽃 행사다. 문제는 축제가 아니라 축제가 끝난 직후 1~2시간이다.

21시가 넘어 마지막 불꽃이 터지면, 공원에 있던 사람들이 한꺼번에 지하철역으로 걷기 시작한다. 이때 벌어지는 일:

1.1 지도앱이 막힌 길로 안내한다

공원에서 가장 가까운 역은 여의나루역(5호선)이다. 네이버지도·카카오맵에 "집 가는 길"을 검색하면 당연히 여의나루역으로 안내한다. 그런데 축제일 저녁 여의나루역은 안전상 무정차 통제된다. 걸어가 봐야 닫혀 있다.

이것은 추측이 아니라 실측이다. 서울교통공사 승하차 데이터(OA-12921)를 보면:

실측 (2024·2025 축제일)수치의미
여의나루역 19~21시 하차 인원≈ 0명무정차 통제 — 역이 사실상 닫혀 있었다
여의도역 21시 승차평시 토요일의 12~17배통제를 피한 인파가 옆 역으로 몰렸다
축제일 저녁 7개 역 초과 승차 합약 10.9만 명 (2년 평균)귀가 인파의 실제 규모

1.2 서울시 데이터는 "상태"만 준다

서울시 실시간 도시데이터라는 훌륭한 공공 API가 있고, 여의도 인구·혼잡도를 5분마다 알려준다. 하지만 알려주는 것은 "지금 붐빔"이라는 상태까지다. 관람객이 정작 알고 싶은 "그래서 어디로, 언제 나가야 하나"라는 행동을 알려주는 서비스는 없다.

1.3 누가 겪는 문제인가

KT 통신 데이터 기준 관람객 최다 집단은 20·30대 여성이다. 대부분 대중교통으로 귀가하고, 쇼가 끝난 뒤 어두운 공원에서 통신까지 폭주하는 상황에서 판단해야 한다. 그 순간에 복잡한 앱은 못 쓴다 — 한눈에 읽히는 한 장이 필요하다.

2해결 방안 — 상태가 아니라 행동을 알려준다

출구(지하철역 6개 + 도보 경로 1개)마다 시간대별로 "얼마나 막힐지"를 미리 예측해서, 순위·등급·예상 대기시간으로 보여주는 웹페이지다. 열면 3초 안에 답이 나온다.

2.1 사용 장면

  1. 21:15, 마지막 불꽃 직후. 관람객이 QR로 페이지를 연다 (통신 폭주 대비 — 한 번 열었던 페이지는 저장돼 있어 다시 열린다).
  2. 화면 맨 위: "지금 나가면: 신길 여유 · 마포대교 도보 여유 / 샛강 주의 · 여의도(5) 주의 / 여의나루 통제 중(21:40 해제)"
  3. 자기 집 방향 탭을 누르면 그 방향 추천 출구와 도보 소요, 시간대별 전망(지금 갈까, 30분 놀다 갈까)이 보인다.

2.2 화면이 주는 정보

2.3 왜 앱이 아니라 정적 웹페이지인가

3설계 방법 — 과거 실측을 기준점 삼은 예측 모델

3.1 왜 머신러닝을 안 썼나

ML로 풀려면 학습 데이터가 필요한데, 이 축제의 데이터는 2024·2025년 단 2일뿐이다. 표본 2개로 모델을 학습시키면 외우기(과적합)가 될 뿐이다. 그래서 학습 대신, 실측값을 뼈대로 쓰고 물리 법칙(걷는 속도, 역 처리량)으로 살을 붙이는 구조 모델을 택했다. 구조 모델은 규칙을 데이터에서 통째로 배우게 하는 대신, 규모(Est)·분배(비중)·시간 형태(도착 곡선)처럼 사람이 검증할 수 있는 부품으로 식을 조립하는 방식이다. 부품마다 따로 근거를 대고 따로 고칠 수 있어, 표본이 적어도 어디가 틀렸는지 짚을 수 있다. 미국 애틀랜타 지하철(MARTA)의 경기장 이벤트 수요 예측 논문도 같은 접근으로 오차 11.7%를 달성했다 — 데이터가 적을 땐 이 방식이 정석이다.

3.2 수요 식 — 세 요소의 곱

출구 수요(역, 시각) = α × Est × 도착형태(시각)
"작년에 여의도(5)역으로 나간 사람이 전체의 29%였다(E_st). 사람들은 쇼가 끝나고 걷는 시간만큼 늦게 역에 도착한다(도착형태). 오늘 인파가 작년의 1.2배로 보이면 전체를 1.2배 한다(α)." — 이 세 개를 곱하면 그 역의 그 시각 수요다.
요소무엇인가어떻게 구했나
Est
규모·분배
축제일에 각 출구로 나간 실제 인원. "초과 승차" = 축제일 승차 − 평시 토요일 중앙값(평소 그 역을 쓰는 사람을 뺀 순수 축제 인파)서울교통공사 시간대별 승하차 + 교통카드 일별 데이터, 2년 평균. 비중: 여의도(5) 29% · 샛강 17% · 여의도(9) 17% · 마포도보 13% · 국회 11% · 여의나루 10% · 신길 3%
도착형태
시간 분포
인파가 몇 시에 역에 도착하는가KT 생활이동 유출 곡선을 쇼 종료 시각에 맞춰 이동(2026년은 21:10 종료라 과거보다 +40분) → 관람 구역에서 각 출구까지 거리 ÷ 보행속도로 지연. 보행속도는 밀도가 높을수록 느려지는 공학 공식(Weidmann) 사용
α
당일 배율
오늘 인파가 예년 대비 몇 배인가당일 실시간 관측으로 갱신 (3.4)

정식 표기 — 세 요소를 수식으로

위 곱셈식을 기호로 적으면 다음과 같다. 역 \(st\), 시각 \(h\)의 출구 수요는

\[ D_{st}(h) \;=\; \alpha \cdot E_{st} \cdot \varphi_{st}(h), \qquad \textstyle\sum_h \varphi_{st}(h) = 1 \]

이고, 도착 형태 \(\varphi_{st}\)는 KT 유출 곡선을 쇼 종료 이동분만큼 옮긴 뒤 도달 지연으로 시간대에 배분한다:

\[ \varphi_{st}(h) \;=\; \sum_{k\in\{0,1,2\}} w_{st}(k)\,\hat g(h-k), \qquad \hat g(h) = g_{\mathrm{KT}}(h - \Delta_{2026}),\ \ \Delta_{2026} = +40\text{분} \]

배분 비율 \(w_{st}\)는 도달 지연 \(\tau_{st}\)에서 나온다 — 시간대 안 균등 출발을 가정하면 \(\tau_{st}\)분 걸리는 이동은 같은/다음 시간대로 \((60-\tau)/60,\ \tau/60\)씩 갈린다. 지연 자체는 거리 나누기 밀도별 보행속도다:

\[ \tau_{st} = \frac{1}{60}\!\left[\frac{\ell_1}{v(\rho_{\mathrm{zone}})} + \frac{L_{st}-\ell_1}{v(\rho_{\mathrm{street}})}\right]\!, \qquad v(\rho) = \max\!\Big(0.15,\ 1.34\big[1-e^{-1.913(1/\rho-1/5.4)}\big]\Big)\ \mathrm{m/s} \]

\(v(\rho)\)는 Weidmann(1993)의 Kladek 식(§8.2 [2]). \(L_{st}\) = 관람 구역→출구 직선거리 × 우회계수 1.3(추정 — 9/1 구축한 보행망으로 재현하면 이벤트광장→여의도역 1.31배로 이 값에 가깝다, §3.10), \(\ell_1 = \min(300\,\mathrm{m}, L_{st})\)은 관람 구역 밀도 \(\rho_{\mathrm{zone}}\)로 걷는 초반 구간(추정), 이후는 가로 밀도 \(\rho_{\mathrm{street}}=1.5\)명/m²(추정). \(\rho_{\mathrm{zone}}\)은 당일 CCTV 등급에서 읽고(여유 1.5 · 주의 3.0 · 경계 4.0 · 심각 5.0명/m², §3.7), 관측이 없으면 3.0. 구현: src/nowcast.py kladek()·lag_table().

표 3-1 — 기호·파라미터 일람

기호뜻값 · 산출근거
\(D_{st}(h)\)역 \(st\)·시각 \(h\)의 출구 수요본문 수요식—
\(\alpha\)당일 규모 배율사전 1.0 · 당일 사후분포 p50 (§3.4)당일 관측
\(E_{st}\)축제일 초과 승차 (연도별)표 3-2OA-12921 · OA-12914
\(\varphi_{st}(h)\)도착 형태 (정규화)KT 유출 곡선 × 도달 지연 배분OA-22300
\(\Delta_{2026}\)쇼 종료 이동+40분 (20:30 → 21:10)공식 공지
\(\tau_{st}\)도달 지연거리 ÷ \(v(\rho)\)Weidmann 1993
\(v(\rho)\)밀도별 보행속도\(v_0\) 1.34 m/s · \(\rho_{\max}\) 5.4명/m² · \(\gamma\) 1.913 · 하한 0.15Weidmann 1993
우회계수 · \(\ell_1\) · \(\rho_{\mathrm{street}}\)경로 보정 상수1.3 · 300 m · 1.5명/m² (2026-09-05 정정: 화면 경로 비용의 가로 기본은 0.8명/m², 여유 등급 0.5명/m² — §3.10)추정
\(C_{st}\)처리 기록 최대표 3-2 — 축제일 관측 최대 시간당 승차 (설계 용량 아님, 용량의 하한)OA-12921 관측
\(L_{st},\ Q_{st},\ W_{st}\)부하율 · 대기열 · 대기시간§3.3 보존식—
\(\sigma_{O1},\ \sigma_{O2}\)관측 오차 (상대)15% · 20%추정

표 3-2 — \(E_{st}\)·\(C_{st}\) 실측값 (명)

역E 2024E 2025E 평균비중Cst비고
여의도(5)38,28125,15931,72029.1%17,788—
샛강(9)19,01918,26818,64417.1%8,455†
여의도(9)18,44417,78718,11616.6%13,208†
마포역 도보13,78615,27714,53213.3%15,000† 보행로
국회의사당(9)9,32214,41011,86610.9%7,373†
여의나루(5)7,45413,95010,7029.8%9,577통제 20~21시
신길(1·5)2,4124,4773,4443.2%5,546† 1호선 합산
합계108,718109,328109,023100%—총량 차 0.6%

E = 축제일 승차 − 평시 토요일 중앙값(그 해 전체), 19~23시 합. † = 용량 추정(9호선·1호선 합산·보행로 — OA-12921 밖). 역별 E는 해마다 흔들리지만(여의도(5) 3.8만 ↔ 2.5만) 합계는 0.6% 차로 안정적이다 — 규모 예측이 성립하는 근거이자, 단일값 대신 2년 범위 밴드를 쓰는 이유다(§3.5 히스토리 표). 산출: data/derived/exit_shares.json.

3.3 부하율과 통제 규칙

기호로 적으면 — 부하율, 시간대를 넘는 대기열, 대기시간은

\[ L_{st}(h) = \frac{D_{st}(h)}{C_{st}}, \qquad Q_{st}(h) = \max\big(0,\ Q_{st}(h\!-\!1) + D_{st}(h) - C_{st}\big), \qquad W_{st}(h) = 60\,\frac{Q_{st}(h)}{C_{st}}\ \text{분} \]

\(Q\)는 유량 보존식이고 \(W\)는 Little 법칙(대기열 ÷ 처리율)이다 — 자유 파라미터가 없어, 수요와 용량이 맞으면 자동으로 맞는다. 통제 이월은 여의나루 \(h\in\{20,21\}\)에서 \(D=0\)으로 두고 그 도착분을 해제 후 첫 시간대(22시)에 가산하는 규칙이다. 이것은 안전측 근사다 — 공지상 통제는 20:40~21:40(60분)이라 20:00~20:40과 21:40~22:00은 열려 있지만, 2024·2025 실적은 두 해 다 19시부터 하차가 0이어서 현장이 공지보다 일찍 무정차로 전환했다. 열려 있다고 가정했다가 조기 무정차가 나면 승객이 탈 수 없는 역에서 대기하게 되므로, 모델은 두 시간을 통째로 닫고 화면 문구만 "공지 기준 20:40부터 · 예년엔 조기 무정차"로 조건부로 안내한다(2026-09-01 자체 검증 H2). 보존 검증은 상시 테스트(tests/test_model.py), 대기 분의 실측 대조는 9/5 현장 스톱워치로 하려 했으나 실측이 없어 미검증으로 남는다(§6.2).

3.4 당일 보정 α — 오늘 규모를 실시간으로 갱신

α 후보를 61개(0.33배~3배) 세워 두고, 5분마다 들어오는 관측과 "만약 오늘이 1.2배라면 이 관측이 나올 확률"을 비교해 각 후보에 점수를 매긴다. 점수 분포의 가운데가 α, 양끝(10%·90%)이 예측 밴드가 된다. 관측이 쌓일수록 밴드가 좁아진다.

α 추정은 격자 베이즈 갱신이다. 로그 등간격 61점 \(\alpha \in [1/3, 3]\) 위에서

\[ w(\alpha) \;\propto\; p_0(\alpha) \prod_{i=1}^{n} \mathcal{N}\big(y_i;\ A_i\,\alpha + B_i,\ \sigma_i\big), \qquad p_0 = \mathrm{LogNormal}(0,\ 0.25) \]

를 계산하고 \(\hat\alpha = p50\), 예측 밴드는 \([p10, p90]\)을 쓴다. 사전분포 폭 0.25는 p10/p90이 0.73/1.38로 2년 관측 밴드와 맞도록 정했다. 관측 \(y_i\)는 두 채널이다:

표 3-3 — \(\alpha\) 관측 채널 (O1·O2)

채널관측 \(y\)예측항 \(A\alpha + B\)\(\sigma\)시간 창
O1 도착측여의나루 30분 하차 증분\(A\) = 2년 평균 증분, \(B = 0\)15%~19시 (기준 증분 < 200 창은 제외)
O2 귀가측핫스팟 9역 합산 30분 승차\(A = c \cdot\) 초과(α=1)/2, \(B = c \cdot\) 평시/220%19시~ (\(c\) = 커버리지 추정)

구현: src/nowcast.py assimilate(). 사후분포가 격자 끝에 붙으면(edge_hit) 관측과 사전이 크게 어긋난다는 경고로 기록한다.
커버리지 \(c\)(핫스팟 9역 ÷ 우리 6출구)는 당일 14~17시 30분 창의 중앙값으로 추정한다. \(\alpha\)는 \(c\)에 반비례하므로 이 값 하나가 규모 전체를 흔든다 — 창이 3개 미만이거나 창끼리 (최대−최소)÷중앙값이 0.3을 넘으면 추정을 버리고 고정값 1.3을 쓴다. 실제 사용값·산포는 assimilation.coverage_basis·coverage_spread로 발행 데이터에 남긴다(2026-09-01 자체 검증 H5).

밴드가 담는 것과 담지 않는 것 (2026-09-01 자체 검증 C1). 위 \([p10, p90]\)은 α, 즉 오늘 규모의 불확실성만 담는다. 시간대 배분(\(\varphi\))·연도 간 이전·9호선 용량 추정의 오차는 들어 있지 않다. 그래서 관측이 쌓이면 밴드가 실제 오차보다 훨씬 빨리 좁아진다 — 관측 수 0·1·4·8·14·20·28건에서 밴드 상대폭은 65·33·23·17·13·11·9%인데(α=1을 정확히 가리키는 이상적 관측으로 측정), 9/5에는 관측이 약 22~24건 쌓여 화면 밴드가 ±5% 수준이 된다. 같은 모델의 교차연도 백테스트 오차는 ±42~44%다(§3.5). 30분 창 관측은 같은 군중을 이어서 재는 것이라 독립이 아닌데 모델은 독립으로 곱한다. 이것이 축소의 직접 원인이다. 실제 사례로 9/1 14:26 발행분에서는 관측 1건(하차 1·승차 0)이 α를 1.0에서 0.527로 끌어내렸고, 사후 p90(0.645)이 사전 p10(0.73)보다 낮았다 — 사전분포가 아무 역할도 못 한 것이다. 밴드에 모델 오차를 넣을지(넓지만 정직) 그대로 둘지(좁지만 방어 불가)는 열린 질문으로 남겨 두었다. (2026-09-06 결론) 밴드 자체는 그대로 두고, 읽는 법을 여기 못 박는다 — 어느 발행분이든 실제 오차의 하한은 교차연도 백테스트 오차 0.42~0.44(§3.5, 4년 9쌍 검증 38~44%)다. 9/5 본번에서 α 는 1.00(사전) → 최고 1.36(12:47, 첫 관측 직후) → 마감 0.91 [0.86~0.97] 로 움직였고 화면 밴드는 ±6% 였다(표 6-2). 근거는 저장소 redteam-20260901.md §C1.

3.5 검증 — 타임머신 시험 (백테스트)

예측 모델은 "맞는지"를 반드시 숫자로 확인해야 한다. 방법: 한 해의 데이터를 가리고, 다른 해의 데이터만으로 그 해를 예측한 뒤 실측과 비교한다. B 모드가 2026년 예측이 실제로 놓이는 조건과 같다(과거 데이터만 있음).

시험으로 비유하면, 2025년 문제지를 가리고 2024년 공부만으로 풀어 본 뒤 실제 답안과 맞춰 보는 것이다. 2026년의 우리도 과거 데이터만 쥐고 있으므로, B 모드 점수가 곧 실전 실력이다.
모드2024 오차2025 오차등급 적중률뜻
A: 그 해 데이터로 그 해 재현0.240.2182% / 100%시간 분배 로직 검증
B: 다른 해 데이터로 예측0.420.4476% / 88%2026 조건의 실전 오차

오차 = |예측−실측| 합 ÷ 실측 합 (시간대 승차가 있는 4개 역 기준). B 오차의 주범은 해마다 총 인파가 다르다는 것(여의도(5) 21시: 2024년 17.8천 ↔ 2025년 12.5천) — 이 폭을 예측 밴드로 정직하게 표시한다. 등급 적중률이 더 중요하다: 관람객이 실제로 보는 것은 숫자가 아니라 여유~심각 등급이고, 이건 B에서도 76~88% 맞는다. 이 밖에 코드가 바뀔 때마다 자동으로 도는 회귀 테스트 122건(2026-09-08 기준 — 수요 총량 보존·통제 이월·쇼 종료 입력 방어·발행 스키마·혼잡장 수치 등)이 상시 통과 상태다. 8/31에는 22건이었고, 9/1 자체 검증(§5)의 조치를 실패하는 테스트부터 쓰는 방식으로 넣으면서 45건이 됐다.

오차 지표는 시간대 절대오차 합 대 실측 합의 비다:

\[ \mathrm{err} = \frac{\sum_h \big|\hat D(h) - D_{\mathrm{obs}}(h)\big|}{\sum_h D_{\mathrm{obs}}(h)} \]

표 3-4 — 역별 오차·등급 적중 (오차 err, 괄호 = 등급 적중/시간대 수)

역A: 2024→2024A: 2025→2025B: 2025→2024B: 2024→2025
여의도(5)0.17 (3/5)0.12 (5/5)0.38 (2/5)0.44 (4/5)
여의나루(5)0.32 (1/2)0.18 (2/2)0.44 (1/2)0.54 (1/2)
신길(1·5)0.26 (5/5)0.30 (5/5)0.69 (5/5)0.51 (5/5)
마포역 도보0.38 (5/5)0.38 (5/5)0.47 (5/5)0.30 (5/5)
전체0.245 (14/17)0.214 (17/17)0.420 (13/17)0.438 (15/17)

읽는 법 — "2025→2024"는 2025년 데이터만으로 2024년을 예측한 교차 검증. 신길의 B 오차 0.69는 절대 규모가 작아(시간당 수백 명) 비율이 과장된 것으로, 등급은 5/5 전부 맞다. 여의나루 등급 분모가 2인 것은 통제로 열린 시간대가 두 개뿐이기 때문이다. 시간대별 원자료: data/derived/backtest.json.

표 3-4b — (2026-09-04 추가) 4년 교차 검증: 2022·2023 을 더한 9쌍

축제는 2000년부터 열렸는데 우리는 두 해만 썼다. 지하철 승하차(OA-12921)는 2008년부터 연도별 파일이 있고, KT 생활이동 일별 OD 는 2023-01-01 부터 있다. 그래서 2023 은 소스와 목표 양쪽에, 2022 는 유출 곡선이 없어 목표(실측)에만 더했다(2020·2021 은 코로나로 열리지 않았다). 소스 3해 × 목표 4해에서 같은 해를 뺀 9쌍이다. 여의나루 통제 시간대는 축제일 승차 실측에서 읽었다 — 2022 는 20시 한 시간대(부분 통제), 2023 은 19·20시.

목표(실측)소스(E·곡선)승차 오차초과 오차등급 적중
202220230.450.5315/19 (79%)
202220240.340.4016/19 (84%)
202220250.530.6215/19 (79%)
202320240.320.3716/18 (89%)
202320250.380.4314/18 (78%)
202420230.350.4013/17 (76%)
202420250.420.4713/17 (76%)
202520230.220.2517/17 (100%)
202520240.440.4915/17 (88%)
9쌍 요약평균 0.38 · 0.22~0.53평균 0.44 · 0.25~0.62134/161 (83%)

승차 오차 = Σ|예측 승차−실측 승차| / Σ실측 승차, 초과 오차는 평시를 뺀 초과분 기준. 4역(여의도·여의나루·신길·마포) 19~23시. 원자료 data/derived/backtest_4y.json, 계산 tools/backtest_4y.py.

결론은 둘이다. 첫째, 표본을 2쌍에서 9쌍으로 늘려도 오차는 줄지 않았다 — 위 표 3-4 의 42~44% 는 두 해의 우연이 아니라 연도 간 이전 오차의 실제 크기다. 둘째, 이 값이 C1 이 요구하는 밴드의 바닥이다. α 사후 밴드가 관측을 먹고 ±5% 로 좁아져도 화면 밴드가 이 아래로 내려갈 근거가 없다. 이 검증은 보고서용이고, 9/5 에 도는 모델과 사전 예측표는 손대지 않았다.

표 3-4a — 백테스트가 검증한 수요 비중 (출구별 실측 비중 share_mean 기준)

구분출구수요 비중왜
검증됨여의도(5) · 여의나루(5) · 신길(1·5) · 마포역 도보55.4%OA-12921에 시간대 승차가 있어 시간대별 대조가 된다
미검증여의도(9) 16.6 · 샛강(9) 17.1 · 국회의사당(9) 10.944.6%9호선은 OA-12921에 없다. 교통카드 일별 승차(OA-12914) 비례로 규모와 용량을 추정했을 뿐, 시간대 대조는 못 했다

약점을 먼저 말한다 (2026-09-01 자체 검증 H1·H3 · 부하율은 2026-09-03 정정본). 백테스트는 수요의 55.4%만 검증했고, 미검증 44.6%에 최악 후보가 몰려 있다. 사전 예측표 22시 부하율 상위는 여의나루 1.00 · 샛강 0.82 · 여의도(5) 0.66 · 국회의사당 0.65인데 2위와 4위가 검증 밖이고, 그 용량은 전부 추정값(estimated_capacity: true)이다. "샛강이 두 번째로 위험하다"는 결론의 근거는 9호선 실측이 아니라 교통카드 일별 비례 추정이라는 뜻이다. 9/5에도 9호선 역 단위 승차 관측은 없다(서울시 실시간 도시데이터의 승차는 핫스팟 합산이다) — 이 구간의 검증 재료는 CCTV 밀도와 현장 관찰뿐이다. 또 하나, 19시 여의나루는 2024·2025 두 해 모두 통제(하차 0)였는데 2026 모델은 공지(20:40~)를 따라 19시를 개방으로 둔다(부하율 0.18). 20·21시는 실제보다 보수적으로 닫으면서 19시는 이력보다 낙관적으로 여는 비대칭이다. 백테스트가 한 번도 검증하지 못한 시간대이므로 화면 문구는 "공지 기준"임을 같이 적는다.

그림 4 — 백테스트: 모델 곡선이 실측을 따라가는가. 대표로 여의도(5)역 저녁. 21시 정점의 높이·위치가 맞는다.

5k10k19시20시21시22시23시 모델 예측 실측 승차 (점선) 21시 예측 11.8k / 실측 12.5k 여의도(5)역 2025-09-27 저녁 · A 모드(시간 분배 검증) · 5역 전체 표는 저장소 backtest.json

히스토리(2024·2025 실측)가 가르쳐준 사실

확인한 사실수치모델에 어떻게 반영했나
여의나루역은 저녁에 실제로 닫힌다19~21시 하차 ≈ 0명 (2년 공통)통제 규칙 · "무정차" 표시
귀가 인파의 총 규모는 해가 바뀌어도 안정적초과 승차 합 2024년 108,718 vs 2025년 109,328 — 차이 0.6%예측 가능성의 핵심 근거. 2년 평균을 규모의 기준점으로
단, 시간대별 배분은 해마다 다르다여의도(5) 21시: 2024년 17.8천 ↔ 2025년 12.5천단일값 대신 예측 밴드로 표시
샛강·마포 방면이 생각보다 크다샛강역 축제일 승차 평시 6배 · 마포역 20~23시 10배(1.4만~1.6만 vs 평시 1.45천)수기 배정표 폐기 → 관측 비중 채택 (오차 0.34→0.24)
여의나루 통제가 풀리면 몰아서 탄다해제 직후 시간대 승차 2024년 2.3천 · 2025년 5.8천통제 중 도착분을 22시로 이월 → "22시 여의나루 최악" 결론
다리 건너 걷는 조기 귀가가 있다마포역 20시 실측 5.1천~5.8천 vs 모델 2.5천알려진 한계로 문서화 (과소 예측 방향 명시)
출발지 구성은 2년간 거의 동일구성비 차 ±0.3%p · 올 때≈갈 때 대칭 ±6%p출발지 지도를 귀가 방향 예측에 재사용
피더 승차가 도착을 1시간 선행상관 lag0 0.72 / lag1 0.99 (2년 공통)당일 조기 신호 후보 (9/6 분 단위 검증 후)
대조 기준일도 검증해야 한다2025 직전 토요일(10/4)이 추석 연휴 — 평시가 아님하루 대신 연간 토요일 중앙값으로 교정 (과정 기록)

3.6 피더 분석 — 사람들은 어디서 타고 오나

여의도에 온 사람들은 그날 어딘가의 역에서 지하철을 탔다. 서울 전 역의 승차 기록에서 "평소 토요일보다 유난히 많이 탄" 흔적을 찾으면, 관람객의 출발지를 역 단위로 되짚을 수 있다. 그 역들을 "피더(공급)역"이라 부른다.

그림 1 — 상위 피더 12곳 점·선 도식. 가운데가 여의도. 선 방향은 실제 방위, 중심에서 떨어진 거리는 지하철 소요시간(점선 링 = 10·20·30분, 추정), 원 크기는 2년간 여의도행으로 집계(귀속)된 인원. ▶ 재생을 누르면 낮 12시부터 인파 유입이 흐른다 — 유입 속도는 그 시각의 실측 승차 강도를 역별 비중으로 배분한 값(추정).

가운데 = 여의도 · 선 방향 = 실제 방위 · 중심에서 거리 = 지하철 소요시간(추정) · 원 크기 = 여의도행 인원(2년) 10분20분30분 여의도 낙성대25분 · 4,864명고속터미널12분 · 4,769명화곡14분 · 4,365명영등포구청5분 · 3,774명홍대입구15분 · 3,537명김포공항25분 · 3,484명영등포시장3분 · 3,432명목동9분 · 3,424명성수35분 · 3,339명사당18분 · 3,279명잠실30분 · 2,850명신길3분 · 2,710명

어떻게 찾았나 — 4단계

  1. 초과 승차 계산: 서울 1~8호선 전 역(253역)에서 축제일 시간대별 승차 − 평시 토요일 중앙값을 구한다. "평소에도 타는 사람"을 걷어낸 나머지.
  2. 2년 필터: 2024·2025 두 해 연속 초과가 난 역만 인정. 한 해만 튄 역은 다른 행사·우연일 수 있어 제외.
  3. KT 데이터로 제약: KT 생활이동(휴대전화 기반)이 "여의도 유입이 어느 구(區)에서 출발했는지" 총량을 준다. 역별 초과분은 그 구 총량을 넘지 않게 배분 — 역 데이터 혼자 결론 내지 않고 독립된 두 소스를 맞물린다.
  4. 교차 검증: 배분한 합계가 여의도 도착과 규모(자릿수)·시간 모양에서 일치하는지 확인.

그림 2 — 핵심 근거: 시간이 맞물린다. 피더역들의 초과 승차(주황 실선)가 먼저 움직이고, 여의도 도착(파랑 점선)이 약 1시간 뒤를 그대로 따라간다. 상관계수 0.99. 차트 아래 슬라이더로 시각을 옮기면 같은 무리가 한 시간 뒤 도착하는 짝이 커서로 보인다.

5k10k15k10시11시12시13시14시15시16시17시18시19시20시 피더역 승차 — 여의도행 몫 (탄다) 여의도 도착 초과 (내린다) 약 1시간 뒤 (r=0.99) 2025-09-27 축제일 · 승차=서울 전 역 초과분 중 여의도행 몫, 하차=여의도(5)·여의나루 하차 초과 (OA-12921)

그림 3 — 개별 역에서 보면. 영등포시장역(5호선, 여의도 3분)의 축제일 승차는 입장 시간대(12~17시)에만 평시보다 많고, 저녁이 되면 격차가 사라진다. "축제 가려고 탄 사람들"이라는 해석과 정확히 맞는 모양.

10시11시12시13시14시15시16시17시18시19시 +216+289+353 축제일 승차 평시 토요일 중앙값 (점선) 영등포시장역(5호선) 2025-09-27 승차 · 초과가 입장 시간대(12~17시)에만 나타나고 저녁엔 소멸 (OA-12921)

"이 사람들이 여의도로 갔다"고 판단한 근거 5가지

근거내용
① 시간이 맞물림피더 초과 승차 정점(17시) → 여의도 도착 정점(17~18시). 상관 0.99 (2024·2025 공통). 멀리 있는 역(성수 35분·잠실 30분)일수록 지연이 커진다 — 지하철 소요시간과 일치하는 물리적 패턴 (그림 2)
② 2년 재현같은 역들이 두 해 연속 같은 모양으로 초과. 일회성 요인이면 재현되지 않는다
③ 독립 소스 정합역 승차(교통공사)와 휴대전화 이동(KT)이라는 서로 다른 두 데이터가 출발지 구성에서 맞물린다. 배분은 KT 구 총량을 상한으로
④ 오후에만 초과초과분이 축제 입장 시간대(12~17시)에 몰리고 저녁엔 소멸 (그림 3). 통근·일상 수요라면 이런 모양이 안 나온다
⑤ 방향 대칭올 때 출발지 구성 ≈ 갈 때 도착지 구성 (구 단위 차이 ±6%p 이내, KT 실측) — 그래서 이 출발지 지도를 귀가 방향 예측에 재사용할 수 있다

역별로 뜯어본 상관 — 거리가 위상을 결정한다

읽는 법 — lag0은 "피더역 승차와 여의도 도착이 같은 시각에 함께 움직였나", lag1은 "피더역 승차가 한 시간 먼저 움직였나"다. 가까운 역(영등포시장 3분)에서 탄 사람은 곧바로 도착하니 같은 시각이 맞고, 먼 역(성수 35분)에서 탄 사람은 한 시간 뒤에 도착하니 lag1이 맞다. 실제로 그렇게 나왔다면, 이 상관은 우연이 아니라 지하철을 타고 오는 물리적 흐름이라는 뜻이다.
피더역노선귀속 인원(2년)같은 시각 r1시간 선행 r소요(추정)판정
영등포시장53,4320.860.743분근거리 — 같은 시각 동행
홍대입구23,5370.820.8115분근거리 — 같은 시각 동행
신길52,7100.810.743분근거리 — 같은 시각 동행
영등포구청2·53,7740.800.405분근거리 — 같은 시각 동행
화곡54,3650.630.5914분중거리 — 두 위상 비슷
성수23,3390.640.9535분원거리 — 1시간 선행
낙성대24,8640.660.7725분원거리 — 1시간 선행
잠실2·82,8500.460.7130분원거리 — 1시간 선행
고속터미널3·74,7690.240.6512분환승 대기로 선행 추정
목동53,4240.130.169분신호 약함 — 감시만
사당2·43,2790.040.1218분신호 약함 — 감시만
김포공항53,484−0.190.3025분공항 유동 혼입 — 제외

2년 합동 Pearson, 표본 n=20 (2년 × 10시간대). 굵은 값이 그 역의 우세 위상. 근거리 4곳은 같은 시각(0.80~0.86), 원거리 3곳은 1시간 선행(0.71~0.95)이 우세하다 — 소요시간이 길수록 선행이 커지는 순서가 그대로 재현된다(근거 ①의 역별 분해). 다만 개별 역은 표본이 작아 잡음이 크므로, 본 판단은 12곳 합산 상관(lag1 0.99)으로 하고 이 표는 위상 확인과 당일 감시 우선순위(강한 역 위주)에 쓴다. 산출: data/derived/feeder_leadlag.json.

한계도 정직하게. 초과 승차가 전부 축제 인파는 아니다 — 서울 전 역의 초과 합은 여의도 유입 규모의 1.7~3.3배로, 다른 나들이·행사가 섞여 있다. 그래서 2년 연속 필터·구 총량 상한으로 보수적으로 배분(귀속)했고, 개별 역 중엔 잡음이 큰 곳도 있다(위 표: 사당 0.04 · 목동 0.13 · 김포공항 −0.19). 분 단위 정밀 검증은 9/5 라이브 수집으로 한다.

이걸로 뭘 하나

3.7 CCTV — 걷는 속도를 실측으로

3.8 규모의 외부 검증 — 생활인구·전야제

모델의 뼈대인 "규모"가 지하철 데이터 안에서만 맞는 것일 수 있다. 그래서 지하철과 독립인 제3의 소스(서울 생활인구, OA-14991 — 통신 기반 추계)로 같은 그림이 나오는지 확인했다.

표 3-5 — 여의동 생활인구: 평시 vs 축제 당일 (19시, 명)

연도평시 토요일 (월내 중앙값)축제 당일배수
202487,764393,4034.48
202581,456359,2814.41

당일 피크(19시)가 2년 연속 같은 배수로 재현되고, 시간 곡선의 모양도 모델의 유출 곡선과 맞물린다. 지하철 승하차(교통공사) · 이동 OD(KT) · 생활인구(추계)라는 서로 독립인 세 소스가 규모에서 합의한다 — §3.6 근거 ③과 같은 삼각측량이다.

표 3-6 — 전야제(축제 전일 금) 여의나루 하차: 17~23시 합 (명)

연도전야제평시 금 중앙값 (51주)배수참고: 여의도역
20248,5104,5211.880.94
20257,2543,8921.861.08

전야제 인파는 여의나루 하차 기준 평시 금요일의 1.9배 수준에서 2년 연속 재현되고, 여의도역은 평시 수준에 머문다. 이 수치를 9/4 전야 관측의 정상 신호 기준으로 삼았다 — 그날 관측이 이 규모면 파이프라인이 맞게 돌고 있는 것이다. (2026-09-06 정정) 9/4 전야 관측으로는 이 기준을 판정하지 못했다 — 판정 불가로 기록한다. 파이프라인 정상 여부는 대신 발행 주기·재기동 0·α 갱신으로 확인했다(§6.3). 한편 생활인구는 전야제를 동 단위에서 1.00~1.09배로만 본다 — 여의동의 평일 직장 인구에 희석되기 때문이다. 작은 행사는 역 단위 관측이 동 단위 추계보다 민감하다는, 우리 관측 설계(§3.4 O1·O2)의 방증이다.

산출: data/derived/lifepop_yeouido.json · data/derived/eve_rehearsal.json.

3.9 혼잡장 — 관측 30여 점을 한 장의 면으로 (2026-09-01 추가)

CCTV 23대와 서울시 구역 등급은 지도 위의 점 몇 십 개다. 관람객이 보고 싶은 것은 점이 아니라 "내가 서 있는 여기, 그리고 가려는 길은 얼마나 붐비나"라는 면이다. 점 사이를 메우는 가장 정직한 방법은 "가까운 관측일수록, 믿을 만한 관측일수록 더 믿고, 관측이 없는 곳은 모른다고 말하는 것"이다. 이것을 수학으로 쓴 게 가우시안 과정 회귀다. 색은 혼잡 정도, 진하기는 확신 정도로 그린다.

지점 \(x\)의 혼잡 지수 \(C(x)\in[0,1]\)를 관측 \(y_i = C(x_i) + \varepsilon_i,\ \varepsilon_i\sim\mathcal N(0,\sigma_i^2)\)에서 복원한다. 사후 평균과 분산은 표준 식이다:

\[ \mu(x) = m_0 + \mathbf{k}(x)^{\!\top}(\mathbf K + \boldsymbol\Sigma)^{-1}(\mathbf y - m_0), \qquad s^2(x) = k(x,x) - \mathbf{k}(x)^{\!\top}(\mathbf K + \boldsymbol\Sigma)^{-1}\mathbf{k}(x) \]

커널은 두 길이척도의 Matérn 3/2 합이다 — 카메라 시야 규모(150m)와 구역 규모(1,200m)를 따로 잡는다:

\[ k(r) = \sum_{\ell\in\{150,\,1200\}} v_\ell\,(1+a_\ell)\,e^{-a_\ell}, \qquad a_\ell = \frac{\sqrt3\, r}{\ell}, \qquad v_{150}=v_{1200}=0.125 \]

RBF(가우시안) 커널은 무한 번 미분 가능해 인파의 장으로는 지나치게 매끄럽다. Matérn 3/2는 한 번 미분 가능한 거친 면을 허용한다(Rasmussen & Williams 2006, §8.2 [6]). 길이척도 150m의 근거 후보: 관측 주기 5분 동안 밀도 3명/m²의 보행 이동 ≈ 99m(§3.2 속도식). 구현 docs/app/field.js · 커널 상수 docs/index.html APF_KERNEL.

표 3-7 — 관측 노이즈 \(\sigma_i\): 신뢰도를 배제가 아니라 폭으로

관측 종류기본 \(\sigma\)확대 규칙 (공통)
밀도 보정 CCTV (현재 0대 — 2026-09-02 정정)0.10신뢰도 낮음 ×2.5 · 플래그 하나당 ×1.3 · 경과 \(t\)분에 \(\sqrt{1+(t/15)^2}\)배(15분에 √2배, 2시간에 8배 — 오래된 관측은 스스로 사전으로 돌아간다) · 같은 관측을 \(n\)곳에 복제하면 ×√n(정보량을 1건으로 유지)
구역 등급 (서울시 citydata, 여의도한강공원)0.20
미보정 CCTV (23대, 판독 등급 여유·주의·경계·심각 → 0.3·0.7·0.9·1.0)0.30

2026-09-02 정정 — 밀도 보정 5대를 등급 판독으로 되돌렸다. ROI 면적(roi_m2 9,000~20,000m²)이 도로·교량 전체 면적이라 720×480 원거리 고각에서 군중 계수 모델이 셀 수 있는 규모와 두 자릿수 어긋났다. 「주의」(3명/m²)에 닿으려면 판독 인원 27,000~60,000 이 필요한데 8/29 야간 실측 최대는 8.4~29.3 이었다. 그대로 두면 이 5대가 언제나 「여유」를 가장 낮은 \(\sigma\) 로 주장해 혼잡장을 끌어내린다. 자체 점검에서 발견해 되돌렸고, 면적 산출 근거는 남겨 두어 근경 부분 ROI 로 재측정하면 복원된다.

사전 평균 \(m_0\)은 고정값이 아니라 관측 수준에서 잡는다(경험적 베이즈) — 관측마다 정보 가중치 \(w_i = k(0)/(k(0)+\sigma_i^2)\)를 주고, 기본값 0.3을 가짜 관측 3건만큼 섞어 가중평균한다. 고정 0.3이면 주변이 다 붐벼도 사후가 "여유"로 끌려간다 — 예컨대 미보정 카메라(σ 0.30)가 0.70을 봐도 사후는 0.59로 여유 밴드에 주저앉아 마커와 면이 어긋난다. \(w_i<0.10\)인 관측은 장에 사실상 반영되지 않고, 화면의 마커·배지도 같은 기준으로 "거의 반영 안 됨"을 표시한다. 격자는 200×156, 색은 \(\mu\), 불투명도는 확신도 \(1 - s/s_{\max}\)(\(s_{\max}=0.5\), 0.15에서 포화)다. 관측이 없는 곳은 색이 옅어질 뿐 회색으로 바뀌지 않는다 — 9/1 회색 혼합을 걷어낸 것은 화면 가독성을 위한 선택이고, "모르는 곳"이 색에서 사라져 낙관 쪽으로 읽힐 수 있다는 점은 인정한다(§3.11).

혼잡 지수는 밀도 5명/m²를 1로 두는 선형 사상 \(C = \min(1,\rho/5)\)이고, 서울시 4단계 등급은 여유 0 · 보통 ⅓ · 약간 붐빔 ⅔ · 붐빔 1로 놓았다. 이 사상과 길이척도·σ·가짜 관측 3건은 전부 가정이다. (2026-09-05 정정) 서울시 구역 등급 관측의 나이에서 API 발행 시차 28분(9/4~9/5 전 틱 실측, 28.8분 일정)을 뺀다 — 전엔 시차를 낡음으로 깎아 σ 가 1.1 로 불어 「붐빔」이 장에서 사실상 무시됐다. 같은 층을 관람객 화면에도 넣어 두 화면의 장이 같은 관측으로 만들어진다. 카메라가 있는 자리는 카메라(σ 0.3)가 이긴다. 9/5 관측이 쌓이면 관측 하나를 빼고 나머지로 그 자리를 맞히는 교차검증(LOO)으로 교체한다(다음 단계, §6.4). 거리가 유클리드라 한강 건너로 번지는 문제가 있고, 보행망 거리 커널이 답이지만 다음 단계 과제다. 검증은 tests/field_spec.mjs가 배포 중인 field.js를 node로 그대로 읽어 22개 단언(커널 단조성·Cholesky 재구성·σ 순서·경험적 베이즈 사전)을 확인한다 — 그래서 field.js는 DOM을 만지지 않는 순수 함수로 유지한다. \(\mathbf K+\boldsymbol\Sigma\)가 양의 정부호가 아니면 지터를 키워 6회 재시도하고, 그래도 안 되면 사전으로 폴백하며 ok:false를 남긴다.

3.10 혼잡 회피 보행 경로 — 혼잡장 위에서 길을 찾는다 (2026-09-01 추가)

"어느 역으로 가라"까지 정했으면 다음 질문은 "어느 길로"다. 지도앱은 최단 거리를 고르지만 축제 밤에는 최단 경로가 가장 붐비는 길이다. 우리는 길마다 지금의 혼잡을 비용에 얹어, 조금 돌더라도 덜 막히는 길을 고른다.

보행망은 OpenStreetMap에서 여의도 일대(카메라 23대를 감싸는 범위)의 도로 기하를 내려받아 만든 그래프다 — 노드 9,219 · 간선 26,042 · 총 연장 1,486km(방향별) · 파일 5.95MB(gzip 전송 775KB). 경로를 처음 누를 때만 내려받는다(지연 로드). 간선 비용은 길이에 혼잡 위험을 곱한 값이고, 위험은 간선 중점에서 읽은 혼잡장 평균 \(\mu\)다:

\[ c(e) = \frac{\max(0.1,\ m_e)}{v(\rho_{\mathrm{eff}})},\qquad \rho_{\mathrm{eff}} = \kappa\,\rho(\mu(x_e)) + (1-\kappa)\,\rho_{\mathrm{street}},\quad \kappa = \min\!\Big(1,\ \tfrac{1-\sigma(x_e)/\sigma_{\max}}{0.15}\Big) \]

(2026-09-04 변경) 전에는 \( c(e) = \max(0.1, m_e)\,(1 + 1.25\,\mu) \) 였다. 가중치 1.25는 출처가 없었고, 최소화 대상이 사람이 신경 쓰는 값도 아니었다. 더 나쁜 것은 경로는 벌점으로 고르면서 화면의 소요 시간은 67m/분 고정으로 따로 계산해 둘이 따로 놀았다는 점이다. 지금 비용은 그 간선을 걷는 데 걸리는 시간이다 — \(v\)는 §3.2와 같은 Kladek 속도식이고 \(\rho\)는 혼잡장 값을 등급 사다리(여유 1.5 · 주의 3.0 · 경계 4.0 · 심각 5.0명/m²)로 되돌린 밀도다. (같은 날 낮 정정) 밀도를 혼잡장 값 그대로 쓰지 않고 장의 확신도 \(\kappa\)(σ에서, §3.9)로 관측 밀도와 가로 기본 \(\rho_{\mathrm{street}}=1.5\)명/m²를 섞는다 — 본 곳은 본 대로, 못 본 곳은 보통 가로. 그대로 쓰면 GP 사전(0.3)과 σ 0.3 수축이 카메라 없는 이면도로까지 2.25명/m²로 읽어 어느 경로든 위험이 같아지고, 균일 배율은 최소경로를 바꾸지 않아 우회가 사라졌다. 경로를 고르는 비용과 화면의 소요 시간이 같은 식이다 — 잠깐 둘을 나눴더니(비용은 장 전체, 표시는 처음 300m) "더 돌게 하고 더 느리다고 말하는" 모순이 났다. 실측 확인(2026-09-04): 실제 관측(15대)에서 6개 목적지 중 2곳이 60여 m 돌고, 교량·광장 심각 배치에서는 4곳이 돈다 — 신길역 +1,278m에 22분 단축, 국회의사당역 +312m에 12분 단축. (2026-09-06 정정) 이 가정 배치의 분 수는 9/5 의 ÷1.4 정정(아래) 뒤 다시 계산해 이벤트광장→여의도역 최단거리 45분 vs 혼잡 회피 21분이 됐다(docs/deck/route_demo.json). 9/5 실측 — M14 부분 닫힘. 9/4 실데이터에서는 장이 매끈해 경로가 바뀌지 않았지만(대장 M14), 9/5 발행 스냅샷 6개로 같은 계산을 다시 돌리면(tools/route_real.mjs) 6시각 × 목적지 6곳 = 36건 중 11건에서 최단시간 경로가 최단거리 경로와 달랐다. 쇼 종료 직후 21:31·21:36 에 4/6(여의도역 +196m·4분 단축 · 샛강 +239m·4분 · 신길 +69m·12분), 22:06 신길 +360m·12분, 23:02 신길 +87m·6분. 20:05 는 0/6 — 공원 전체가 붐빔이라 어느 길이나 같이 느렸다. 오늘 코드로 그날 관측을 다시 돌린 값이라 22:31 이전 화면이 실제로 그린 분 수와는 배율이 다르다(경로 선택은 배율에 거의 무관). 구현 docs/app/field.js blendSeconds(), 두 화면이 같이 쓴다. 속도식이 src/nowcast.py kladek()와 같은 값인지는 테스트가 대조한다(tests/test_routing.py).

탐색은 A*(이진 힙, 직선거리 휴리스틱)를 두 단계로 돈다. 1단계는 보행로·횡단보도·보행자 도로만 허용한 부분 그래프(출발·도착 180m 안에 같은 연결 성분이 있을 때), 2단계는 자동차전용·진입금지를 뺀 OSM 전체다. 화면에는 어느 단계로 찾았는지 표시한다. 브라우저 비용(맥 데스크톱, Node/V8): JSON 파싱 22ms · 그래프 구성 17ms · A* 5ms. 모바일은 몇 배로 본다.

도보 소요 시간은 §3.2와 같은 Kladek 속도식이다 — 처음 300m는 관람구역 밀도 3.0명/m², 이후 가로 1.5명/m². 경로탐색 모듈의 초기 구현은 상수 75m/분(1.25m/s)이어서 같은 1.47km 경로가 20분으로 나왔고, §3.2 기준으로는 39분이다(2026-09-03 정정 — 경로 길이를 1.82km 로 잡아 24분·47분으로 적었던 것을 재현값 1.47km 기준으로 고친다. 2배 차이라는 결론은 그대로다). 2배 차이는 원본이 축제 밤의 밀도를 넣지 않아서 생긴다. 관람객 화면(docs/go.html)은 그때 두 값 중 어느 쪽도 아닌 67m/분을 썼다 — 같은 경로가 22분으로 나왔다. (2026-09-04 결정) 두 화면 모두 위 \(c(e)\)의 구간별 시간을 그대로 표시한다. 같은 경로가 실제 관측(한산한 아침)에서 49분, 관측이 없으면 가로 기본(1.5명/m²)으로 30분이다. 세 값 사이의 선택은 이것으로 닫혔다. (2026-09-05 정정) 화면이 쓰는 밀도 사다리에서 여유 1.5 → 0.5명/m²(4.7km/h), 못 본 길의 가로 기본 1.5 → 0.8명/m²(4.2km/h)로 낮췄다. 1.5명/m² 는 Kladek 식으로 2.9km/h 라 한산한 새벽에도 같은 1.47km 가 32분으로 읽혔다 — 지금은 관측이 없으면 21분, 여유 관측이면 19분이다. 주의 이상은 그대로라 붐비면 여전히 느려진다. 모델의 도달 지연(§3.2, 첫 300m 3.0·이후 1.5)은 바꾸지 않았다 — 화면 시간과 모델 지연의 가로 밀도가 이제 다르다. 두 값 모두 가정이고 9/5 실측 뒤 다시 본다. (2026-09-05 22:30 정정, 당일) 걷는 시간 = Weidmann 시간 ÷ 1.4(속도 ×1.4, 자유보행 1.34 m/s 상한). Weidmann 식은 3.5명/m² 에서 0.8 km/h 까지 떨어지는데, 서울시 「붐빔」 배경 관측이 깔린 광장 일대(u≈0.8)에서 여의도역 1.4 km 가 72분으로 읽혔다 → 51분. 22:20 에 먼저 넣은 속도 바닥 2.2 km/h 는 주의·경계·심각의 시간 차를 지워 10분 만에 철회했다. 1.4 는 가정값이고 속도식 자체(§3.2, 모델 지연)는 그대로다. (2026-09-04 정정) 같은 날 관람객 화면을 v2 로 바꿨다. 경로는 두 화면이 같은 보행망 A* 와 같은 비용 \(c(e)\) 를 쓴다 — 관람객 화면은 첫 답을 늦추지 않으려고 보행망(5.9MB, 전송 0.8MB)을 첫 화면 뒤에 받고, 받기 전에는 역별 근사 경유점(직선합 ×1.15) 위에서 같은 속도식으로 시간만 세며 화면에 「근사」라고 적는다(같은 날 오전 한때 근사만 쓰기로 했다가, 근사 직선은 붐비는 곳을 돌아가지 못해 되돌렸다). 관람객 화면의 추천 1위는 부하율이 아니라 도보 + 대기 시간 오름차순이다(운영 화면·이 보고서의 순위는 부하율 그대로). 혼잡 가중 1.25는 출처 없는 추정치였다. (2026-09-03 정정) 이 보행망의 우회비는 이벤트광장→여의도역 직선 1,120m 대 경로 1,471m = 1.31배이고, 모델의 우회계수 1.3(§3.2)과 거의 같다. 초기 구현 문서(routing/README.md)가 적은 1,820m·1.64배는 지금 보행망에서 재현되지 않는다 — 마포대교 보완 간선과 태그 필터를 넣고 빼 본 네 조합이 모두 1,471m 였고, 같은 계산이 이벤트광장→마포역 1,907m(9/2 실측)는 정확히 재현한다. 따라서 "실측이 모델보다 크므로 보정이 필요하다"는 앞선 서술은 철회한다. 우회계수 1.3은 이 한 구간에서 잘 맞지만 표본이 하나뿐이라, 9/5 이후 여러 구간으로 다시 볼 항목으로 남긴다. 지도 데이터는 © OpenStreetMap contributors, ODbL. 구축·편입 기록: 저장소 routing/README.md.

3.11 버린 결정 — 과정의 기록

맞는 답만큼 중요한 게 "왜 그 길로 안 갔나"다. 심사 항목("만든 과정")이기도 하다.

버린 것왜 버렸나대신 택한 것
ML 학습 모델학습 표본이 2일 — 외우기가 될 뿐실측을 기준점 삼은 구조 모델
방향→역 수기 배정표실측과 3~7배 어긋남 (샛강을 3.5배 적게 예측)관측 초과 승차 비중 — 백테스트 오차 0.34→0.24
평시 기준을 상위 10%로환승 큰 역이 기준에서 밀려나는 왜곡 발견토요일 중앙값
입자 필터(난수 시뮬레이션)추정할 값이 α 하나 — 난수가 필요 없음결정론적 격자 (테스트 가능)
피더 신호를 실전 모델에 직결9/5 전에 분 단위 검증이 불가능관측만 하고 사후 검증 → 확장 기능
가상 설문 1,000건 (생성 응답)실측(하차 0명 · 승차 12~17배)이 이미 있고, 생성 응답은 실측보다 근거 등급이 낮다실측과 보도로 페인포인트 절 구성 — 8/31 드랍
관측마다 가우시안을 얹어 더하는 혼잡장 (반경 σ=220m, 초기 구현)신뢰도 낮은 관측을 버리거나 그대로 믿는 양자택일. 불확실성을 내놓지 못함가우시안 과정 회귀 — 신뢰도를 노이즈 σ로, 불확실성을 불투명도로 (§3.9)
불확실한 곳을 회색으로 섞어 그리기화면이 탁해진다는 가독성 판단. 대신 낙관 편향 가능성을 §3.9에 명시불투명도 + "관측 없음" 배지 + 한계 문구 (되돌리기는 1줄)
스테일 CCTV 판독을 시간 기준으로 잘라내기12시간 컷 안쪽의 3시간 전 판독이 "지금"으로 그려진다경과 시간에 따라 σ 확대 — 사후가 저절로 사전으로 돌아간다 (§3.9)
도보 소요 상수 75m/분 (초기 구현)축제 밤 밀도를 무시해 §3.2와 2배 어긋남밀도별 속도식 → 구간별 걷는 시간 (§3.10 에서 닫음)

4아키텍처 — 전체가 어떻게 맞물리나

4.1 3층 구조

핵심 설계 원칙: 아래층이 죽어도 위층이 산다. 라이브 수집이 전부 실패해도 사전 예측표가 있고, 인터넷이 끊겨도 기기에 저장된 마지막 화면이 있다.

도식 읽는 법: [사전층]의 산출물을 [당일층]이 보정하고, 그 결과를 [화면층]이 보여준다. 당일층이 멈추면 화면층은 사전층의 예측표를 그대로 쓴다.

[사전층]  행사 전에 완성 — 라이브 없이도 서비스 성립
  data/raw (KT 이동·승하차·교통카드 원본)
    ├─ baseline.py ──────▶ 유출 곡선·역별 처리용량
    ├─ exit_shares.py ───▶ 출구별 실측 인파 E_st  ← 모델의 뼈대
    ├─ line9_capacity.py ▶ 9호선 용량 추정
    └─ backtrack.py ─────▶ 출발역 역추적 + 2026 사전 예측표
         └─ backtest.py ─▶ 타임머신 검증 (§3.5 표)

[당일층]  9/4·9/5 — run_all.sh 가 프로세스 3개를 띄우고 워치독이 지킨다
  ① collector_api.py ── 도시데이터 5분 (여의도 3곳 상시 · 피더 12곳 12~19시)
                        + 실시간 지하철 도착 17~23시 ──────────────┐
  ② collector_cctv.py ─ TOPIS 23대, 60초 (프레임 미저장 · 집계만) ─┼─▶ ③ nowcast.py ─▶ publish.py
  쇼 종료 실시각 (현장 기입 · 형식·범위 검사) ─────────────────────┘   5분 루프 · α 갱신·통제 이월·보행 지연
     워치독  30초마다 생존 확인·재기동 · 수집 로그 10분 정체 경고 · 10분에 5회 넘게 죽으면 300초 간격
     발행    내용이 같으면 커밋 안 함 · push 실패 시 pull --rebase 후 재시도, 충돌이면 abort + 경보
             연속 실패 횟수는 data/live/publish_fail_streak ──▶ GitHub Pages

[화면층]  정적 웹페이지 (서버 없음) — 출구 랭킹 · 지도(혼잡장 + 회피 경로, MapLibre) · CCTV 카드
  라이브 최신이면 "LIVE" · 15분 넘게 안 오면 "오래됨" · 아예 없으면 사전 예측표
  + 기기 저장(localStorage)으로 오프라인 폴백 · 지도 라이브러리·WebGL 실패 시 안내 문구로 폴백

당일층의 워치독·발행 방어·입력 검사는 9/1 자체 검증(§5, C2~C5)의 조치다. 9/2 저녁 30분 시험 운전이 이 조치들이 실제 수집기와 함께 도는 첫 실행이었고(고장 주입으로 워치독 재기동 1초 확인), 9/4 17:08 → 9/6 01:01 의 32시간 본번 운전에서 재기동 1회(계획된 간격 전환) · 크래시 0 · 슬립 공백 0 으로 지났다(§6.1).

4.2 데이터 소스 — 전부 공개·합법 데이터

소스어디에 쓰나비고
KT 생활이동 OD (서울시 공개)인파의 시간 곡선·출발지 구성축제일 2일 + 대조용 토요일
서울교통공사 시간대별 승하차 (OA-12921)출구별 실측 인파·용량·검증1~8호선, 2개년 전체
교통카드 일별 승하차 (OA-12914)9호선 규모 (시간대 데이터가 없어 일별로)월 단위 파일
서울 실시간 도시데이터 (citydata)당일 α 관측·피더 감시5분 주기, API 키 3개로 호출량 분산
TOPIS 교통 CCTV보행 밀도 → 걷는 속도 보정 · 혼잡장 관측23대(720p 9대), 등급 판독(밀도 보정은 9/2 보류), 프레임 미저장
2026 공식 공지 (주최측)통제 규칙·쇼 시간표여의나루 20:40~21:40 등
OSM 지도·교량 공식 제원CCTV 관심영역 면적추정 표기
OSM 보행망 (Overpass)혼잡 회피 경로탐색 그래프노드 9,219 · 간선 26,042 · ODbL

원칙: 모든 숫자에 출처·기준일을 남기고, 추정치는 "추정"으로 표기한다. 빈칸을 지어내지 않는다. 데이터셋 ID·링크·사용 범위는 §8에 상세히 정리했다.

4.3 2026 예측 미리보기 (사전 예측표, 2년 평균 [예측 범위])

시각붐비는 곳추천
21시
쇼 직후
샛강 0.66 주의 · 여의도(5) 0.58 주의 · 여의나루 통제 중신길 0.16 여유 · 마포대교 도보 0.24 여유
22시
통제 해제 후
여의나루 1.00 [0.65~1.41] 심각 · 샛강 0.82 경계신길 0.27 여유 · 마포대교 도보 0.38 여유
직관과 다른 발견 둘. ① 쇼 직후 최악은 제일 큰 여의도(5)역이 아니라 샛강역(9호선인데 용량이 작다). ② 여의나루는 통제가 풀리는 22시가 가장 위험하다 — 기다리던 사람들이 한꺼번에 몰리기 때문. 이 두 가지가 "지도앱은 절대 못 알려주는" 우리 서비스의 핵심 정보다.

표의 숫자가 뜻하는 것 (2026-09-01 자체 검증 H4·M1 · 2026-09-03 수치 정정). 헤드라인 부하율은 2024·2025를 각각 계산해 평균한 값이다. 여의나루 22시 1.00은 2024년 0.72와 2025년 1.28의 평균이고, 화면의 대기 8분은 0분과 17분의 평균이다 — 부하율 1.00이면 대기가 사실상 0(0.2분)이어야 하므로 두 숫자는 어느 한 해의 상태가 아니다. 사실 그대로 말하면 "2년 중 1년은 용량을 28% 넘겼다"이고, 범위 [0.65~1.41]이 그 정보를 담는다. (2026-09-03 정정) 이 문단은 9/2까지 같은 자리를 0.91 [0.57~1.30]·대기 6분·"19% 초과"로 적었다. 부하율의 분자에 평시 토요일 승차가 빠져 있었기 때문이다(결함 H9) — 분모인 처리용량은 축제일 총 승차의 시간당 최댓값이라 분자도 평시 + 초과여야 단위가 맞는다. 고친 뒤 여의나루 22시는 경계에서 심각으로 올라갔고, 21시 여의도(9)·국회의사당은 0.50 동률이 됐다. 결론의 방향(여의나루 22시가 최악, 2년 중 1년은 용량 초과)은 그대로다. 또 하나의 한계: 순위는 선택한 시각의 부하율로만 매긴다. 21시 1·2위인 신길(도보 58분)·마포역 도보(43분)는 21시에 출발하면 22시에 닿는데 보여 주는 값은 21시 것이다. 도달 지연 \(\tau_{st}\)를 수요 배분에는 쓰면서 랭킹에는 아직 쓰지 않는다. "지금 출발 → 도착 시각 기준" 정렬은 다음 단계 개선안이다(§6.4).

(2026-09-06 실측 대조) 9/5 화면의 하루 최대 부하는 여의나루 22시 0.93(재개통 직후) · 샛강 0.85 였고 등급은 「경계」까지, 「심각」은 없었다. 사전표의 여의나루 22시 1.00 [0.65~1.41] 안에 들어온다. 실제 통제는 공지(20:40~21:40)가 아니라 18:10~22:05 였다 — 19시 통제를 당일 19:13 에 규칙에 추가했다(H3 확인, §6.1).

5자체 적대적 검증 — 우리 것을 먼저 깨 봤다

9/1, 실전 나흘 전에 공격자 관점으로 모델·파이프라인·대시보드를 깬 것을 시작으로, 실전 뒤까지 7회차 39건(치명 6 · 높음 10 · 중간 16 · 낮음 7, 철회 1)을 등재했다. 1~2회차는 코드를 읽었고, 4회차는 다른 모델(Codex)에게 전체 diff 를 주고 설계를 공격시켰으며, 3·5·6·7회차는 실행(시험 운전·야간 연속 운전·본번)에서 찾았다. 치명·높음은 전부 조치했고(C1 은 서술로 닫음), 중간·낮음의 보류 판단은 결함 대장에 근거와 함께 남겼다. 방법은 Claude Code에 "이 저장소를 깨라"는 공격 프롬프트를 주고, 모든 지적에 파일:줄 또는 데이터 파일로 재현 가능한 근거를 요구한 것이다.

시험 전날 친구에게 내 답안지를 주고 "틀린 데 다 찾아줘"라고 부탁하는 것과 같다. 남이 찾기 전에 우리가 먼저 찾으면, 고칠 수 있는 것은 고치고 못 고치는 것은 "알고 있다"고 말할 수 있다.

5.1 먼저, 깨지지 않은 것

공격이 통하지 않은 곳도 기록한다 — 같은 곳을 다시 파지 않기 위한 기록이다. API 키 유출(추적 파일·라이브 데이터·로그 전수 검색, 히트 0) · 오류 로그로 키가 새는지(가짜 키로 실제 호출, 예외 문자열에 키 없음) · HTML 이스케이프(API 유래 문자열 17곳 전부 적용) · 표 3-2·3-4·3-5·3-6 수치(원본 JSON에서 역산, 전부 일치) · KaTeX 렌더(3개 폭에서 DOM 계측, 에러 0·넘침 0) · 스테일·오프라인 폴백(3일 지난 데이터로 렌더, 사전 예측표로 정상 강등) · 지도 실패(라이브러리·WebGL 차단, 안내 문구 폴백).

5.2 결함 39건 — 7회차

표 5-1 — 회차별 발견과 처리 (2026-09-08 기준)

회차언제 · 방법신규무엇을 찾았나 · 처리
19/1 · 코드 읽기18C1~C5 · H1~H5 · M1~M7 · L1 — 즉시 5건 + H2·H5 조치(TDD), 4건 보고서에 명시. 상세 표 5-1b
29/2 · 미감사 파일(collector_cctv.py·field.js)8H6 CCTV 밀도 보정 스케일(면적이 두 자릿수 어긋나 보정 카메라 0대로 보류) · H7 α 기준선 시프트 누락 · M8 배경차분 일몰·정지 군중(부분) · M9·M10 조치 · L2~L4(자정 넘김·단위 테스트). H8(API 쿼터)은 철회
39/2 저녁 · 30분 시험 운전 + 고장 주입2M11 관측 전 예측에 prior 표식 없음 · L5 Ctrl+C 뒤 정지까지 30초 — 당일 조치. L2·C3 닫힘(워치독 재기동 1초)
49/2 밤 · Codex 교차검증(다른 모델이 전체 diff 공격)3H9 부하율 분자에 평시 승차 누락 → 여의나루 22시 경계→심각, 순위가 바뀜(§4.3) · H10 수집 끊김이 화면에 안 보임 → 표식·워치독·화면 3층 · M12 보류
59/3 · 테스트런(키 무효화로 끊김 재현)2H11 관람객 화면 지도 바닥이 신설 이후 안 그려짐(래스터 타일 404, 휴대폰으로 발견) · M13 우회비 1.64배가 재현 안 됨 → 1.31배로 정정, 보정 재료 서술 철회(§3.10)
69/3~4 · 야간 연속 운전3C6 뚜껑을 덮으면 맥이 자고 수집·워치독이 같이 멈춘다(caffeinate 무력) → 운전 절차에 반영 · L6 예약 종료 스크립트의 zsh 단어 분리 · M14 실측 장이 매끈해 경로가 안 바뀜 → 9/5 부분 닫힘(§3.10)
79/5 · 본번 실전3M15 유출 꼬리 과소(보류, §6.2) · M16 API 발행 시차 28.8분을 낡음으로 셈 → 당일 조치 · L7 자정 CCTV 빈 프레임

결함 대장은 저장소 redteam-20260901.md 하나이며 상태가 바뀌면 그 문서가 먼저다. 덱·보고서의 건수는 tools/deck_data.py 가 대장에서 세고 테스트가 대조한다.

표 5-1b — 1회차(9/1) 결함 18건과 처리

#등급무엇이 깨지나처리
C1치명α 사후분포 과신 — 관측 1건이 α를 1.0→0.53으로 끌고, 관측 20건이면 밴드가 ±5%로 붕괴(백테스트 오차는 ±42%). (2026-09-04 추가) 2022·2023 을 더한 4년 9쌍 교차 검증에서도 승차 오차 평균 38%(22~53%)·초과 44% — 표본 우연이 아니다(표 3-4b)서술로 닫음(9/6) — 밴드는 그대로, 실제 오차 하한 0.42~0.44 를 §3.4 상자에 못 박음. 본번 α 궤적은 표 6-2
C2치명발행 실패를 아무도 모른다 — 내용이 같아도 시각만 바뀌어 매 틱 커밋, push 실패는 무시✅ 내용 비교(generated 제외) · 실패 시 rebase 후 재시도 · 연속 실패 횟수 파일 + 경보
C3치명수집기가 죽으면 재시작도 경보도 없다✅ run_all.sh 워치독 — 30초 생존 확인·재기동 · 10분 정체 경고 · 시작 전 .env 키 점검
C4치명쇼 종료 시각 기입 — 인자·파일·기본값 세 경로가 전부 취약하고 기입 절차 미정✅ 형식·범위(19:00~24:00) 검사, 오류는 크래시 대신 사용법 출력 · planned_invalid_file 표기. 본번 21:27 기입으로 한 번 작동(§6.1)
C5치명API 실패 원인이 로그에서 구분되지 않는다(키 만료·쿼터·장애가 같은 예외)✅ 비-JSON 응답 원문 200자를 예외에 · 로그 기록 전 키 값 제거
H1높음백테스트가 수요의 55.4%만 검증 — 미검증 44.6%(9호선 3역)에 최악 후보 샛강·국회가 있다보고서에 명시 — 표 3-4a로 승격
H2높음통제창 20:40~21:40(60분)을 두 시간으로 반올림✅ 안전측 근사임을 §3.3에 명시 · 화면 문구를 시간대별로 분기(20시 "공지 기준 · 예년엔 조기 무정차", 21시 "21:40 해제 예정")
H3높음19시 여의나루 — 2년 다 통제였는데 2026은 개방(0.18). 검증 근거 0에 낙관보고서에 명시 — §3.5에 비대칭 명시
H4높음사전 예측표가 연도 평균 — 부하율 1.00에 대기 8분이라는 자기모순 조합 (9/1 발견 당시 0.91·6분, 9/3 H9 정정 반영)보고서에 명시 — §4.3에 "2년 중 1년은 초과" 승격
H5높음커버리지 c 하나가 α를 최대 3.75배 흔든다 · 문서(14~17시)와 코드(14~16시) 불일치⚠️ 부분 — 창을 14~17시로 일치 · 산포 가드((최대−최소)÷중앙값 > 0.3이면 고정값) · coverage_spread 발행. 클램프 (0.8, 3.0)은 좁힐 근거가 없어 그대로
M1중간순위가 부하율만 본다 — 도달 시간이 랭킹에 없다(21시 1위 신길은 도보 58분)보고서에 명시 — §4.3 한계 명시. 도착 시각 기준 정렬은 다음 단계
M2중간데이터가 비면 어느 출구든 "여의나루 통제" 문구로 표시미조치 — 보류(대장)
M3중간CCTV 패널만 스테일 가드가 없다 — 8/29 판독이 "지금"으로⚠️ 혼잡장은 σ 확대로 해결(§3.9) · CCTV 카드는 미조치
M4중간시각 판단이 전부 뷰어 기기 시계 기준미조치 — 다음 단계
M5중간타일 서버만 죽으면 빈 배경에 마커만 남고 폴백이 없다미조치 — 다음 단계
M6중간사전 모드의 α=1.00이 측정값처럼 보인다미조치 — 보류(대장)
M7중간수집은 하는데 화면에 없는 신호(edge_hit 경보 · 실시간 통제 메시지)미조치 — 다음 단계
L1낮음표 3-3 번호 누락✅

5.3 조치는 실패하는 테스트부터

즉시 5건과 H2·H5는 결함을 재현하는 테스트를 먼저 써서 실패를 확인한 뒤 고쳤다. 신규 테스트 15건 중 13건이 그렇게 빨강→초록을 거쳤고, 2건은 기존 동작을 굳히는 회귀 가드라 처음부터 통과했다. 테스트는 26 → 41 → 45건이 됐고(§3.5), 이후 회차와 기능을 거쳐 9/8 기준 122건이다. 조치 자체가 만든 새 실패 모드 3건도 같이 닫았다 — 고치기 전에는 없던 위험이라 남겨 두면 순손실이다.

표 5-2 — 조치가 스스로 만든 실패 모드

#새로 생긴 위험조치
C2-apull --rebase가 충돌하면 저장소가 rebase 상태로 묶여 이후 모든 틱의 커밋이 실패한다(두 기기가 같은 파일을 고쳤을 때)실패 시 rebase --abort로 되돌리고 경보만 올린다. 자동 병합은 하지 않는다(사람이 판단할 일). 실제로 충돌하는 저장소 2개를 만들어 검증하는 통합 테스트 1건
C3-a뜨자마자 죽는 크래시 루프면 30초마다 재기동 = 정상 주기의 10배로 API 쿼터를 태운다10분 안에 5회 넘으면 재시도 간격을 300초로 늘린다. 영구 정지는 하지 않는다 — 밤중에 조작자가 없을 수 있어 자가복구를 남긴다
정규식-a9/1 혼잡장 작업 중 한 줄 함수를 여러 줄 정규식으로 교체하다 뒤따르는 RouteHeap·routeAstar가 통째로 지워졌다. 문법 검사는 통과했고 지도까지는 정상이라 네 커밋 동안 아무도 몰랐다이전 커밋에서 인덱스 절단으로 복원(정규식 안 씀). 인라인 스크립트가 부르는 지역 함수가 전부 정의돼 있는지 보는 회귀 가드 추가 — 블록을 지우면 ['routeAstar']로 실패함을 역검증. 규칙은 저장소 CLAUDE.md에

5.4 범위와 한계

전수 감사가 아니다. 1회차가 읽은 것은 nowcast.py · collector_api.py · publish.py · backtrack.py(출구 계산부) · run_all.sh · index.html · report.html과 data/derived/ 산출물이다. 읽지 않은 것은 collector_cctv.py · evaluate.py · exit_shares.py · feeder_leadlag.py, 쉬운 버전·피치 덱, 그리고 9/1에 새로 들어온 field.js(혼잡장) · routing/(경로탐색) · tools/demo.sh다. collector_cctv.py는 구역 밀도를 도달 지연 계산에 직접 먹이는 미감사 입력 중 가장 큰 것이고, 혼잡장은 공개 화면에서 가장 눈에 띄는 요소라 2회차의 1순위다. 결함 대장은 저장소 redteam-20260901.md 하나이며 이 절은 그 요약이다 — 상태가 바뀌면 그 문서가 먼저다. (2026-09-08 추기) 위에 적은 미감사 파일은 2회차가 읽었고(collector_cctv.py·field.js·routing/), 4회차는 다른 모델이 전체 diff 를 공격했다. 5~7회차는 코드가 아니라 실행에서 찾은 것이라 코드 감사 범위와는 별개다.

6실전 결과 — 2026-09-05 당일

9/4 17:08 에 켠 파이프라인을 끄지 않고 9/6 01:01 까지 32시간 돌렸다. 전야제와 본번을 한 번의 운전으로 지났고, 본번 12시간 동안 5분마다 예측을 발행했다. 채점 숫자는 전부 src/evaluate.py --date 20260905(data/derived/eval_20260905.json)에서 가져왔다. 그 시각 화면은 운영·관람객 재생으로 그대로 열린다 — 배너 칩으로 20:05 → 23:02 여섯 시점을 오간다.

시험을 치른 뒤 채점지를 공개하는 절이다. 맞힌 것(피크 시각)과 놓친 것(꼬리)을 같은 무게로 적는다.

6.1 운전 기록

항목값
발행 스냅샷287개 — 12:00~24:00, 5분 간격, 자동
수집서울시 API 9,147건 · CCTV 판독 33,120건
재기동1회 — 16:44 수집 간격 전환(계획). 크래시 0 · 슬립 공백 0
여의나루 통제 실측18:10~22:05 무정차 (공지 20:40~21:40). 19시 통제를 19:13 에 규칙에 추가했다 — H3 「19시 낙관」이 현장에서 확인됐다
쇼 종료 실시각21:27 (계획 21:10). 현장에서 한 줄 기입 → 21:36 발행분부터 21시 유출 예측 76.6천 → 88.3천
화면 최대 부하여의나루 22시 0.93(재개통 직후) · 샛강 0.85 — 「경계」까지, 「심각」 없음. 사전표의 1.00 [0.65~1.41] 안

표 6-1 — 당일 수정 4건 (회귀 테스트 통과 후 배포)

시각무엇을왜
00:26보행 밀도 사다리 여유 1.5 → 0.5명/m², 못 본 길의 가로 기본 1.5 → 0.8한산한 새벽에 2.9km/h 로 읽혀 소요 시간이 부풀었다 (§3.10)
17:55서울시 구역 등급의 나이에서 API 발행 시차 28분을 뺐다시차를 낡음으로 세어 σ 가 1.1 까지 커졌고 서울시 등급이 혼잡장에서 빠져 있었다 (M16)
19:13여의나루 통제에 19시를 추가했다실측 조기 무정차 18:10~ (H3)
22:20 → 22:31속도 바닥 0.6m/s 를 10분 만에 철회하고 걷는 시간 ÷1.4 로 바꿨다바닥은 주의·경계·심각을 한 속도로 뭉갠다. 배율은 등급 간 시간 차를 지킨다. 1.4 는 가정값 (§3.10)

6.2 채점 — 맞힌 것과 놓친 것

세 가지를 쟀다. ① 오늘 배율 α 가 어떻게 움직였나 ② 유출 곡선의 형태가 실측 승차와 맞나 ③ 같은 도시데이터를 쓰는 서울시 12시간 선행 예측은 얼마나 맞았나(기준선). 규모의 절대값은 실측(핫스팟 9역 30분 승차 합)과 우리 예측(출구 7곳)의 범위가 달라 비교하지 않는다 — 피크 대비 비율만 비교한다.

표 6-2 — α 궤적 (사후 p50 · 관측 수 하차·승차)

시각α관측비고
12:011.000 · 0사전분포 [0.73~1.38]
12:471.361 · 0하루 최고 [1.07~1.68] — 첫 관측 한 건이 사전을 밀어냈다(C1 의 모습이 본번에도 있었다)
14:021.103 · 0
16:031.027 · 0
18:040.9912 · 0이후 여의나루 무정차로 하차 관측 종료
20:000.9513 · 3귀가측 관측 시작
21:360.9013 · 6쇼 종료 21:27 기입 직후
22:060.8913 · 7화면 최대 부하 시각
23:570.9113 · 10마감 [0.86~0.97] — 화면 밴드 ±6%. 실제 오차 하한은 §3.4 상자의 0.42~0.44 로 읽는다

표 6-3 — 유출 곡선의 형태: 22시 피크 대비 비율 (실측 = 핫스팟 9역 30분 승차 합 · 예측 = 출구 7곳 유출)

시각19시20시21시22시23시
실측0.090.080.401.000.92
예측0.250.380.681.000.41

맞힌 것 — 피크 시각 22시가 일치했고 형태 상관은 r 0.66 이다. 놓친 것 — 23시 꼬리. 실측은 피크의 92% 가 23시에도 이어졌는데 예측은 41% 로 꺼졌다. 원인 후보는 둘이다. ① 감쇠 형태 \(f_s(h)\) 가 2024·2025 실적에서 왔는데 2026 은 여의나루 조기 무정차(18:10~22:05)로 승차가 22시 뒤로 밀렸다. ② 쇼 종료 +17분을 반영해도 꼬리의 폭은 늘지 않는다. 조치는 보류했다 — 2026 실측 하루로 꼬리 파라미터를 다시 맞추면 과적합이다. 결함 대장 M15.

표 6-4 — 기준선: 서울시 12시간 선행 예측 vs 실측 (여의도한강공원 실시간 인구, 명)

시각실측12시간 전 예측오차
18시23,0006,25073%
20시43,0006,75084%
21시49,0007,25085%
22시53,0005,75089%
23시13,0003,75071%

하루 MAPE 0.56, 예측 범위 적중 4%. 같은 도시데이터를 쓰는 공식 선행 예측이 저녁 피크를 9배 낮게 봤다. 우리 α 를 5분마다 관측으로 갱신해야 하는 이유의 실측 근거다. 이 표는 우리 모델과의 직접 비교가 아니라 "선행 예측만으로는 안 된다"는 기준선이다.

이 밖에 실측이 확인해 준 것 — 조기 무정차의 방향(H2 의 "공지 시각 + 예년 이력" 병기가 맞았다) · 쇼 종료 기입 경로(C4)가 본번에서 한 번 작동했다 · 실측 장에서 최단시간 경로가 실제로 바뀌었다(36건 중 11건, §3.10 · M14 부분 닫힘) · 서울시 API 발행 시차 28.8분(2회차 가설 확정, M16). 미검증으로 남은 것 — 대기 분 문구(스톱워치 실측 없음).

6.3 실측이 닫은 열린 질문

  1. C1 α 밴드 — 밴드는 그대로 두고 실제 오차 하한(교차연도 0.42~0.44)을 §3.4 에 못 박았다(9/6). 본번 마감 밴드 ±6% 와 나란히 읽는다.
  2. 전야제 합격선 — 9/4 전야 관측으로는 "평시 금요일의 1.9배" 기준을 판정하지 못했다. 판정 불가로 기록하고, 파이프라인 정상 여부는 발행 주기·재기동 0·α 갱신으로 확인했다(§3.8).
  3. 쇼 종료 실시각 기입 — 21:27 에 한 줄 기입, 예측이 즉시 밀렸다(§6.1).
  4. 현장 관찰 — 여의나루 실제 무정차 18:10~22:05, 공지와 달랐다. 19시 통제를 당일 규칙에 넣었다.
  5. 경로 소요 기준 — §3.10 에서 닫았다(구간별 Kladek 시간, 9/5 ÷1.4 정정 포함).

6.4 다음 단계

7용어 사전

부하율
수요 ÷ 처리용량. 1.0이면 그 시간에 온 사람을 그 시간에 딱 다 태우는 상태. 넘으면 줄이 쌓인다.
초과 승차
축제일 승차 − 평시 토요일 중앙값. "평소에도 그 역을 쓰는 사람"을 빼고 남는 순수 축제 인파.
중앙값
여러 토요일 값을 줄 세웠을 때 한가운데 값. 평균과 달리 명절 같은 특이한 날에 안 끌려간다.
백테스트
과거의 한 시점으로 돌아가 "그때 알 수 있던 것만으로" 예측해 보고 실측과 비교하는 검증.
사전 예측표 / 나우캐스트
사전 예측표 = 행사 전에 과거 데이터만으로 만든 예측. 나우캐스트 = 당일 관측을 반영해 5분마다 갱신하는 예측.
α (알파)
오늘 인파가 예년 평균의 몇 배인지 나타내는 배율. 당일 관측으로 계속 갱신.
예측 밴드
"0.62가 아니라 0.55~0.70 사이" — 예측의 불확실성 폭. 관측이 쌓이면 좁아진다.
피더(feeder)역
축제 관람객이 여의도로 오려고 지하철을 탄 출발역. 여의도로 사람을 "공급"하는 역.
ROI (관심 영역)
CCTV 화면에서 계산에 넣을 부분만 오려낸 다각형. 우리는 "축제일에 보행로가 되는 차도"를 지정.
이월 규칙
역이 통제된 시간에 도착한 사람들을 "해제 후 첫 시간대"의 수요로 옮겨 세는 규칙.
혼잡장
지도 위 점 관측(CCTV·구역 등급)을 메워 만든 "어디가 얼마나 붐비나"의 면. 색은 혼잡, 진하기는 확신.
가우시안 과정 회귀
가까운 관측일수록, 믿을 만한 관측일수록 더 믿어 점 사이를 메우고, 관측 없는 곳은 "모른다"(분산 큼)로 남기는 통계 기법.
커널 · 길이척도
커널 = 두 지점이 얼마나 닮았을지를 거리로 정하는 규칙. 길이척도 = 그 닮음이 사라지는 거리 규모(150m·1,200m).
관측 노이즈 σ
관측 하나를 얼마나 못 믿는지의 폭. 클수록 그 관측이 장을 덜 끈다. 배제 대신 쓰는 손잡이.
LOO 교차검증
관측 하나를 빼고 나머지로 그 자리를 맞혀 보는 검사. 혼잡장의 길이척도·σ 가정을 9/5 이후 이것으로 고른다.
A* 탐색
목적지까지 남은 거리를 힌트로 삼아 최소 비용 경로를 찾는 알고리즘. 여기서 비용은 그 구간을 걷는 데 걸리는 시간이다(거리 ÷ 혼잡도별 보행속도). 붐비는 길은 느려지므로, 충분히 국소적인 혼잡이면 돌아가는 쪽이 더 빠르다. (2026-09-04 변경 — 전에는 거리 × (1 + 1.25 × 혼잡)이었다.)
워치독
프로세스가 살아 있는지 주기적으로 보고, 죽었으면 다시 띄우는 감시 루프.
적대적 검증(레드팀)
만든 사람이 공격자 입장으로 바꿔 자기 시스템을 깨 보는 검증. 지적마다 재현 근거를 붙인다.

8참고문헌·데이터 출처

8.1 데이터 출처 (상세)

데이터셋제공·ID사용 범위용도
서울교통공사 역별 일별 시간대별 승하차인원서울 열린데이터광장 OA-129212024·2025 연간 CSV (1~8호선, 9호선 미포함)출구별 실측 인파 E_st · 처리용량 · 백테스트 · 피더 역추적
티머니 교통카드 역별 일별 승하차서울 열린데이터광장 OA-129142024-10 · 2025-09 월 파일 (1~9호선)9호선 규모 · 용량 비례 추정
KT 수도권 생활이동 — 목적별 OD서울 열린데이터광장 OA-22300 (서울시×KT, 통신 기반 전수화 추정)축제일 2일(2024-10-05 · 2025-09-27) + 대조 토요일유출 시간 곡선 · 출발지(구 단위) 구성. 목적코드 3=귀가 (빅데이터캠퍼스 자료안내서 B078로 확정)
KT 생활이동 — 수단별 OD서울 열린데이터광장 OA-22657축제일 1일지하철 비중 (수단코드 6=지하철, 코드표 미공개라 "추정" 표기)
서울 생활인구 (행정동)서울 열린데이터광장 OA-149912024-10 · 2025-09 월 파일 (여의동 11560540)규모 외부 검증 (§3.8) — 당일 4.4~4.8배 · 전야 동 단위 희석
서울 실시간 도시데이터 (citydata)data.seoul.go.kr/SeoulRtd API핫스팟: 여의도한강공원(POI105)·여의도·여의서로 + 피더 12곳, 5분 주기당일 α 관측(30분 승하차) · 피더 감시 · 서울시 12시간 예측과 오차 비교
TOPIS 교통 CCTVtopis.seoul.go.kr 공개 HLS여의도 반경 23대 — 720p HD 9대, 나머지 720×480보행 밀도 → 보행속도 보정 · 혼잡장 관측. 프레임 미저장(집계만)
2026 축제 공식 공지hanwhafireworks.com공지 6호 등통제 규칙(여의나루 20:40~21:40 · 여의동로 전면통제) · 쇼 21:10 종료
2025 서울시 보도자료서울시2025 축제 교통대책통제 규칙 시드 (여의동로 14~23시 전면통제, 마포대교 하위 1차로 임시 보행로, 버스 26노선 증차)
지하철 실시간 도착서울 열린데이터광장 realtimeStationArrival여의도권 4역, 17~23시 (전용 키 6개 틱 회전 배정, 키당 ~56회/일)당일 화면 참고 정보
OpenStreetMap 도로 기하OSM Overpass API여의도 일대 4개 도로CCTV 관심영역 면적 산출 (차로 수·연장)
OpenStreetMap 보행망OSM Overpass API (© OpenStreetMap contributors, ODbL)카메라 23대를 감싸는 범위. 노드 9,219 · 간선 26,042 (2026-09-01 구축)혼잡 회피 경로탐색 그래프 docs/data/routing/walk_graph.json
서울시 한강교량 제원서울시 공표 자료마포대교 1,390×25m · 원효대교 1,120×20m교량 위 관심영역 면적

원본 대용량 파일은 저장소 밖 보관(공개 Release 예정). 전 데이터셋 재다운로드 명령은 저장소 data/README.md.

8.2 참고문헌

  1. Santanam, Trasatti, Van Hentenryck, Zhang (2021). Public Transit for Special Events: Ridership Prediction and Train Optimization. arXiv:2106.05359 — 애틀랜타 MARTA 경기장 이벤트 수요 예측. 소표본에서 "관측 패턴 + 곡선 맞춤" 접근(선형회귀 MAPE 11.7%)의 근거. 우리 모델 구조의 직접 참고.
  2. Weidmann, U. (1993). Transporttechnik der Fussgänger. ETH Zürich, IVT Schriftenreihe Nr. 90 — 보행 속도–밀도 관계(Kladek 식: 자유속도 1.34 m/s, 정체밀도 5.4명/m²). 출구 도달 지연 계산의 근거. 보행 기본도 개관: Seyfried et al.
  3. Wang, Liu, Samaras, Hoai (2020). Distribution Matching for Crowd Counting (DM-Count). NeurIPS 2020 — CCTV 군중 카운팅 모델. lwcc 라이브러리로 사용. 자체 검증: 밀집 군중 사진에서 밀도맵 계열 3종 합의 ±12%, 객체 검출(YOLO)은 12배 과소 → 밀도맵 채택.
  4. 특별행사 지하철 수요 문헌: Beijing 다변량 교란 DNN (ESWA 2021) · 역간 상호작용 실시간 예측 (Transp. Res. Part C 2018) — ML 계열 접근의 데이터 요구량 확인(수년치 필요), 구조 모델 선택의 방증.
  5. 서울시 지하철 혼잡도 기준 (서울시) · 국토부 도시철도 정거장 설계지침 (고시) — 용량·혼잡 등급 기준. 정거장 시설별 처리량(pph) 확보 시 9호선 추정 용량 대체 예정.
  6. Rasmussen, C. E. & Williams, C. K. I. (2006). Gaussian Processes for Machine Learning. MIT Press. gaussianprocess.org/gpml — 혼잡장(§3.9)의 사후 평균·분산 식과 Matérn 커널의 근거.

8.3 벤치마크한 기존 시스템 (11건 중 대표)

8.4 도구

본문의 모든 수치는 저장소 data/derived/*.json에 산출 근거와 함께 저장돼 있고, data/README.md의 명령으로 원본에서 재현 가능하다. 추정치는 본문·데이터 모두 "추정" 표기. 원본 대용량 데이터는 저장소 외부 보관(공개 Release 예정). 기준일 2026-09-08.