인천공항 주차장 예측 대시보드 상세 계획
공식 주차 API를 바탕으로 짠 계획입니다. 계획을 세우면서 모델 입력으로 쓸 추가 공공 API 두 개(시간대별 예상 승객 수, 공휴일)도 확인했습니다.
0. 목표와 성공 기준
대시보드가 답할 질문: "내가 ○시에 도착하면 어느 주차장에 자리가 있을까?", 그리고 "그 예측은 실제로 얼마나 맞았나?"
초기 목표치 (4주 운영 후 실제 데이터로 다시 정합니다) → 우선은 모으는 것에 집중합니다. 아래는 임의로 지정한 지표와 목표입니다.
| 지표 | 목표 |
|---|---|
| 1시간 뒤 예측 오차(점유율, 평균) | 3%p 이하 |
| 3시간 뒤 예측 오차(점유율, 평균) | 5%p 이하 |
| 예측 범위(하위 10%~상위 10%) 안에 실제값이 들어온 비율 | 약 80% |
| "만차 위험" 경보가 실제 만차를 잡아낸 비율 | 80% 이상 |
| 수집 성공률 | 월 99% 이상 (빠진 5분 구간 1% 미만) |
1. 데이터 소스
공공데이터포털 서비스키 하나로 아래 API를 모두 신청할 수 있습니다.
| 용도 | API | 호출 주기 | 하루 호출 수 |
|---|---|---|---|
| 예측 대상: 구역별 주차 대수와 총 면수 | B551177/StatusOfParking/getTrackingParking | 5분 | 288 |
| 선행 지표: 시간대별 예상 출·입국 승객 (오늘·내일) | B551177/passgrAnncmt/getPassgrAnncmt | 1시간 | 약 48 |
| 공휴일·연휴 달력 | 한국천문연구원 특일 정보 | 월 1회 | 약 0 |
| (선택) 날씨 | 기상청 단기예보 | 3시간 | 8 |
- 합계가 하루 약 350회라서 개발 계정(주차·승객예고 API는 하루 1,000회) 안에 들어옵니다. 공개할 때는 운영 계정으로 올립니다.
- 예상 승객 수가 중요한 이유: 연휴·명절 효과를 주차 데이터만으로 배우려면 1년이 필요합니다. 공항공사가 매일 내는 오늘·내일 예상 승객 수를 넣으면 첫 연휴부터 대응할 수 있습니다.
구역 유형별로 움직이는 방식이 다릅니다.
- 장기주차장·주차타워는 며칠에 걸쳐 차가 쌓입니다. 연휴 전에 차오르고 연휴가 끝나면 빠집니다. 그래서 "지난 3일 출국 승객 − 입국 승객" 같은 누적 값을 입력으로 씁니다.
- 단기주차장은 몇 시간 단위로 들고 납니다. 시간대별 출국 승객 수를 입력으로 씁니다.
2. 아키텍처 (Azure, MCAP 구독)
%%{init: {'theme':'neutral'}}%%
flowchart LR
API["data.go.kr<br/>주차 · 승객예고 · 특일"] --> C["collect<br/>5분"]
API --> X["exog<br/>1시간"]
C --> RAW[("Blob raw/<br/>원본 JSON")]
C --> OBS[("Blob obs/<br/>일별 JSONL")]
X --> EXO[("Blob exog/")]
OBS --> FC["forecast<br/>15분"]
EXO --> FC
FC --> FCS[("Blob forecasts/<br/>발행분 불변 저장")]
FCS --> EV["evaluate + publish<br/>1시간 · 매일"]
OBS --> EV
EV --> PUB[("public/*.json")]
C --> PUB
PUB --> WEB["Static Web Apps<br/>ECharts 대시보드"]
C -.-> AI["App Insights<br/>20분 무수집 알림"]
| 구성요소 | 선택 | 예상 월 비용 |
|---|---|---|
| 실행 | Azure Functions Flex Consumption (Python 3.12), 함수 4개 | 무료 제공량 내외, $0~3 |
| 저장소 | Blob Storage (1년치가 수백 MB 수준) | $0.5 미만 |
| 비밀값 | Key Vault (서비스키) | 약 $0.03 |
| 모니터링 | App Insights (하루 사용량 상한 설정) | $0~2 |
| 웹 | Static Web Apps Free | $0 |
| 배포 | GitHub + Actions(OIDC 인증) + Bicep | $0 |
비용은 합쳐서 월 $1~5 정도로 추정합니다.
이렇게 짠 이유
- 방문자는 미리 계산해 둔 JSON만 읽습니다. 사람이 몰려도 공공 API 호출 한도나 계산 비용에 영향이 없습니다.
- 데이터가 하루 약 5,500행(19개 구역 × 288회)으로 작습니다. 그래서 DB 없이 일별 JSONL 파일을 DuckDB로 바로 읽는 구조로 충분합니다.
검토했지만 뺀 선택지
| 선택지 | 뺀 이유 |
|---|---|
| GitHub Actions 예약 실행 | 실행이 수 분~수십 분 밀리거나 건너뛰는 일이 흔해 5분 수집에 맞지 않음 |
| Azure SQL serverless | 5분마다 쓰기가 들어와 자동 일시정지가 안 돼서 비용이 커짐 |
| ADX 무료 클러스터 | 시계열 분석 기능은 강하지만 외부 공개가 안 됨. 개인 분석용 보조로는 쓸 수 있음 |
| Power BI 웹에 게시 | 사내 정책상 보통 막혀 있음 |
먼저 확인할 위험: MCAP 구독에 Blob 공개 접근을 막는 정책이 걸려 있을 수 있습니다. 그렇다면 Function의 HTTP 엔드포인트가 managed identity로 Blob을 읽어 JSON을 캐시와 함께 내주는 방식으로 바꿉니다. 0단계에서 확인합니다.
3. 데이터 모델
| 테이블 | 주요 컬럼 |
|---|---|
zones (구역 목록) | zone_id, 터미널, 유형(단기/장기/타워/예약), 원본 구역명·별칭, 사용 기간 |
observations (실측) | 5분 구간 시각(KST), zone_id, 주차 대수, 총 면수, 가용 대수, 점유율, 원본 갱신 시각(datetm), 수집 시각, 품질 표시 |
raw_calls (호출 기록) | 호출 시각, HTTP 상태, 결과 코드, 응답 시간, 원본 파일 경로 |
calendar (달력) | 날짜, 요일, 공휴일 여부, 연휴 묶음 번호, 연휴 며칠째, 연휴까지 남은 날 |
exog_passenger (예상 승객) | 날짜, 시간, 터미널, 예상 출국·입국 승객, 가져온 시각 |
forecasts (예측, 덮어쓰지 않음) | 발행 시각, 모델·버전, zone_id, 대상 시각, 몇 분 뒤 예측인지, 점유율 예측(중앙값·하위 10%·상위 10%), 가용 대수 예측, 만차 확률 |
accuracy (정확도 집계) | 날짜, 모델, 구역, 예측 거리 구간, 요일·시간, 평균 오차, 편향, 적중률, 표본 수 |
- 첫 구역 목록은 19개입니다(현재 외부 사이트 기준, 1단계 첫 응답으로 확정).
- T1 10개: 단기 지하1·2·3층·지상층, 장기 P1·P2·P3, P1·P2 주차타워, P5 예약
- T2 9개: 단기 지하M층·지상1~4층, 장기, P1·P2 주차타워, 예약
- 예약주차장은 일반 이용자 추천에서 따로 분리합니다.
4. 수집기 (collect, 5분마다)
numOfRows=100,type=json으로 호출합니다. 타임아웃은 10초, 실패하면 2초·5초·10초 간격으로 세 번까지 다시 시도합니다.- 받은 응답을 그대로
raw/parking/YYYY/MM/DD/HHmm.json에 저장합니다. 나중에 가공을 다시 하거나 점검할 때 씁니다. - 정리 과정
- 구역 이름(
floor)을 구역 목록과 맞춥니다. 목록에 없는 이름이 나오면 알림을 보냅니다(신규 주차장이나 이름 변경 감지). - 가용 대수와 점유율을 계산합니다. 총 면수가 0이면 미운영으로 처리합니다.
- 구역 이름(
- 품질 표시
STALE: 원본 갱신 시각이 직전과 같음 (원천 데이터가 멈춤)OVER: 주차 대수가 총 면수보다 많음CAPCHANGE: 총 면수가 바뀜 (공사 등)GAP: 수집이 빠진 구간
obs/YYYY/MM/DD.jsonl에 이어 붙이고, 중복은 읽을 때 걸러냅니다.public/latest.json도 갱신합니다.- 20분 동안 수집 성공이 없거나 원본 갱신 시각이 30분 동안 멈추면 알림을 보냅니다.
5. 예측 모델 (데이터가 쌓이는 만큼 단계적으로)
예측 대상은 구역별 점유율입니다. 총 면수는 공사 등으로 바뀔 수 있어서, 점유율로 예측한 뒤 가용 대수로 바꿉니다. 발행은 15분마다, 15분 간격으로 24시간 뒤까지입니다.
| 모델 | 방법 | 쓸 수 있는 시점 |
|---|---|---|
| M0 | 지난주 같은 요일·같은 시각 값 (비교 기준선) | 수집 1주 뒤 |
| M1 | 최근 4~8주 같은 요일·같은 15분 구간의 가중 중앙값과 상·하위 10% 범위 | 2주 뒤 (4주 뒤 안정) |
| M1b | M1 × (오늘 예상 승객 ÷ 같은 요일 평균 예상 승객) | 2주 뒤, 연휴 대응 |
| M2 | M1에 "지금 평소보다 얼마나 다른지"를 더하고, 먼 시간일수록 그 차이를 줄여 반영 | 2주 뒤, 다가오는 몇 시간 예측의 핵심 |
| M3 | LightGBM 분위수 회귀. 입력: 시간대, 요일, 연휴 정보, 예상 승객, 최근 값(현재·1시간 전·어제·지난주) | 8~12주 뒤 |
- 운영 모델 선택: 모든 모델이 예측을 계속 내고, 최근 14일 오차율이 가장 낮은 모델을 대시보드에 보여줍니다. 구역 유형 × 예측 거리별로 따로 고릅니다.
- 예측에서 뽑는 결과
- 가용 대수 예측
- 만차 확률: 가용 대수가 50대 또는 총 면수의 3% 중 큰 값보다 적을 확률
- 도착 시각별 여유 구역 순위: 보수적 값(하위 10%) 기준
6. 예측과 실제 비교
- 예측은 발행 당시 값 그대로 보관합니다. 발행 시각과 모델 버전(git 커밋)을 붙여 저장하고, 나중에 다시 계산해서 덮어쓰지 않습니다. 그래야 "그때 뭐라고 예측했나"를 정직하게 잴 수 있습니다.
- 평가는 1시간마다 합니다. 대상 시각이 지난 예측을 가장 가까운 5분 실측값과 짝지어 비교합니다.
- 지표
- 평균 오차(%p와 대수 두 가지)
- 편향: 늘 높게 또는 낮게 예측하는지
- 오차율
- 예측 범위 적중률
- 만차 경보가 맞은 비율과 놓친 비율
- 나눠 보는 기준: 모델, 구역·터미널, 예측 거리(1시간 이내 / 1
3시간 / 36시간 / 6~24시간), 요일 × 시간 - 특이 상황 기록: 오차가 컸던 날에 연휴, 데이터 누락, 면수 변경을 자동으로 표시합니다. 폭설이나 공사 같은 메모는
annotations.yaml에 직접 적습니다. - 흐름 변화 감지: 최근 7일 편향이 ±5%p를 넘는 날이 3일 이어지면 알림을 보내고 재학습 대상으로 표시합니다.
7. 대시보드 구성
| 페이지 | 보여줄 내용 |
|---|---|
| 앞으로 24시간 (메인) | T1/T2 탭. 오늘 실측(실선), 예측 중앙값(점선), 예측 범위(띠)를 겹쳐 표시하고 현재 시각에 세로선. 만차 위험 시간대 강조. 도착 시각 선택 → 여유 예상 구역 순위와 만차 확률, 최근 정확도를 보여주는 신뢰도 표시 |
| 요일 × 시간 패턴 | 최근 8주 기준 7×24 히트맵(점유율 / 만차 확률 / 가용 대수 전환), 요일별 하루 흐름 비교 |
| 예측 정확도 | 어제·오늘 예측 대 실제 겹쳐 보기, 예측 거리별 오차 추이, 요일×시간 오차 히트맵, 모델 비교, 특이 상황 기록 |
| 장기 추이 | 하루 최고 점유율 달력 히트맵, 연휴 3일 전~3일 후 겹쳐 보기, 주간 평균 추이 |
| 방법·출처 | 출처 표시("인천국제공항공사, 공공데이터포털"), 비공식 서비스라는 안내, 데이터 수집률 |
- 화면은 정적 HTML과 Apache ECharts로 만듭니다. 히트맵, 달력, 범위 띠를 그대로 지원합니다. 모바일을 먼저 생각하고, 필요하면 한/영 전환을 넣습니다.
- 대시보드가 읽는 JSON 파일
| 파일 | 갱신 주기 | 내용 |
|---|---|---|
latest.json | 5분 | 구역별 최신값과 오늘 5분 단위 흐름 |
forecast.json | 15분 | 앞으로 24시간 예측 |
accuracy.json | 1시간 | 정확도 지표 |
pattern.json | 매일 | 요일×시간 통계 |
history_daily.json | 매일 | 하루 단위 최고·평균 점유율 |
8. 저장소 구조
icn-parking-forecast/
infra/ Bicep (main + modules)
functions/ function_app.py, collect/, exog/, features/, models/, evaluate/, publish/
web/ index.html, app.js, charts/, i18n/
config/ zones.yaml, annotations.yaml
notebooks/ 데이터 탐색
tests/
.github/workflows/ infra.yml, functions.yml, web.yml
9. 일정
| 단계 | 기간 | 산출물 | 완료 기준 |
|---|---|---|---|
| 0단계 | 오늘~내일 | API 키 3개 발급, 첫 응답으로 구역 목록 확정, 리소스 그룹 결정, Blob 공개 정책 확인 | 실제 응답을 받음 |
| 1단계 (최우선) | 1~3일 | 수집기 가동, 원본 저장, 알림 | 24시간 동안 288번 모두 수집 |
| 2단계 | 1~2주 | 대시보드 첫 버전: 오늘 실측 + 어제·지난주 겹쳐 보기, 수집 현황. M0 예측 발행 시작 | 공개 URL로 접속 가능 |
| 3단계 | 2~4주 | M1·M1b·M2, 정확도 페이지, 요일×시간 히트맵, 운영 계정 신청 | 예측 거리별 정확도가 매일 갱신됨 |
| 4단계 | 6~12주 | M3, 만차 확률 보정, 7일 전망 | 운영 모델이 M0보다 20% 이상 정확 |
| 5단계 | 3개월 이후 | 장기 추이·연휴 비교, 전날 밤 "내일 만차 위험" 요약 알림 | 연휴 3회 이상 평가 완료 |
⚠️ 1단계를 서두를 이유: 과거 데이터는 나중에 소급해서 받을 수 없습니다. 이번 주말 개천절 연휴(10/35)나 한글날 연휴(10/911) 전에 수집을 시작해야 첫 연휴 자료를 확보할 수 있습니다.
10. 위험 요소와 대응
| 위험 | 대응 |
|---|---|
| API 장애·지연 | 재시도, 누락 구간 표시, 알림. 대시보드에 "데이터 지연" 표시 |
| 구역 구성 변경 (신규 주차장, 공사) | 구역 목록에 없는 이름이 오면 알림, 면수 변경 표시, 구역 목록 이력 관리 |
| 연휴 데이터 부족 | 예상 승객 수로 보정(M1b), 1년 뒤 연휴 전용 모델로 전환 |
| 예측을 공식 정보로 오해 | "비공식 · 예측값" 안내 문구. 예상 승객 수를 화면에 보여줄 경우 API 설명에 따라 공항공사 관리부서와 안내 문구 협의 필요 (입력으로만 쓰면 협의 부담이 작음) |
| 개발 계정 한도나 기간 만료 | 공개 시점에 운영 계정으로 전환 (자동 승인) |
| 비용 초과 | App Insights 하루 사용량 상한, 예산 알림 |
11. 결정할 사항 (괄호 안이 제가 권하는 기본값)
- 공개 범위 (외부 전체 공개)
- 배포할 구독 (기존 SWA와 같은 MCAP 구독)
- 예측 간격 (15분)
- 언어 (한국어 먼저, 영어는 5단계)
# 인천공항 주차 예측 대시보드 — 실행 계획
> 최종 갱신: 2026-10-01 KST · 상태: 0단계 대기 (data.go.kr 서비스키 발급 필요)
## 1. 목표
- 과거 요일×시간 패턴으로 **다가오는 시간대**의 주차 여유를 예측한다.
- 발행 시점에 고정한 예측과 실제를 계속 비교해 정확도·편향·흐름 변화를 추적한다.
- 공개 웹 대시보드로 누구나 미리 참고하고 움직일 수 있게 한다.
### 성공 기준 (초기 가설, 4주 운영 후 재설정)
| 지표 | 목표 |
|---|---|
| 1시간 앞 점유율 예측 평균 오차 | ≤ 3%p |
| 3시간 앞 점유율 예측 평균 오차 | ≤ 5%p |
| 예측 범위(P10–P90) 적중률 | ≈ 80% |
| 만차 경보 재현율 | ≥ 80% |
| 수집 가용성 | ≥ 99% / 월 |
## 2. 데이터 소스 (data.go.kr, 서비스키 1개)
| 용도 | 엔드포인트 | 주기 | 호출/일 |
|---|---|---|---|
| 목표변수: 구역별 주차대수·총면수 | `https://apis.data.go.kr/B551177/StatusOfParking/getTrackingParking` | 5분 | 288 |
| 선행지표: 시간대별 예상 출·입국 승객 (D, D+1) | `https://apis.data.go.kr/B551177/passgrAnncmt/getPassgrAnncmt` | 1시간 | ~48 |
| 공휴일·연휴 | `https://apis.data.go.kr/B090041/openapi/service/SpcdeInfoService/getRestDeInfo` | 월 1회 | ~0 |
| (선택) 날씨 | 기상청 단기예보 (data.go.kr 15084084) | 3시간 | 8 |
- 주차 API 요청: `serviceKey`, `numOfRows=100`, `pageNo=1`, `type=json`
- 주차 API 응답: `floor`(구역명), `parking`(주차대수), `parkingarea`(총면수, 0=미운영), `datetm`(`yyyyMMddHHmmss.fff`, KST)
- 조회 당일 현재값만 제공 → **과거 데이터 소급 불가, 수집을 최대한 빨리 시작**
- 개발계정(1,000회/일)으로 충분. 공개 시 운영계정(활용사례 등록, 자동승인)으로 전환
- 승객예고 데이터를 화면에 노출하면 인천공항공사 관리부서와 안내문구 협의 필요 (모델 입력으로만 쓰면 부담 적음)
### 구역 유형별 특성
- 장기주차장·주차타워: 며칠 단위로 쌓이는 **재고형** → 최근 3일 누적(출국 − 입국 예상 승객)이 핵심 입력
- 단기주차장: 시간 단위 **흐름형** → 시간대별 출국 예상 승객이 핵심 입력
- 예약주차장: 일반 이용자 추천에서 분리
## 3. 아키텍처
```mermaid
flowchart LR
API["data.go.kr<br/>주차 · 승객예고 · 특일"] --> COL["collect<br/>5분"]
API --> EXO["exog<br/>1시간"]
COL --> PRIV[("Blob private<br/>raw · obs · exog<br/>forecasts · accuracy · model")]
EXO --> PRIV
PRIV --> FC["forecast<br/>15분"]
FC --> PRIV
PRIV --> EV["evaluate 1시간<br/>profile 매일"]
EV --> PRIV
COL --> WEB[("Blob $web<br/>대시보드 + data/*.json")]
FC --> WEB
EV --> WEB
WEB --> U["브라우저"]
```
| 구성 | 선택 | 비고 |
|---|---|---|
| 실행 | Azure Functions Flex Consumption, Python 3.12, 인스턴스 2,048MB | TZ 설정 미지원 → CRON은 UTC 기준. 실행당 최소 1초 과금 |
| 저장 | Storage Account (StorageV2, Hot, LRS, Korea Central) | private 컨테이너 + `$web` 정적 웹사이트 |
| 서빙 | Storage 정적 웹사이트 (`$web`) | 대시보드와 데이터가 같은 출처 → CORS 불필요 (정적 웹사이트는 CORS 미지원). `$web`은 익명 접근 차단 설정과 무관하게 항상 공개 |
| 비밀 | Key Vault | data.go.kr 서비스키, Function managed identity로 참조 |
| 모니터링 | Azure Monitor 메트릭 알림 2개 + App Insights 최소 설정 | §10 |
| 비용 통제 | Budget 알림 $5 / $10 | |
| 배포 | GitHub + Actions(OIDC) + Bicep | 구독: MCAP, 지역: Korea Central |
| 이번 범위 제외 | Cosmos DB, Static Web Apps, CDN | §14 결정 기록 |
### 함수와 스케줄
| 함수 | CRON (UTC, 6필드) | KST | 역할 |
|---|---|---|---|
| `collect` | `0 */5 * * * *` | 5분마다 | 주차 API 수집 → raw, obs, `$web/data/latest·today` |
| `exog` | `0 5 * * * *` | 매시 05분 | 승객예고 D/D+1 수집, 매월 1일 특일 갱신 |
| `forecast` | `0 2/15 * * * *` | 02·17·32·47분 | 모델 실행 → forecasts(불변 저장), `$web/data/forecast` |
| `evaluate` | `0 10 * * * *` | 매시 10분 | 대상 시각이 지난 예측 vs 실측 → accuracy, `$web/data/accuracy` |
| `profile` | `0 30 18 * * *` | 매일 03:30 | 최근 8주 요일×슬롯 통계 → `model/profile.json`, `$web/data/pattern·history` |
## 4. 저장소 레이아웃
### private 컨테이너
```
raw/parking/YYYY/MM/DD/HHmm.json 호출별 원본 + {http_status, result_code, latency_ms, error} (PutBlob)
raw/passenger/YYYY/MM/DD/HH.json 승객예고 원본 (PutBlob)
obs/YYYY/MM/DD.jsonl 정규화 실측, 수집 성공 시에만 append (AppendBlock, 유일)
exog/passenger/YYYY/MM/DD.json 승객예고 정규화, 하루 파일 덮어쓰기 (PutBlob)
exog/calendar/YYYY.json 공휴일·연휴 블록 (PutBlob)
forecasts/YYYY/MM/DD/HHmm.json.gz 발행분 불변 저장, 덮어쓰기 금지 (PutBlob)
accuracy/YYYY/MM/DD.json 일별 정확도 집계 (PutBlob)
model/profile.json 요일×15분 슬롯 통계, forecast가 읽음 (PutBlob)
```
- `AppendBlock`은 `obs/`에서만 사용한다 → 적재 중단 알림의 기준 신호 (§10)
- 규모: 실측 5,472행/일, 예측 발행 96회/일 × 19구역 × 96시점, 1년 누적 약 3GB 이하
- 필요 시 월 1회 obs를 Parquet으로 압축 (선택)
### `$web` 컨테이너 (공개)
| 경로 | 갱신 | Cache-Control | 내용 |
|---|---|---|---|
| `index.html`, `app.js`, `style.css` | 배포 시 | `max-age=300` | 대시보드 |
| `data/latest.json` | 5분 | `public, max-age=60` | 구역별 최신값, 마지막 수집 시각, stale 여부 (~3KB) |
| `data/today.json` | 5분 | `public, max-age=60` | 오늘 5분 단위 실측 (~20KB) |
| `data/forecast.json` | 15분 | `public, max-age=300` | 앞으로 24시간 예측, P10/P50/P90, 만차 확률 (~15KB) |
| `data/accuracy.json` | 1시간 | `public, max-age=3600` | 정확도 지표 |
| `data/pattern.json` | 매일 | `public, max-age=3600` | 요일×시간 통계 |
| `data/history.json` | 매일 | `public, max-age=3600` | 일별 최고·평균 점유율 |
- 모든 데이터 JSON은 gzip으로 저장하고 블롭 속성 `Content-Encoding: gzip`, `Content-Type: application/json` 지정
- 크기는 압축 후 기준
- 브라우저는 5분 경계 + 45초에만 갱신, `pattern`·`accuracy`·`history`는 해당 탭을 열 때만 로드
- 데이터 기본 URL은 설정값 하나로 둔다 → CDN 전환 시 코드 변경 없음
## 5. 데이터 모델
| 엔터티 | 주요 필드 |
|---|---|
| `zones` (repo `config/zones.yaml`) | zone_id, terminal(T1/T2), category(단기/장기/타워/예약), raw_names(별칭), active_from, active_to |
| `observations` | slot_ts_kst(5분 내림), zone_id, parking, capacity, available, occupancy, source_ts(datetm), collected_at_utc, flags |
| `raw_calls` | collected_at_utc, http_status, result_code, latency_ms, error, blob_path |
| `calendar` | date, dow, is_holiday, holiday_name, block_id, day_in_block, days_to_block, days_from_block |
| `exog_passenger` | date, hour, terminal, dep_expected, arr_expected, fetched_at |
| `forecasts` | issued_at, model, model_version(git SHA), zone_id, target_ts, horizon_min, p10, p50, p90, available_p50, full_prob |
| `accuracy` | date, model, zone_id/terminal/category, horizon_bucket, dow, hour, mae_pp, mae_cars, bias, wape, coverage, n |
### 초기 구역 (19개, 1단계 첫 응답으로 확정)
- T1 (10): 단기 지하1층·지하2층·지하3층·지상층, 장기 P1·P2·P3, 장기 P1 주차타워·P2 주차타워, P5 예약주차장
- T2 (9): 단기 지하M층·지상1층·지상2층·지상3층·지상4층, 장기 주차장, P1 장기주차타워·P2 장기주차타워, 예약주차장
## 6. 수집기 (`collect`)
1. 주차 API 호출 (`numOfRows=100`, `type=json`), 타임아웃 10초, 재시도 3회 (2초·5초·10초)
2. 원본 + 호출 메타(상태·결과코드·지연·에러)를 `raw/parking/...` 에 PutBlob (실패해도 저장)
3. 정규화
- `floor` → zone_id 매핑 (공백 제거, 별칭). 미등록 구역명 → 경고 로그 + `unknown_zone` flag
- available = max(capacity − parking, 0), occupancy = parking / capacity, capacity=0 → 미운영(null)
4. 품질 플래그: `STALE`(datetm 직전과 동일), `OVER`(parking > capacity), `CAPCHANGE`(총면수 변경), `GAP`(누락 구간)
5. 성공 시에만 `obs/YYYY/MM/DD.jsonl` 에 append (중복은 읽을 때 slot+zone 기준 제거)
6. `$web/data/latest.json`, `today.json` 갱신 (datetm 30분 이상 정체 시 `stale: true`)
7. 시각: slot_ts는 KST 5분 내림, 원본 `datetm`과 UTC 수집 시각 모두 보존
## 7. 예측 모델
- 예측 대상: 구역별 점유율 → 최신 총면수로 가용 대수 환산
- 발행: 15분마다, 15분 해상도, 24시간 앞까지
- 터미널·유형 합계는 면수 가중 합산
| 모델 | 방법 | 사용 가능 시점 |
|---|---|---|
| M0 | 지난주 같은 요일·같은 시각 (벤치마크) | 1주 후 |
| M1 | 최근 4~8주 같은 요일·15분 슬롯 가중 중앙값 + 잔차 기반 P10/P90 | 2주 후 (4주 후 안정) |
| M1b | M1 × (당일 예상 승객 ÷ 같은 요일 평균 예상 승객) | 2주 후, 연휴 대응 |
| M2 | M1 + 현재 편차 Δ를 horizon에 따라 감쇠 (Δ·φ^h, φ는 구역 유형별 학습) | 2주 후, 단기 nowcast 핵심 |
| M3 | LightGBM 분위수 회귀 (슬롯, 요일, 연휴 블록, 승객예고, 래그: 현재·1시간 전·어제·지난주) | 8~12주 후 |
- 챔피언: 최근 14일 WAPE 최저 모델을 구역 유형 × horizon 버킷별로 선택, 나머지는 도전자로 계속 발행
- 파생 출력
- 가용 대수 예측 (P50, 보수적 P10)
- 만차 확률: P(가용 < max(50대, 총면수 3%))
- 도착 시각별 여유 구역 순위 (P10 기준)
- 예측 작업은 `model/profile.json` + 오늘 실측 + 승객예고만 읽는다 (8주 재스캔 금지)
## 8. 정확도 추적
- 모든 예측은 발행 시각·모델 버전과 함께 불변 저장, 재계산으로 덮어쓰지 않음
- `evaluate`가 매시 대상 시각이 지난 예측을 가장 가까운 5분 실측(±5분)과 조인
- 지표: 평균 오차(%p, 대수), 편향, WAPE, P10–P90 적중률, pinball loss, 만차 경보 정밀도·재현율
- 분할: 모델 × 구역/터미널/유형 × horizon 버킷(≤1h, 1–3h, 3–6h, 6–24h) × 요일 × 시간
- 이례일 로그: 오차 상위 사례에 연휴·데이터 누락·면수 변경 자동 주석 + `config/annotations.yaml` 수동 메모
- 드리프트: 7일 편향이 ±5%p를 3일 연속 초과 → 경고 로그 + 재학습 플래그
## 9. 대시보드
| 페이지 | 내용 |
|---|---|
| 앞으로 24시간 (메인) | T1/T2 탭, 유형별 차트: 오늘 실측(실선) + 예측 P50(점선) + P10–P90 밴드 + 현재 시각선, 만차 위험 구간 강조. 도착 시각 선택 → 여유 예상 구역 순위, 만차 확률, 최근 정확도 기반 신뢰도 |
| 요일×시간 패턴 | 최근 8주 7×24 히트맵 (점유율 / 만차 확률 / 가용 대수), 요일별 하루 곡선 비교 |
| 예측 정확도 | 어제·오늘 예측 vs 실제, horizon별 오차 추이, 요일×시간 오차 히트맵, 모델 비교, 이례일 로그 |
| 장기 추이 | 일별 최고 점유율 달력 히트맵, 연휴 D-3~D+3 겹쳐 보기, 주간 평균 추이 |
| 방법·출처 | 출처 "인천국제공항공사, 공공데이터포털", 비공식 서비스·예측값 안내, 수집률, 마지막 수집 시각 |
- 기술: 정적 HTML/JS + Apache ECharts, 빌드 도구 없음, 모바일 우선
- 언어: 한국어 우선 (영어는 5단계)
## 10. 모니터링·알림
| 대상 | 방법 | 조건 | 비용 |
|---|---|---|---|
| 데이터 적재 중단 (앱 중단 + API 실패 모두) | Storage 메트릭 `Transactions`, `ApiName = AppendBlock` | 15분 합계 < 1 → 이메일 | ~$0.1/월 |
| 앱 미실행 | Function 메트릭 `OnDemandFunctionExecutionCount` | 20분 합계 < 3 → 이메일 | ~$0.1/월 |
| 원천 데이터 정지 | `latest.json`의 `stale` + 대시보드 "마지막 수집 N분 전" 배지 | datetm 30분 정체 | $0 |
| 코드 예외 | App Insights 최소 설정: 로그 Warning 이상, 샘플링, 일일 상한 50MB, 대시보드·워크북 없음 | — | $0~1/월 |
| 비용 | Budget 알림 | $5 / $10 | $0 |
## 11. 비용 추정 (Korea Central 정가, 2026-10-01 Retail Prices API)
### 단가
| 항목 | 단가 |
|---|---|
| Blob Hot LRS 저장 | $0.02 / GB·월 |
| Blob 쓰기 | $0.05 / 1만 건 |
| Blob 읽기 | $0.004 / 1만 건 |
| 인터넷 전송 | 월 100GB 무료, 이후 $0.11 / GB |
| Functions Flex 실행 | $0.000037 / GB·s (무료 제공량 있음), 실행 $0.40 / 100만 건 |
| Log Analytics 수집 | $3.11 / GB |
| (참고) Cosmos 서버리스 | $0.271 / 100만 RU, 저장 $0.25 / GB·월 |
| (참고) Front Door Standard | 기본료 $35 / 월, 요청 $1.08 / 100만 건, 전송 $0.115 / GB |
### 월 추정 (1년차)
| 항목 | 월 비용 |
|---|---|
| Functions (약 1.3만 실행, 전부 과금 시 약 7.8만 GB·s) | $0 ~ 3 |
| Blob 쓰기 (약 4만 건) | $0.2 |
| Blob 내부 읽기 | $0.06 |
| Blob 저장 (연말 약 3GB) | $0.06 |
| 방문자 서빙 (하루 1만 방문, gzip) | $0.4 |
| Key Vault, 메트릭 알림 2개 | $0.25 |
| App Insights | $0 ~ 1 |
| **합계** | **약 $1 ~ 5** |
### 방문자 수별 서빙 비용 (방문당 파일 3개, gzip 약 50KB)
| 하루 방문 | Blob 직접 | Front Door Standard |
|---|---|---|
| 1천 | $0.04 | 약 $35 |
| 1만 | $0.4 | 약 $38 |
| 10만 (한 달 지속 가정) | 약 $9 | 약 $62 |
| 10만, 압축 안 함 | 약 $75 | — |
## 12. 단계별 일정
| 단계 | 기간 | 산출물 | 완료 기준 |
|---|---|---|---|
| **0단계** | 10/1 | 서비스키 발급(주차·승객예고·특일), 첫 응답으로 구역 마스터 확정, 리소스 그룹·구독 확정 | 실제 응답 수신 |
| **1단계 (최우선)** | 10/1~10/2 | Bicep 인프라, `collect`, raw/obs 저장, `latest·today.json`, 메트릭 알림 2개, App Insights 최소, 예산 알림 | 24시간 288/288 슬롯 수집. 10/3 연휴 전 가동 |
| 2단계 | 1~2주 | 대시보드 v0 (오늘 실측 + 어제·지난주 겹쳐 보기, 수집 현황), `exog`, M0 발행 시작 | `$web` 공개 URL 접속 가능 |
| 3단계 | 2~4주 | `profile`, M1·M1b·M2, `evaluate`, 정확도 페이지, 요일×시간 히트맵, 운영계정 신청 | horizon별 정확도 매일 갱신 |
| 4단계 | 6~12주 | M3, 만차 확률 보정, 7일 전망 | 챔피언이 M0 대비 WAPE 20% 이상 개선 |
| 5단계 | 3개월~ | 장기 추이·연휴 비교, 전날 밤 "내일 만차 위험" 요약, 영어 | 연휴 3회 이상 평가 |
- 10월 연휴(10/3~5 개천절·대체공휴일, 10/9~11 한글날) 전에 1단계를 가동해야 첫 연휴 샘플을 확보한다.
## 13. 위험과 대응
| 위험 | 대응 |
|---|---|
| API 장애·지연 | 재시도, GAP 플래그, 적재 중단 알림, 대시보드 지연 배지 |
| 구역 구성 변경 (신규 주차장, 공사) | 미등록 구역명 경고, CAPCHANGE 플래그, zones 이력 관리 |
| 연휴 데이터 부족 | M1b 승객예고 스케일링, 1년 후 연휴 전용 모델 |
| 예측을 공식 정보로 오인 | "비공식·예측값" 안내, 승객예고 노출 시 공항공사 협의 |
| 개발계정 한도·만료 | 공개 시점에 운영계정 전환 |
| 트래픽 급증·악용 | gzip, Cache-Control, 예산 알림. 기준 초과 시 CDN 전환 (§14 D4) |
| Flex 제약 | TZ 미지원 → CRON UTC, 앱 초기화 30초 제한 → 무거운 import 지연 로딩 |
## 14. 결정 기록
| # | 날짜 | 결정 | 근거 |
|---|---|---|---|
| D1 | 10/1 | 저장소는 Blob 단독 (JSONL·JSON·gzip) | 시계열 append + 범위 통째 읽기 + 정적 배포 패턴. Blob 월 $0.5 미만 vs Cosmos 잘 설계 $1.5~2.5, 단순 설계 $80~250 |
| D2 | 10/1 | Cosmos 병행 보류 | 같은 데이터 이중 쓰기로 비용 절감 없이 복잡도만 증가. 방문자 API 경로는 Function 최소 1초 과금으로 Blob 대비 수십 배. 사용자 기능(알림 구독·즐겨찾기·피드백) 도입 시 그 부분만 Cosmos 서버리스. 저장소 접근은 인터페이스로 분리 |
| D3 | 10/1 | 서빙은 Storage 정적 웹사이트 `$web` (SWA 제외) | 같은 출처라 CORS 불필요, 익명 접근 차단 정책과 무관, 추가 비용 없음 |
| D4 | 10/1 | CDN 보류, CDN-ready 설계 | Front Door 기본료 $35가 전체 비용보다 큼. 전환 기준: 월 전송 100GB 초과 지속, 악용 트래픽, 커스텀 도메인+HTTPS 필요 → Front Door Standard 또는 Cloudflare 무료. Azure CDN classic은 2025-08-15부터 신규 생성 불가 |
| D5 | 10/1 | App Insights 최소 설정 | 분석·대시보드 불필요. 수집 중단 감지는 메트릭 알림으로 해결, App Insights는 코드 예외 기록용 |
| D6 | 10/1 | 예측 15분 발행, 15분 해상도, 24시간 앞 | 다가오는 시간대 중심, 저장량 부담 적음 |
| D7 | 10/1 | MCAP 구독, Korea Central | 데이터·사용자 모두 국내 |
## 15. 다음 행동
- [ ] (사용자) data.go.kr 활용신청: 인천국제공항공사_주차 정보(15095047), 승객예고-출·입국장별(15095066), 한국천문연구원_특일 정보(15012690)
- [ ] (사용자) 구독·리소스 그룹 이름 확정
- [ ] 첫 응답으로 `config/zones.yaml` 확정
- [ ] 1단계 착수: repo 생성 → Bicep → `collect` → 알림 → 배포 → 24시간 검증
## 16. 저장소 구조 (repo)
```
icn-parking-forecast/
infra/ main.bicep, modules/ (storage, function, keyvault, monitor, budget)
functions/ function_app.py, collect/, exog/, forecast/, evaluate/, profile/, common/storage.py
web/ index.html, app.js, style.css, charts/
config/ zones.yaml, annotations.yaml
notebooks/ 탐색 분석
tests/
.github/workflows/ infra.yml, functions.yml, web.yml
```