feat: 일별 리포트 스크립트 + 10차 시운전 caveat
- 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 <noreply@anthropic.com>
This commit is contained in:
78
scripts/analysis/daily_report.py
Normal file
78
scripts/analysis/daily_report.py
Normal file
@@ -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()
|
||||||
@@ -22,9 +22,12 @@ COLUMNS = [
|
|||||||
("81", "/ASSETS/P8", "C-8111 (8차, 단일 train)"),
|
("81", "/ASSETS/P8", "C-8111 (8차, 단일 train)"),
|
||||||
("91", "/ASSETS/P9", "C-9111 (9-1차)"),
|
("91", "/ASSETS/P9", "C-9111 (9-1차)"),
|
||||||
("92", "/ASSETS/P9", "C-9211 (9-2차)"),
|
("92", "/ASSETS/P9", "C-9211 (9-2차)"),
|
||||||
("101", "/ASSETS/P10", "C-10111 (10-1차)"),
|
("101", "/ASSETS/P10", "C-10111 (10-1차) ※본 dump엔 시운전기간 多, 생산샘플 적음"),
|
||||||
("102", "/ASSETS/P10", "C-10211 (10-2차)"),
|
("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}
|
PREFIX_ASSET = {p: a for p, a, _ in COLUMNS}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user