Files
HC900-Crawler/scripts/analysis/c6111_shadow.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

100 lines
4.7 KiB
Python

"""
C-6111 Shadow 예측기 — 히스토리 리플레이 백테스트 (플랜 §7 shadow 진입).
학습기간 운전점으로 `스팀유량=f(피드,제품,목표T_C)` 학습 → held-out 미래기간을
매 시점 리플레이하여 예측 스팀→(밸브 역특성)→예측 OP 를 산출, **실제 오퍼레이터 OP와 비교**.
"이 예측기를 shadow로 돌렸다면 오퍼레이터 손과 얼마나 일치했나" 를 정직 검증.
선행: c6111_data.pkl. 포팅대상(추후 C# live shadow)은 동일 로직.
"""
import numpy as np
import pandas as pd
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.metrics import r2_score, mean_absolute_error
BASE = "/home/windpacer/projects/hc900_ax/scripts/analysis/"
FEATURES = ["feed", "product", "T_C"] # 깨끗한 인과/목표 입력 (§16.6)
SMOOTH = 40 # 입력 평활 20분(운전점 성격 유지)
TRAIN_FRAC = 0.70 # 앞 70% 기간 학습, 뒤 30% held-out shadow
class SteamPredictor:
"""운전점 학습 + 밸브 역특성(flow→OP)."""
def fit(self, df_train):
ops = (df_train.set_index("dtat").resample("6h").median(numeric_only=True)
.dropna(subset=["steam_flow", "feed"]))
ops = ops[ops["feed"] > 50]
self.model = GradientBoostingRegressor(n_estimators=200, max_depth=2,
learning_rate=0.05, random_state=0)
self.model.fit(ops[FEATURES].values, ops["steam_flow"].values)
# 밸브 역특성: OP = poly(flow) (단조, 3차)
self.inv = np.polyfit(df_train["steam_flow"], df_train["steam_op"], 3)
return self
def predict_flow(self, X):
return self.model.predict(X)
def flow_to_op(self, flow):
return np.clip(np.polyval(self.inv, flow), 0, 100)
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")
# 입력 평활 (실제 shadow도 노이즈 평활 사용)
for c in FEATURES:
df[c + "_s"] = df[c].rolling(SMOOTH, min_periods=1, center=True).median()
cut = df["dtat"].quantile(TRAIN_FRAC)
tr, te = df[df["dtat"] <= cut], df[df["dtat"] > cut]
print(f"학습 {tr.dtat.min()}~{tr.dtat.max()} ({len(tr)}) "
f"shadow(held-out) {te.dtat.min()}~{te.dtat.max()} ({len(te)})")
pred = SteamPredictor().fit(tr)
# OOD(학습 운전envelope 밖) 게이트: 입력이 학습 1~99% 범위 밖이면 '저신뢰→오퍼레이터 폴백'
lo, hi = tr[FEATURES].quantile(0.01), tr[FEATURES].quantile(0.99)
print(f"학습 envelope: " + ", ".join(f"{c}[{lo[c]:.0f},{hi[c]:.1f}]" for c in FEATURES))
def in_env(d):
return ((d[FEATURES] >= lo) & (d[FEATURES] <= hi)).all(axis=1)
for name, d in [("학습기간", tr), ("★held-out shadow", te)]:
Xs = d[[c + "_s" for c in FEATURES]].values
pf = pred.predict_flow(Xs)
po = pred.flow_to_op(pf)
ao = d["steam_op"].values
env = in_env(d).values
within = np.mean(np.abs(po - ao) <= 2.0) * 100
print(f"\n[{name}] OOD(범위밖)={100*(~env).mean():.1f}%")
print(f" 전체 OP MAE={mean_absolute_error(ao,po):.2f} |Δ|≤2%={within:.1f}%")
if env.sum() > 50:
print(f" in-envelope OP MAE={mean_absolute_error(ao[env],po[env]):.2f} "
f"|Δ|≤2%={np.mean(np.abs(po[env]-ao[env])<=2)*100:.1f}% ← shadow가 신뢰구간에서 조언")
d = d.assign(pred_flow=pf, pred_op=po, ood=~env)
if name.startswith(""):
te = d
# 플롯: held-out 시계열 오버레이 + OP 비교 + 오차분포
fig, ax = plt.subplots(3, 1, figsize=(16, 11))
s = te.iloc[::20]
ax[0].plot(s.dtat, s["steam_flow"], lw=.6, label="actual steam flow")
ax[0].plot(s.dtat, s["pred_flow"], lw=.6, c="r", label="predicted")
ax[0].set_title("held-out shadow: steam flow actual vs predicted"); ax[0].legend(fontsize=8)
ax[1].plot(s.dtat, s["steam_op"], lw=.6, label="actual operator OP")
ax[1].plot(s.dtat, s["pred_op"], lw=.6, c="r", label="predicted OP")
ax[1].set_ylabel("OP %"); ax[1].set_title("operator OP vs shadow-predicted OP"); ax[1].legend(fontsize=8)
err = te["pred_op"] - te["steam_op"]
ax[2].hist(err, bins=80); ax[2].axvline(0, c="k", lw=.5)
ax[2].set_title(f"OP error (pred-actual): median {err.median():+.2f}%, std {err.std():.2f}%")
fig.tight_layout(); fig.savefig(BASE + "c6111_shadow.png", dpi=95)
print(f"\n플롯 저장: {BASE}c6111_shadow.png")
if __name__ == "__main__":
main()