Skip to content

docs: 파이프라인 방법론 근거 대장 + 분석 스크립트 레포 이관 - #74

Open
choigod1023 wants to merge 7 commits into
devfrom
docs/methodology-evidence
Open

docs: 파이프라인 방법론 근거 대장 + 분석 스크립트 레포 이관#74
choigod1023 wants to merge 7 commits into
devfrom
docs/methodology-evidence

Conversation

@choigod1023

Copy link
Copy Markdown
Contributor

왜 필요한가

지금까지 검증 근거가 임시 폴더 스크립트와 대화에만 있어 재현이 불가능했습니다. 레포 전수 검색 결과입니다.

근거 레포 내 언급
Bühlmann-Straub 16곳 ✓
Syntetos-Boylan 2곳 ✓
Bergmeir & Benítez 1곳
Johansen 공적분 0
Granger 인과성 0
Bonferroni 보정 0
Koenker & Bassett 0

산학협력 결과물인데 근거가 휘발성 위치에 있으면 나중에 아무도 재현할 수 없습니다.

추가

docs/2026-08-11_01_METHODOLOGY_EVIDENCE.md

9개 절. 각 설계 결정을 문헌 근거(저자·연도·학술지·권:페이지·해당 절)outputs/ 실측 수치 로 연결했습니다.

원문 인용은 싣지 않았습니다(저작권). 출처를 특정해 검증자가 원문을 찾아가게 하고, 주장은 우리 맥락으로 서술했습니다.

사전지정(confirmatory) 과 탐색(exploratory) 가설을 구분해 기술했습니다. 이 구분 없이 p값을 보고하면 다중검정으로 부풀려진 결과를 확정 사실처럼 말하게 됩니다.

철회한 권고도 왜 철회했는지 남겼습니다. 리드타임 55일 권고는 M(품절→입고)을 최적화한 값이고, 클라이언트가 정의한 L(발주→입고)이 아니었습니다. 이런 게 기록에 없으면 나중에 누가 그 숫자를 다시 주워 씁니다.

scripts/analysis/ — 분석 6종 이관

lead_time_optimal · lead_time_shrinkage · lead_time_sensitivity · vecm_transmission · material_lag_all · price_to_stock

임시 폴더에 있어 사라질 뻔했고, C:\Users\user\TeamLex-ai 절대경로가 박혀 있어 다른 PC 에서는 실행조차 안 됐습니다. __file__ 기준 상대경로로 교체하고 실제 실행까지 확인했습니다.

src/procurement/lead_time_collector.py — 조달청 납품요구 수집기

원장에는 발주일 필드가 없어(날짜 컬럼이 재고마감일 하나뿐) L 을 식별할 수 없습니다. 조달청 납품요구는 발주일과 납기가 모두 있는 유일한 공개 자료입니다.

L_계약 = maxDlvrTmlmtDate(납품기한) − dlvrReqRcptDate(납품요구접수일)
  • 일 할당량 소진(X-RateLimit-Remaining: 0)을 rate limit 과 구분해 즉시 중단합니다. 구분하지 않아 30·60·90·120·150·180초, 총 8분을 헛되이 대기한 적이 있습니다
  • 진행 파일로 중단 지점부터 이어받습니다. 종전 방식("마지막 달은 무조건 미완료")은 재실행마다 정상 완료된 달까지 버려서 7월치 1,054건이 날아갔습니다

중간 결과 (2024-01~06, 유효 10,109건)

p25 = 30    median = 30    p75 = 30    p90 = 60      (현행 fallback 15일)

의료 소모품은 분산 없이 30일 고정입니다 — 의료용살충제(n=3,289), 백신(n=3,214), 저출력심장충격기(n=250) 모두 p25=median=p90=30. 리드타임 15→30일이면 발주량이 2.03배이므로 재고 정책에 직접 영향이 있습니다.

한계: L_계약계약상 납기지 실제 도착일이 아닙니다. 원장 기관코드가 비식별화(P;485, R4<4<)되어 있어 입고일과 대조가 불가능하므로 납기 초과분은 검증할 수 없습니다.

scripts/apply_pending_migrations.py

backend PR #62/#68/#72 의 DDL 이 프로덕션 DB 에 적용되지 않아 /inventory-policy 가 500 이었습니다(column inv.order_suppress_reason does not exist). users.is_active 부재는 로그인도 깨뜨렸습니다. 멱등 ALTER 7종을 적용하는 스크립트입니다. 적용 완료 확인했습니다.

검증

  • scripts/analysis/lead_time_shrinkage.py 레포에서 실행 성공
  • 전체 테스트 230 passed (실패 3건은 기존 WSL bash 충돌, 본 변경과 무관)

🤖 Generated with Claude Code

choigod1023 and others added 6 commits August 7, 2026 16:11
training.py의 _build_estimator()가 파라미터를 손으로 고정해둔 상태(n_estimators=160,
learning_rate=0.05, num_leaves=31 등)라, 같은 시간순 분할(TRAIN/VALID)로 WAPE를
목적함수 삼아 Optuna로 탐색하는 스크립트를 추가한다. TEST 구간은 탐색에 쓰지 않는다.

사용법: python -m src.modeling.tune_hyperparameters --variant stock_model_a_usage_only --n-trials 200
결과는 outputs/tuned_hyperparameters_<variant>.json 에 저장되며, best_params를
training.py의 _build_estimator() parameters 딕셔너리에 검토 후 손으로 반영하는 방식이다.

합성 더미 데이터로 배선(분할→피처선택→전처리→학습→WAPE→optuna 루프) 검증 완료.
실제 raw_stock 데이터로 돌린 실측 결과는 아직 없음 — 원본 DAT 파일 필요.
리뷰에서 지적된 5개 항목을 반영한다.

1. 분할 오류 — `_load_feature_table()` 은 VALID_END 까지만 읽는데
   `split_time_series()` 는 TEST 가 비면 예외를 낸다. 실데이터에서 항상
   `ValueError: Empty split detected (test=0)` 로 탐색 시작 전에 중단됐다.
   `split_time_series()` 호출을 제거하고 운영 학습과 같은
   `select_training_window()` 로 fold 별 학습셋을 만든다. TEST 를 읽도록
   바꾸지 않았으므로 오염 위험은 없다.

2. rolling validation — 단일 분할 대신 `config.VALIDATION_FOLDS`
   (2025_q1, 2025_q2) 를 쓰고, fold 예측을 합친 통합 WAPE 를 objective 로
   삼는다. fold 평균이 아니라 예측을 풀링해 계산한다. 과거자료 가중치는
   `load_historical_training_policy()` 정책값을 `training_sample_weights()`
   로 적용한다. 학습 구간이 검증 구간을 침범하면 예외를 낸다.

3. 실데이터 검증 — 단위 테스트 8건을 추가하고 3 trial smoke test 를
   실데이터로 완주했다. fold 2025_q1 의 train=2,335,363 /
   historical=952,002 로 `historical_training_policy.json` 기록값과 일치하며,
   현재 고정 파라미터의 통합 WAPE 는 39.0464% 로 리뷰의 챔피언 값 39.11% 와
   일치한다. 즉 기존 검증 결과와 같은 조건이다.

4. 외부 신호 게이트 — `_missing_external_signal()` 을 재사용해 뉴스·원자재·
   Module C 신호가 전부 0 인 variant 는 탐색 자체를 거부한다(fail closed).

5. 트리 수 혼동 — `search_max_estimators`(탐색 상한 2000) 와
   `selected_n_estimators`(early stopping 이 고른 실제 값) 를 분리하고
   `best_params.n_estimators` 에는 후자를 넣는다. fold 별 WAPE/MAE/RMSE/BIAS,
   정책 버전, 매핑 버전, 학습 기간, seed 를 리포트에 함께 남긴다.

추가로 현재 고정 파라미터를 같은 fold 로 자동 측정해 리포트에 baseline 으로
저장하므로, 별도 비교 스크립트 없이 개선폭을 확인할 수 있다. trial 마다
fold 별 WAPE·최고값·경과시간·ETA 를 로그로 남긴다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sehyeon03 님이 병합 보류 사유로 든 두 가지를 고친다.

1) 저장된 파라미터로 결과를 재현할 수 없던 문제

   best_params = {**study.best_params, "n_estimators": ...} 였다.
   study.best_params 에는 Optuna 가 **제안한 값만** 들어가므로 objective ·
   subsample_freq · random_state · n_jobs · force_col_wise · verbosity 가 전부
   빠졌다. LightGBM 기본값이 subsample_freq=0 이라, 저장된 JSON 을 training.py 로
   옮기면 subsample=0.79976 이 **적용되지 않는다**. 보고한 WAPE 가 재현되지 않는다.

   공용 빌더 build_estimator_params(searched, objective, n_estimators, seed) 를
   두고 tuner · baseline · JSON 복원이 모두 이걸 거치게 했다. 저장 결과에는
   best_params(전체) · searched_params(탐색값만) · fixed_params 를 분리해 남긴다.

   리뷰에서 지적되지 않은 문제를 하나 더 발견했다. _baseline_params 에만
   histogram_pool_size=256 이 있고 _suggest_params 에는 없었다. 즉 baseline 과
   tuned 가 애초에 동일 조건이 아니었다. 공용 빌더로 합쳐 같은 값을 쓰게 했다.

2) 외부 신호 게이트에 validation 정보가 섞이던 문제

   _prepare_folds() 가 _missing_external_signal(feature_table, options) 을
   validation 구간까지 포함한 전체 테이블에 호출했다. 학습 구간엔 신호가 없고
   validation 에만 있어도 통과한다.

   fold 를 먼저 만들고, 각 fold 의 train 행으로만 검사하도록 바꿨다.
   실패 메시지에 어느 fold 인지도 남긴다.

테스트 5건 추가 (tests/test_tune_hyperparameters.py)
  - 고정 파라미터가 저장값에 살아남는지
  - LGBMRegressor 에 넘겼을 때 subsample 이 실제로 활성인지(subsample_freq=1)
  - JSON round-trip 후 동일 파라미터가 나오는지
  - baseline 과 tuned 가 같은 고정값을 쓰는지
  - 게이트가 전체 테이블이 아니라 fold train 행으로 호출되는지

검증: python3 -m unittest tests.test_tune_hyperparameters → 13건 전부 통과.

실데이터 smoke test 는 아직 못 붙였다. feature table 생성에 품목 표준화 사슬이
선행돼야 하는데(item_normalization → enrichment → material → integrated),
공공데이터 인증키를 확보해 지금 실행 중이다. 결과가 나오면 별도로 보고하겠다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
리뷰 지적 2번 대응. tune_hyperparameters._baseline_params() 가 파라미터를
자체 나열하면서 subsample_freq=1 을 넣고 있었다. 운영(_build_estimator)은
subsample_freq 를 지정하지 않아 LightGBM 기본값 0 이고, 따라서 subsample=0.9
가 실제로는 비활성이다. 두 조건이 달라서 "baseline 대비 개선폭"에 파라미터
효과와 동작 변경이 섞여 들어갔다.

- training.production_lgbm_params() 를 운영 파라미터의 단일 정의로 두고
  _build_estimator() 와 tuner baseline 이 같은 함수를 쓴다.
- baseline 은 subsample_freq 미지정 = 현행 운영 동작 그대로 유지한다.
  탐색 쪽 subsample_freq=1 은 파라미터 탐색이 아니라 동작 변경이므로
  보고 시 분리해서 기술한다.
- VALIDATION_FOLDS 2개(6개월) → 4개(12개월). fold 수가 적으면 WAPE 차이가
  특정 분기 계절성에 좌우된다(Bergmeir & Benitez 2012, Information Sciences
  191:192-213). fold 폭 3개월과 rolling-origin 구조는 유지한다.
- _fit_and_predict 의 warnings.simplefilter("ignore") 를 lightgbm UserWarning
  으로 좁혔다. 데이터 이상 경고까지 삼키고 있었다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
정책 파일(supply_risk_level_policy.json)은 lead_time_estimation.method 를
"stockout_duration_p25" 로 선언하지만, prediction._finalize_predictions 와
recursive_inventory_simulation._apply_policy 가 add_inventory_recommendations 를
lead_time_days_col 없이 호출하고 있었다. _resolve_lead_time 은 컬럼이 없으면
전 행을 NaN 으로 보고 fallback 을 적용하므로, 실제로는 **모든 품목이 15일**로
고정되어 있었다. 품목별 추정은 선언만 되고 동작하지 않았다.

- 두 호출부에 lead_time_days_col / review_period_days_col 을 명시적으로 전달.
  컬럼이 없으면 종전과 동일하게 fallback 이므로 동작 변경은 없다.
- 대시보드: 입력한 리드타임이 하한 미만이라 fallback 되거나 상한을 넘어
  잘린 경우를 경고로 표시하고, 적용된 값과 보호기간 계산을 함께 보여준다.
  종전에는 기본값 0 이 조용히 15일이 되어 "바꿨는데 발주량이 그대로"로 보였다.
- 회귀 테스트 추가. 호출부가 컬럼을 넘기는지까지 고정한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
지금까지 검증 근거가 임시 폴더 스크립트와 대화에만 있어 재현이 불가능했다.
레포 전수 검색 결과 Johansen·Granger·Bonferroni·Koenker-Bassett·newsvendor 는
언급이 0건이었다. 산학협력 결과물인데 근거가 휘발성 위치에 있으면 안 된다.

docs/2026-08-11_01_METHODOLOGY_EVIDENCE.md
- 각 설계 결정을 문헌 근거(저자·연도·학술지·권:페이지·절)와 실측 수치로 연결
- 원문 인용은 싣지 않는다(저작권). 출처를 특정해 검증자가 원문을 찾아가게 한다
- 사전지정(confirmatory) 가설과 탐색(exploratory) 가설을 구분해 기술
- 철회한 권고(리드타임 55일)를 왜 철회했는지까지 남긴다. M(품절→입고)을
  최적화한 값이라 클라이언트 정의인 L(발주→입고)이 아니었다

scripts/analysis/ — 임시 폴더에 있던 분석 6종 이관
- 절대경로(C:\Users\user\TeamLex-ai)가 박혀 있어 다른 PC 에서 실행 불가였다.
  __file__ 기준 상대경로로 교체하고 실행까지 확인했다

src/procurement/lead_time_collector.py — 조달청 납품요구 수집기
- L_계약 = 납품기한 − 납품요구접수일. 원장에는 발주일이 없어 L 을 식별할 수 없다
- 일 할당량 소진(X-RateLimit-Remaining: 0)을 레이트리밋과 구분해 즉시 중단.
  구분하지 않아 8분을 헛되이 대기한 적이 있다
- 진행 파일로 중단 지점부터 이어받는다. 종전 방식은 재실행마다 정상 완료된
  달까지 버려서 7월치 1,054건이 날아갔다

scripts/apply_pending_migrations.py — backend PR #62/#68/#72 의 밀린 DDL 적용

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
README 의 배치 순서는 단계 목록일 뿐이라 "돌긴 도는데 안에 뭐가 들었는지" 를
알 수 없다. 오늘 같은 유형의 사고를 세 번 겪은 원인이 거기에 있다.

- 4개 층의 의존 관계를 mermaid 로 그렸다. 합류 지점이 feature_engineering
  이라는 점, 거기서 조인이 실패하면 0으로 채워진 채 성공으로 끝난다는 점을
  구조에 드러냈다.
- 각 구성요소가 **지금 실제로 어떤 데이터로 돌아가는지** 를 근거 파일과 함께
  적었다. 원자재·관세청은 실데이터, 뉴스는 수집 중이라는 구분이 문서에 없으면
  다음 사람이 또 합성 데이터를 실데이터로 읽는다.
- 반복된 실패 유형(조용한 성공) 3건을 표로 정리하고, 지금까지 붙인 방어막과
  **아직 없는 것**(피처테이블 생성 시 비영 0% 를 실패로 처리하는 게이트)을
  구분했다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@choigod1023

Copy link
Copy Markdown
Contributor Author

커밋 86112e2 추가했습니다.

docs/2026-08-11_02_PIPELINE_TOPOLOGY.md — 파이프라인 구성·실행 상태

근거 대장이 "각 결정의 문헌 근거" 라면, 이 문서는 "지금 무엇이 어떤 데이터로 돌고 있는가" 입니다.

  • 4개 층 의존 관계를 mermaid 로 도식화. 합류 지점이 feature_engineering 이고, 거기서 조인이 실패하면 0으로 채워진 채 성공으로 끝난다는 점을 구조에 드러냈습니다
  • 구성요소별 현재 데이터 출처를 명시 — 원자재·관세청은 실데이터, 뉴스는 수집 중. 이 구분이 문서에 없으면 다음 사람이 또 합성 데이터를 실데이터로 읽습니다
  • 반복된 실패 유형(조용한 성공) 3건 을 표로 정리하고, 붙인 방어막 5개와 아직 없는 것 을 구분했습니다

아직 없는 것으로 적어둔 항목이 리뷰에서 지적하신 것과 같습니다 — 피처테이블 생성 시점에 "이 신호 비영 0%" 를 실패로 처리하는 게이트. 지금은 경고만 남고 학습이 그대로 진행됩니다. --require-external-signals 같은 엄격 모드를 후속 후보로 문서에 남겨 두었습니다.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant