From 23a9202b9118eeb6cc7d3e42bca30d577038f9a3 Mon Sep 17 00:00:00 2001 From: windpacer Date: Fri, 5 Jun 2026 21:47:37 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9D=BC=EB=B3=84=20=EB=A6=AC=ED=8F=AC?= =?UTF-8?q?=ED=8A=B8=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20+=2010?= =?UTF-8?q?=EC=B0=A8=20=EC=8B=9C=EC=9A=B4=EC=A0=84=20caveat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - daily_report.py: 지정 기간 일별 운전값(피드·스팀·비율·온도) + 생산맵 예측품질 (predOP_MAE·within2%·OOD) 표. --prefix/--asset/--start/--end/--train-end. 6-2차 4~5월 검증: 신뢰구간 OP MAE 0.89%, 고부하(OOD) 외삽으로 최대 -10%. - run_column.py: 10차(101/102)는 본 dump 창 대부분 시운전(PROD 2~6%) 주석. 2026-06 생산 진입 → 데이터 누적/최신 dump 재반입 시 정상 분석 대상. Co-Authored-By: Claude Opus 4.8 --- scripts/analysis/daily_report.py | 78 ++++++++++++++++++++++++++++++++ scripts/analysis/run_column.py | 7 ++- 2 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 scripts/analysis/daily_report.py diff --git a/scripts/analysis/daily_report.py b/scripts/analysis/daily_report.py new file mode 100644 index 0000000..9db81dd --- /dev/null +++ b/scripts/analysis/daily_report.py @@ -0,0 +1,78 @@ +""" +컬럼 일별 리포트 — 지정 기간 일별 운전값 + 생산맵 예측품질(shadow). + +사용: python3 daily_report.py --prefix 62 --asset /ASSETS/P6 \ + --start 2026-04-01 --end 2026-06-01 --train-end 2026-04-01 + +train-end 이전 PROD로 생산맵 학습 → start~end 일별로 예측 OP vs 실제 비교(전진 shadow). +""" +import argparse +import numpy as np +import pandas as pd +import psycopg +from c6111_extract import roles_for, tag_frame, classify_phases, DSN +from c6111_shadow import SteamPredictor, FEATURES, SMOOTH +from sklearn.metrics import mean_absolute_error + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--prefix", default="62") + ap.add_argument("--asset", default="/ASSETS/P6") + ap.add_argument("--start", default="2026-04-01") + ap.add_argument("--end", default="2026-06-01") + ap.add_argument("--train-end", default="2026-04-01") + a = ap.parse_args() + + with psycopg.connect(DSN) as conn: + df = tag_frame(conn, roles_for(a.prefix, a.asset), a.asset) + df["mode"] = classify_phases(df) + for c in FEATURES: + df[c + "_s"] = df[c].rolling(SMOOTH, min_periods=1).median() + + prod = df[df["mode"] == "PROD"].copy() + prod = prod[(prod["feed"] > 50) & (prod["steam_flow"] > 10) & (prod["steam_op"] > 1)] + te = pd.Timestamp(a.train_end) + train = prod[prod["dtat"] < te] + print(f"전체 {df.dtat.min()}~{df.dtat.max()}, PROD {len(prod)}행. " + f"학습(<{a.train_end}) {len(train)}행") + + pred = SteamPredictor().fit(train) if len(train) > 500 else None + if pred: + lo, hi = train[FEATURES].quantile(0.01), train[FEATURES].quantile(0.99) + + s, e = pd.Timestamp(a.start), pd.Timestamp(a.end) + win = df[(df["dtat"] >= s) & (df["dtat"] < e)] + rows = [] + for day, g in win.groupby(win["dtat"].dt.date): + gp = g[(g["mode"] == "PROD") & (g["feed"] > 50) & (g["steam_flow"] > 10) & (g["steam_op"] > 1)] + r = dict(date=day, n=len(g), + PROD=round(100*(g["mode"] == "PROD").mean()), + feed=gp["feed"].median(), steam=gp["steam_flow"].median(), + ratio=round((gp["steam_flow"]/gp["feed"]).median(), 3) if len(gp) else np.nan, + T_C=round(gp["T_C"].median(), 2) if len(gp) else np.nan, + OP=round(gp["steam_op"].median(), 1) if len(gp) else np.nan) + if pred and len(gp) > 5: + Xs = gp[[c + "_s" for c in FEATURES]].values + po = pred.flow_to_op(pred.predict_flow(Xs)) + ao = gp["steam_op"].values + env = ((gp[FEATURES] >= lo) & (gp[FEATURES] <= hi)).all(axis=1) + r["predOP_MAE"] = round(mean_absolute_error(ao, po), 2) + r["within2%"] = round(100*np.mean(np.abs(po-ao) <= 2)) + r["OOD%"] = round(100*(~env).mean()) + rows.append(r) + R = pd.DataFrame(rows) + pd.set_option("display.width", 200) + print(f"\n=== {a.prefix} 일별 리포트 ({a.start}~{a.end}) ===") + R["date"] = R["date"].astype(str) + print(R.round(1).to_string(index=False)) + print("\n=== 요약 ===") + print(f" feed 중앙 {R.feed.median():.0f}, steam 중앙 {R.steam.median():.0f}, " + f"steam/feed비 중앙 {R.ratio.median():.3f}") + if "predOP_MAE" in R: + print(f" 예측 OP MAE 중앙 {R.predOP_MAE.median():.2f}%, " + f"within2% 중앙 {R['within2%'].median():.0f}%, OOD 중앙 {R['OOD%'].median():.0f}%") + + +if __name__ == "__main__": + main() diff --git a/scripts/analysis/run_column.py b/scripts/analysis/run_column.py index 25c200c..ea618cc 100644 --- a/scripts/analysis/run_column.py +++ b/scripts/analysis/run_column.py @@ -22,9 +22,12 @@ COLUMNS = [ ("81", "/ASSETS/P8", "C-8111 (8차, 단일 train)"), ("91", "/ASSETS/P9", "C-9111 (9-1차)"), ("92", "/ASSETS/P9", "C-9211 (9-2차)"), - ("101", "/ASSETS/P10", "C-10111 (10-1차)"), - ("102", "/ASSETS/P10", "C-10211 (10-2차)"), + ("101", "/ASSETS/P10", "C-10111 (10-1차) ※본 dump엔 시운전기간 多, 생산샘플 적음"), + ("102", "/ASSETS/P10", "C-10211 (10-2차) ※첫생산 5/26~, 이후 데이터 유효"), ] +# ※ 10차는 본 dump 창(2026-02~06) 대부분이 시운전이라 PROD 샘플이 적음(10-1 2%, 10-2 5/26~). +# 단 2026-06 현재 정상 생산 진입 — 데이터 누적되면(또는 최신 dump 재반입) 정상 분석 대상. +# 현재는 생산구간만 추려 쓰되 샘플 부족에 주의(맵 R²·트리거 변동성 클 수 있음). PREFIX_ASSET = {p: a for p, a, _ in COLUMNS}