how-many-blood — 헌혈량 통계·예측 플랫폼
전국 헌혈량을 시간별로 수집해 시간대/요일/지역 패턴을 분석하고, 날씨 기반 예측 모델로 향후 헌혈량을 일별로 예측하는 데이터 플랫폼. 경기혈액원 복무(2024.10–2026.07) 중 만든 Red Cross Works의 핵심 축. 이 페이지는 GitHub 레포(a7garden/how-many-blood)의 실제 코드를 직접 읽어 정리한 분석이다.
① 시스템 전체 구조 — 서버리스 예측 파이프라인
백엔드가 서버가 아니라 Firebase Cloud Functions(v2) 스케줄러 5종이다. 프런트엔드는 사전 계산된 Firestore 문서를 조회만 한다 — 예측 연산은 전부 서버리스 측.
0 10-20 * * *혈액정보공개 API(bissp.bloodinfo.net)에서 시간별 헌혈량 스냅샷 → `hourly_blood_records` (문서 id `{date}_{hour}`)
functions/src/hourlyScheduler.ts 0 0,6,12,18 * * *Open-Meteo 7일 예보 → `weatherForecastLatest/119` 단일 스냅샷. 수집 직후 예측 생성까지 자동 진행
functions/src/weatherScheduler.ts 일 01:00기상청 ASOS 일자료 API (수원 관측소 119) → `dailyWeatherSummary/{regionCode}_{date}` — 학습용 과거 날씨
functions/src/weatherCollector.ts 일요일 02:00 / 02:10개인(individual) / 단체(group) 채널별로 각각 학습 → 계수를 `predictionModels`에 저장
functions/src/predictionScheduler.ts 예보 수집 직후학습된 모델 + Open-Meteo 예보로 향후 평일(토/일 제외) 일별 예측 → `donationPredictions`
functions/src/predictionModel.ts 데이터 소스 분리가 핵심 설계: 학습용 관측은 기상청 ASOS(실측), 추론용 예보는 Open-Meteo(7일). README에 "기상청 API 연동"이라 쓰여 있지만 예측 시점에 쓰이는 것은 Open-Meteo다 — 면접에서는 "학습=ASOS 관측, 추론=Open-Meteo 예보"로 정확히 구분해 말하는 것이 안전.
② 예측 모델 — 정확히 무엇인가
핵심 파일은 functions/src/predictionModel.ts. "ML 예측"이라고 소개하지만
실제 구현은 가산(additive) 선형 모델이다:
예측값 = w_temp·temperature + w_precip·precipitation + w_snow·snowfall
+ dayOfWeek[요일] + intercept - 피처별 독립 단변량 최소제곱 회귀 — 각 날씨 피처를 개별적으로 `calculateLinearRegression`으로 피팅. 다변량 공동 피팅(피처 간 상호작용 흡수)이 아니므로 "다중 선형회귀"가 아니라 피처별 회귀의 가산 합이다.
- 요일 가중치 — 요일평균/전체평균 비율을 `dayOfWeek[7]` 계수로.
- 개인/단체 채널 분리 학습 — `trainWeeklyIndividualPredictionModel`('0 2 * * 0')과 `trainWeeklyGroupPredictionModel`('10 2 * * 0')이 각각 다른 계수 세트를 만든다. 두 채널의 날씨 민감도가 다르다는 도메인 가정.
- 학습 데이터 — `fetchTrainingData`: 매일 20시 스냅샷을 일별 라벨로 사용, 토/일 제외(헌혈의 집 미운영), 경기 지역만.
모델 계수 (predictionTypes.ts · LinearModelCoefficients)
temperature 일평균 기온 — 헌혈량과 음/양의 상관을 데이터에서 직접 회귀
precipitation 일 강수량 — 우천 시 혈액원/이동식 헌혈버스 방문 감소 반영
snowfall 일 적설량 — 강설 시 외출 감소의 극단적 케이스
dayOfWeek[7] 요일별 평균 가중치 7개 — 월~금 패턴 차이
intercept 전체평균 절편 — 모든 피처 기여를 합산한 베이스라인
성능 평가의 한계 — 반드시 짚어야 할 지점
`ModelMetrics`는 in-sample R²/MAE/RMSE만 계산한다 (학습 데이터 재예측). 홀드아웃/시계열 교차검증이 없으므로 "새 데이터에 대한 일반화 성능"이 아니라 "피팅 정확도"를 보고하는 것. 면접에서 "R²가 높다"고 말하면 바로 "그건 in-sample 아니냐"는 꼬리질문이 나온다 — 미리 인정하고 개선 방향(time-series CV, holdout)을 같이 말하는 것이 좋다. confidence 필드도 사실 R² 재노출이다.
③ 데이터 수집 — 공공 API 스크래핑
functions/src/constants.ts: 대상은 혈액정보공개 시스템bissp.bloodinfo.net의HB10DataP3.do?DIV=2/3/4엔드포인트.DEFAULT_HEADERS에Referer: …dashmobile.do를 맞춰 모바일 페이지 흉내로 접근하는 전형적 공공데이터 스크래핑 패턴.hourlyScraper.ts가 지역별 individual/group/total을 파싱 →hourlyDatabase.ts의saveHourlySnapshot이 문서 id{date}_{hour}로 저장. 일중 덮어쓰기가 자연스럽다 (같은 날 10시 문서는 계속 갱신).firestore.rules: 읽기는 전면 허용(공개 통계), 쓰기는collectionMethod: scheduled/manual으로 제한,donationPredictions는 함수 전용(클라이언트 쓰기 금지) — 예측 무결성 확보.firestore.indexes.json: region+donationChannel+date, date+hour 복합 인덱스 — 쿼리 패턴에 맞춘 인덱스 설계.
④ 프런트엔드 — 조회 전용 아키텍처
- React Query(
src/hooks/useHistoricalData.ts) — dailySummaries/hourlyRecords/statistics 쿼리 키 + staleTime으로 서버 상태 캐싱. - AnalysisDashboard.tsx — 경기 필터링 후
mlPredictionService.predict(7일)을 useEffect로 호출(React Query 밖).donationPredictions문서를 읽어MLForecastChart(recharts ComposedChart: 실측 Line + 예측 + 신뢰구간 + 날씨 마커)로 표시. - 신뢰구간·영향 요인·날씨 시나리오는 클라이언트 휴리스틱(
calculateWeatherImpact등) — 서버가 준 것은 점예측뿐이다. "예측 불확실성 표시"가 UI 체감을 만든다. - Zustand는 테마 스토어 하나만 — 상태 관리 도구의 '용도별 분리' 사례로 언급하기 좋다.
⑤ 배경지식 — 이 시스템을 이해하는 데 필요한 개념
- 최소제곱 선형회귀 (OLS)
- Σ(관측−예측)²을 최소화하는 기울기/절편을 닫힌 해로 구하는 표준 회귀. 피처 1개면 단변량, 여러 피처를 동시에 쓰면 다변량. 이 프로젝트는 피처별 단변량 회귀의 합이라 다변량의 상호작용(예: "비 + 금요일" 시너지)은 못 잡는다 — 다만 계수 해석이 쉽고 적은 데이터에 강하다.
- in-sample vs holdout / 시계열 CV
- 학습에 쓴 데이터로 평가하면 낙관적으로 나온다. 시계열은 미래를 가리는 rolling-origin 검증이 정석. 이 프로젝트는 in-sample만 있음(위 ② 한계 참조).
- 관측 vs 예보의 도메인 갭
- 학습은 실측(ASOS), 추론은 예보(Open-Meteo) — 예보 오차가 곧 예측 오차로 전이된다. 두 소스를 쓰는 이유는 "미래 날씨"는 관측할 수 없기 때문. 프로덕션 ML에서 흔한 train/serve skew 사례.
- 서버리스 배치 예측
- 요청 시 예측(online inference)이 아니라 주기적으로 Firestore에 예측 문서를 미리 굽는 batch scoring. 프런트는 읽기만 하므로 비용/지연이 낮고, 모델 버전이 문서에 함께 저장된다.
- 혈액 수급이라는 도메인
- 헌혈량은 계절성(방학/연말 급감) + 요일성(주중 집중) + 기상 민감성을 가진 시계열. 대한적십자사가 일별 잔여량을 공개하지만 시간대별 데이터는 없어서, 이 시스템이 10–20시 매시간 스냅샷을 직접 수집해 만든 데이터셋 자체가 차별점.
⑥ 면접 한 줄 답변
- "예측 모델이 뭔가요?" — 날씨 피처(기온/강수/적설)별 독립 최소제곱 선형회귀 + 요일 가중치 + 절편의 가산 모델. 개인/단체 채널별로 주 1회 재학습하고, Open-Meteo 7일 예보로 일별 예측을 Firestore에 미리 계산해 둡니다.
- "왜 딥러닝이 아니라 선형회귀인가요?" — 확보한 데이터가 약 14개월의 일별 시계열이라 표본 수가 적어 고차 모델이 과적합되기 쉽고, 도메인(계절/요일/날씨 민감도)이 선형 가산 구조로 설명이 잘 되기 때문. 해석 가능한 계수도 실무 보고에 유리했습니다.
- "가장 아쉬운 점은?" — 평가가 in-sample R²/MAE/RMSE뿐이라 일반화 성능을 입증 못 한 점, 그리고 학습(ASOS 관측)-추론(Open-Meteo 예보) 소스가 달라 예보 오차가 그대로 전이되는 점. time-series holdout과 예보 오차 시뮬레이션이 다음 개선 과제입니다.
- "데이터는 어떻게 모았나요?" — 공개된 시간별 데이터가 없어서, Firebase 스케줄러가 10–20시 매시간 혈액정보공개 API를 스크래핑해 자체 시계열을 구축했습니다. 문서 id {date}_{hour}로 일중 갱신을 처리하고 쓰기 권한은 함수 전용으로 잠갔습니다.
- "red-toolkit의 예측과 차이는?" — how-many-blood는 지역 단위 총헌혈량-날씨 회귀, red-toolkit은 단체별 섭외 인원 예측(곱셈 팩터 휴리스틱 + 300건 이상 시 Ridge 로그선형). 같은 도메인의 다른 그래니얼리티를 각각 맞는 모델로 푼 사례입니다.