Files
HC900-Crawler/scripts/analysis/c6111_rolling.py
windpacer 1bc46b1eb0 feat: 6-1차 컬럼 학습형 제어 오프라인 분석 (생산맵·shadow·startup)
신암정유 6-1차 측류 솔벤트 컬럼(C-6111) 실데이터로 오퍼레이터 모방 제어 분석:

- 데이터 추출기 tag_frame() (field_hist WIDE 포맷 디코드) + 운전모드 분류
- ① 생산맵: 스팀유량=f(피드,제품,목표T_C) 운전점 GBM R²0.99 (steam/feed≈0.73)
- shadow 백테스트: in-envelope 오퍼레이터 OP 94% 모방, OOD 게이트→폴백
- 롤링 재학습: 새 로드레짐 적응 (5월 OP MAE 3.9→1.2%)
- 캠페인내 트림: 컬럼 자기제어, 피드백 미미 → 전향 맵이 제어 지배
- ② START-UP 절차: 레시피 + 제품컷인 트리거(reb-A 84.5℃, ΔT 2℃, 조건기반)

문서: 설계·진행 플랜 + 남은 작업(형제확장·shutdown·assist·live포팅) 작업지시서.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 17:33:17 +09:00

79 lines
3.7 KiB
Python

"""
C-6111 롤링(walk-forward) 재학습 — OOD/외삽 바이어스 해소 데모 (플랜 §16.7-(1)).
held-out 5월을 하루씩 전진하며 '그 날 이전 전체 이력(expanding window)'으로 매일 재학습→그 날 예측.
정적 모델(2~4월 고정)의 +4% 외삽 바이어스가 모델이 5월 저부하 데이터를 흡수하며
사라지는지(적응 곡선) + OOD 비율이 떨어지는지 확인. 입력 평활은 인과(trailing).
"""
import numpy as np
import pandas as pd
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from sklearn.metrics import mean_absolute_error
from c6111_shadow import SteamPredictor, FEATURES, BASE, SMOOTH
HELDOUT_START = "2026-05-01"
RETRAIN_EVERY = "1D"
def main():
df = pd.read_pickle(BASE + "c6111_data.pkl")
df = df[df["mode"] == "PROD"].copy()
df = df[(df["feed"] > 50) & (df["steam_flow"] > 10) & (df["steam_op"] > 1)
& df[FEATURES + ["steam_op"]].notna().all(axis=1)].sort_values("dtat")
# 인과(trailing) 평활 — 미래누설 없음
for c in FEATURES:
df[c + "_s"] = df[c].rolling(SMOOTH, min_periods=1).median()
ho = pd.Timestamp(HELDOUT_START)
days = pd.date_range(ho, df["dtat"].max(), freq=RETRAIN_EVERY)
# 정적 모델: 5월 이전 전체로 1회 학습
static = SteamPredictor().fit(df[df["dtat"] < ho])
slo, shi = (df[df["dtat"] < ho][FEATURES].quantile(0.01),
df[df["dtat"] < ho][FEATURES].quantile(0.99))
rows = []
for d0, d1 in zip(days[:-1], days[1:]):
day = df[(df["dtat"] >= d0) & (df["dtat"] < d1)]
if len(day) < 30:
continue
train = df[df["dtat"] < d0] # expanding: 그 날 이전 전체
roll = SteamPredictor().fit(train)
lo, hi = train[FEATURES].quantile(0.01), train[FEATURES].quantile(0.99)
Xs = day[[c + "_s" for c in FEATURES]].values
ao = day["steam_op"].values
po_r = roll.flow_to_op(roll.predict_flow(Xs))
po_s = static.flow_to_op(static.predict_flow(Xs))
ood_r = (~((day[FEATURES] >= lo) & (day[FEATURES] <= hi)).all(axis=1)).mean()
rows.append(dict(day=d0,
mae_roll=mean_absolute_error(ao, po_r),
mae_static=mean_absolute_error(ao, po_s),
w2_roll=np.mean(np.abs(po_r - ao) <= 2) * 100,
w2_static=np.mean(np.abs(po_s - ao) <= 2) * 100,
ood_roll=ood_r * 100))
r = pd.DataFrame(rows)
print(f"=== 5월 held-out, 일별 walk-forward 재학습 ({len(r)}일) ===")
print(f"정적 모델 : OP MAE {r.mae_static.mean():.2f}% |Δ|≤2% {r.w2_static.mean():.1f}%")
print(f"롤링 모델 : OP MAE {r.mae_roll.mean():.2f}% |Δ|≤2% {r.w2_roll.mean():.1f}%")
print(f"롤링 OOD 비율: 첫주 {r.head(7).ood_roll.mean():.0f}% → 마지막주 {r.tail(7).ood_roll.mean():.0f}%")
print("\n일별(요약):")
print(r[["day", "mae_static", "mae_roll", "w2_roll", "ood_roll"]]
.assign(day=r.day.dt.strftime("%m-%d")).round(1).to_string(index=False))
fig, ax = plt.subplots(2, 1, figsize=(14, 8), sharex=True)
ax[0].plot(r.day, r.mae_static, "r.-", label="static (Feb-Apr model)")
ax[0].plot(r.day, r.mae_roll, "g.-", label="rolling retrain")
ax[0].axhline(2, color="gray", ls=":", label="2% 허용")
ax[0].set_ylabel("OP MAE %"); ax[0].legend(); ax[0].set_title("Rolling vs static — adaptation over May")
ax[1].plot(r.day, r.ood_roll, "b.-"); ax[1].set_ylabel("rolling OOD %")
ax[1].set_title("OOD fraction (학습 envelope 밖) — 5월 데이터 흡수하며 감소")
fig.tight_layout(); fig.savefig(BASE + "c6111_rolling.png", dpi=95)
print(f"\n플롯 저장: {BASE}c6111_rolling.png")
if __name__ == "__main__":
main()