주차 예측에 confidence를 붙인 이유

p10, p50, p90, risk, backtest로 예측을 하나의 정답처럼 보이지 않게 했다

한강자리의 주차 예측은 미래를 정확히 맞히겠다는 약속이 아니다. 도착 전 “지금 가면 위험한가”를 가늠하게 돕는 참고값이다.

그래서 계산식보다 먼저 정한 것은 화면에서 어떻게 말할지였다.

예측은 현재값과 분리한다. confidence를 같이 둔다. 출처가 오래됐는지도 이유로 남긴다. backtest로 계속 얼마나 빗나갔는지 본다.

사용자는 예측 내부를 보지 않는다. 대신 “갈 만한가”, “불안한가”, “정보가 부족한가”를 본다. 그래서 예측값을 계산하는 일과 그 값을 어떤 말로 보여줄지를 같이 정해야 했다. 숫자만 바로 화면에 올리면, 앱은 그 숫자의 불확실성을 설명하지 못한다.

예측은 네 가지 근거를 섞는다

주차 예측은 네 종류의 근거를 섞는다.

근거의미
최근 현황가장 최근에 확인한 잔여 대수와 총면수
최근 변화최근 확인값 사이의 변화 속도
과거 평균선같은 예측 간격과 시간대에서의 과거 평균·분산
최근 구간 통계최근 구간의 이동 평균, 변동성, 변화 기울기 보정

현재값이 있으면 최근 변화를 강하게 반영하고, 예측 시간이 멀수록 과거 평균선의 비중을 올린다. 현재값이 없으면 과거 평균선이나 최근 구간 통계를 대신 쓴다. 아무 근거도 없으면 값 없음으로 남긴다.

가중치와 threshold는 쓰면서 바뀔 수 있다. 더 오래 남는 것은 어떤 근거를 보고, 근거가 약할 때 어떻게 낮춰 말할지다.

flowchart LR
  Snapshots["최근 현황"] --> Trend["분당 변화량"]
  History["과거 평균선"] --> Estimate["p50 예상값"]
  Features["최근 구간 통계"] --> Estimate
  Trend --> Estimate
  Estimate --> Quantiles["p10 / p50 / p90"]
  Quantiles --> Risk["위험도"]
  Quantiles --> Status["예상 혼잡"]

하나의 숫자 대신 범위와 위험도를 둔다

하나의 숫자만 내면 사용자는 그 숫자를 사실처럼 읽는다. 그래서 예측값에는 p10, p50, p90을 둔다.

  • p50: 대표 예상 잔여 대수.
  • p10: 보수적으로 봤을 때의 낮은 잔여 대수.
  • p90: 낙관적으로 봤을 때의 높은 잔여 대수.

위험도는 이 세 값을 보고 만든다. 주차는 “평균적으로 남아 있다”보다 “낮게 보면 위험한가”가 더 중요하기 때문이다.

근거가 약하면 confidence를 낮춘다

confidence는 예측을 그럴듯하게 보이게 하는 점수가 아니다. 사용자와 개발자 모두에게 “이 예측을 얼마나 조심해서 읽어야 하는지”를 알려주는 값이다.

지금은 다음을 본다.

  • 예측 시간이 멀수록 confidence를 낮춘다.
  • 출처가 오래됐을수록 낮춘다.
  • 과거 표본이 충분하면 약간 올린다.
  • 최근 구간 표본이 충분하면 약간 올린다.

reason code도 함께 남긴다. 예를 들어 출처가 오래됐거나, 예측 시간이 멀거나, 과거 평균선을 사용했거나, confidence가 낮다는 신호를 함께 저장한다.

flowchart TD
  Horizon["예측 시간"] --> Confidence
  Freshness["출처 최신성"] --> Confidence
  Samples["과거 표본 수"] --> Confidence
  Features["최근 구간 표본 수"] --> Confidence
  Confidence --> Reason["사유 코드"]

예측은 한 번 만든 묶음으로 남긴다

예측은 한 번 만들 때마다 run으로 남긴다. 하나의 run에는 model version, 만든 시각, 예측 간격이 들어간다. 성공하면 저장한 줄 수와 어느 확인 시각을 바탕으로 삼았는지 남기고, 실패하면 run을 실패로 닫는다.

sequenceDiagram
  autonumber
  participant Job as 예측 작업
  participant Repo as 예측 저장소
  participant Gen as 예측 생성기

  Job->>Repo: 새 예측 실행 기록 만들기
  Gen->>Repo: 주차장/이력/평균선/통계 읽기
  Gen->>Gen: 예측 간격별 값 계산
  Gen->>Repo: 예측 결과 저장
  Gen->>Repo: 기준 확인 시각과 함께 실행 기록 닫기

run이 있어야 “현재 API에 보이는 예측이 언제 계산됐는가”를 따라갈 수 있다. 또 model version을 바꿨을 때 이전 결과와 섞이지 않는다.

예측 문구는 backtest를 보고 조심한다

예측은 출시 뒤 실제값과 비교해야 한다. 한강자리 backtest는 과거 forecast가 목표로 삼은 도착 시각 근처의 실제 확인값을 label로 붙이고, 예측 간격별 metric을 계산한다.

보는 값은 대략 다음이다.

  • 표본 수.
  • MAE 성격의 평균 오차.
  • p10~p90 구간이 실제값을 포함한 비율.
  • 출처가 오래된 표본 비율.
  • gate 통과 여부.
flowchart LR
  ForecastRows["과거 예측 값"] --> Labels["도착 시각 근처 실제값 붙이기"]
  Labels --> Score["예측과 실제값 비교"]
  Score --> Metrics["예측 간격/모델 메트릭"]
  Metrics --> Gate["품질 게이트"]

backtest가 있어야 예측을 앱 문구로 과장하지 않을 수 있다. 정답 맞히기가 아니라, 오차와 불확실성을 계속 보는 참고값으로 유지할 수 있다.

backtest는 계산식을 고치는 도구이면서 화면 문구의 안전망이다. metric이 좋지 않은 예측 간격을 알고 있으면 그 구간을 더 조심스럽게 표현할 수 있다. 반대로 검증 없이 예측을 “정확한 안내”처럼 말하면, 겉으로는 좋아 보여도 사용자는 쉽게 실망한다.

예측은 과신을 줄일 때 쓸모 있었다

화면에서 예측값과 현재값은 분리해야 했다. p50 하나보다 p10/p50/p90과 위험도(risk level)를 함께 두는 편이 안전했고, confidence는 예측 시간, 출처 최신성, 표본 수의 영향을 받아야 했다.

reason code는 디버깅용이면서 사용자 표현의 근거가 됐다. run과 model version을 남겨야 결과를 재현하고 비교할 수 있고, backtest 없이는 예측이 나아졌는지도 말할 수 없다.

주차 예측은 이름만 보면 멋있지만, 실제로는 과신을 줄이는 쪽에서 가치가 생겼다. 숫자를 하나 더 보여주는 것보다, 그 숫자를 어디까지 믿어야 하는지 함께 말하는 일이 앱에 더 가까웠다.

이미지 확대