Refactor app.py to integrate threshold diagnosis and trend detection features, enhancing Gradio UI for improved user experience.
Browse files
app.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
| 1 |
-
#
|
|
|
|
| 2 |
import gradio as gr
|
| 3 |
import pandas as pd
|
| 4 |
import numpy as np
|
|
|
|
| 5 |
import json
|
| 6 |
import os
|
| 7 |
|
| 8 |
-
#
|
| 9 |
def judge_status(value, ll, l, h, hh):
|
| 10 |
if pd.notna(ll) and value < ll:
|
| 11 |
return "LOW-LOW"
|
|
@@ -23,23 +25,8 @@ def convert_value(v):
|
|
| 23 |
return v.item()
|
| 24 |
return float(v) if isinstance(v, (np.floating, float)) else int(v) if isinstance(v, (np.integer, int)) else v
|
| 25 |
|
| 26 |
-
#
|
| 27 |
-
def diagnose_process_range(
|
| 28 |
-
try:
|
| 29 |
-
df = pd.read_csv(csv_file.name, header=[0, 1, 2])
|
| 30 |
-
timestamp_col = df.iloc[:, 0]
|
| 31 |
-
df = df.drop(df.columns[0], axis=1)
|
| 32 |
-
df.insert(0, "timestamp", timestamp_col)
|
| 33 |
-
df["timestamp"] = pd.to_datetime(df["timestamp"], errors="coerce")
|
| 34 |
-
|
| 35 |
-
thresholds_df = pd.read_excel(excel_file.name)
|
| 36 |
-
thresholds_df["Important"] = thresholds_df["Important"].astype(str).str.upper().map({"TRUE": True, "FALSE": False})
|
| 37 |
-
for col in ["LL", "L", "H", "HH"]:
|
| 38 |
-
if col in thresholds_df.columns:
|
| 39 |
-
thresholds_df[col] = pd.to_numeric(thresholds_df[col], errors="coerce")
|
| 40 |
-
except Exception as e:
|
| 41 |
-
return None, None, None, f"❌ 入力ファイルの読み込みに失敗しました: {e}", None
|
| 42 |
-
|
| 43 |
try:
|
| 44 |
target_time = pd.to_datetime(datetime_str)
|
| 45 |
except Exception:
|
|
@@ -76,7 +63,11 @@ def diagnose_process_range(csv_file, excel_file, process_name, datetime_str, win
|
|
| 76 |
["LOW-LOW", "LOW", "OK", "HIGH", "HIGH-HIGH"], fill_value=0
|
| 77 |
)
|
| 78 |
status_ratio = (status_counts / total * 100).round(1)
|
| 79 |
-
result_df_all = pd.DataFrame({
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
important_results = [r for r in all_results if r["重要項目"]]
|
| 82 |
if important_results:
|
|
@@ -85,7 +76,11 @@ def diagnose_process_range(csv_file, excel_file, process_name, datetime_str, win
|
|
| 85 |
["LOW-LOW", "LOW", "OK", "HIGH", "HIGH-HIGH"], fill_value=0
|
| 86 |
)
|
| 87 |
status_ratio_imp = (status_counts_imp / total_imp * 100).round(1)
|
| 88 |
-
result_df_imp = pd.DataFrame({
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
else:
|
| 90 |
result_df_imp = pd.DataFrame(columns=["状態", "件数", "割合(%)"])
|
| 91 |
status_ratio_imp = pd.Series(dtype=float)
|
|
@@ -126,32 +121,122 @@ def diagnose_process_range(csv_file, excel_file, process_name, datetime_str, win
|
|
| 126 |
|
| 127 |
return result_df_all, result_df_imp, result_df_imp_items, summary, result_json
|
| 128 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
return
|
| 133 |
|
| 134 |
-
#
|
| 135 |
-
|
| 136 |
-
|
| 137 |
|
|
|
|
|
|
|
|
|
|
| 138 |
|
| 139 |
-
#
|
| 140 |
-
|
| 141 |
-
gr.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
with gr.Tabs():
|
| 144 |
-
# Tab1: 閾値診断
|
| 145 |
with gr.Tab("閾値診断"):
|
| 146 |
-
with gr.Row():
|
| 147 |
-
csv_input = gr.File(label="CSVファイル", file_types=[".csv"], type="filepath")
|
| 148 |
-
excel_input = gr.File(label="Excel閾値ファイル", file_types=[".xlsx"], type="filepath")
|
| 149 |
-
|
| 150 |
process_name = gr.Textbox(label="プロセス名", value="E018-A012_除害RO")
|
| 151 |
datetime_str = gr.Textbox(label="診断基準日時", value="2025/8/1 1:05")
|
| 152 |
window_minutes = gr.Number(label="さかのぼる時間幅(分)", value=60)
|
| 153 |
run_btn = gr.Button("診断を実行")
|
| 154 |
-
|
| 155 |
result_df_all = gr.Dataframe(label="全項目の状態集計結果")
|
| 156 |
result_df_imp = gr.Dataframe(label="重要項目全体の状態集計結果")
|
| 157 |
result_df_imp_items = gr.Dataframe(label="重要項目ごとの状態集計結果")
|
|
@@ -160,21 +245,28 @@ with gr.Blocks(css="body {overflow-y: auto !important;}") as demo:
|
|
| 160 |
|
| 161 |
run_btn.click(
|
| 162 |
diagnose_process_range,
|
| 163 |
-
inputs=[
|
| 164 |
outputs=[result_df_all, result_df_imp, result_df_imp_items, summary_output, json_output]
|
| 165 |
)
|
| 166 |
|
| 167 |
-
# Tab2: 傾向検出
|
| 168 |
with gr.Tab("傾向検出"):
|
| 169 |
-
gr.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
|
|
|
|
|
|
| 174 |
|
| 175 |
-
# --- 実行モード選択 ---
|
| 176 |
if __name__ == "__main__":
|
| 177 |
-
use_mcp = os.getenv("USE_MCP",
|
| 178 |
if use_mcp:
|
| 179 |
demo.launch(mcp_server=True)
|
| 180 |
else:
|
|
|
|
| 1 |
+
# 統合版: 閾値診断 + 傾向検出 (CSV/Excel共通入力, MCP対応)
|
| 2 |
+
|
| 3 |
import gradio as gr
|
| 4 |
import pandas as pd
|
| 5 |
import numpy as np
|
| 6 |
+
from sklearn.linear_model import LinearRegression
|
| 7 |
import json
|
| 8 |
import os
|
| 9 |
|
| 10 |
+
# ===== 共通ユーティリティ =====
|
| 11 |
def judge_status(value, ll, l, h, hh):
|
| 12 |
if pd.notna(ll) and value < ll:
|
| 13 |
return "LOW-LOW"
|
|
|
|
| 25 |
return v.item()
|
| 26 |
return float(v) if isinstance(v, (np.floating, float)) else int(v) if isinstance(v, (np.integer, int)) else v
|
| 27 |
|
| 28 |
+
# ===== 閾値診断処理 =====
|
| 29 |
+
def diagnose_process_range(df, thresholds_df, process_name, datetime_str, window_minutes):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
try:
|
| 31 |
target_time = pd.to_datetime(datetime_str)
|
| 32 |
except Exception:
|
|
|
|
| 63 |
["LOW-LOW", "LOW", "OK", "HIGH", "HIGH-HIGH"], fill_value=0
|
| 64 |
)
|
| 65 |
status_ratio = (status_counts / total * 100).round(1)
|
| 66 |
+
result_df_all = pd.DataFrame({
|
| 67 |
+
"状態": status_counts.index,
|
| 68 |
+
"件数": status_counts.values,
|
| 69 |
+
"割合(%)": status_ratio.values
|
| 70 |
+
})
|
| 71 |
|
| 72 |
important_results = [r for r in all_results if r["重要項目"]]
|
| 73 |
if important_results:
|
|
|
|
| 76 |
["LOW-LOW", "LOW", "OK", "HIGH", "HIGH-HIGH"], fill_value=0
|
| 77 |
)
|
| 78 |
status_ratio_imp = (status_counts_imp / total_imp * 100).round(1)
|
| 79 |
+
result_df_imp = pd.DataFrame({
|
| 80 |
+
"状態": status_counts_imp.index,
|
| 81 |
+
"件数": status_counts_imp.values,
|
| 82 |
+
"割合(%)": status_ratio_imp.values
|
| 83 |
+
})
|
| 84 |
else:
|
| 85 |
result_df_imp = pd.DataFrame(columns=["状態", "件数", "割合(%)"])
|
| 86 |
status_ratio_imp = pd.Series(dtype=float)
|
|
|
|
| 121 |
|
| 122 |
return result_df_all, result_df_imp, result_df_imp_items, summary, result_json
|
| 123 |
|
| 124 |
+
# ===== 傾向検出処理 =====
|
| 125 |
+
def detect_trends_with_forecast(df, thresholds_df, process_name, datetime_str, window_minutes, forecast_minutes):
|
| 126 |
+
target_time = pd.to_datetime(datetime_str)
|
| 127 |
+
start_time = target_time - pd.Timedelta(minutes=window_minutes)
|
| 128 |
+
end_time = target_time
|
| 129 |
+
df_window = df[(df["timestamp"] >= start_time) & (df["timestamp"] <= end_time)]
|
| 130 |
+
if df_window.empty:
|
| 131 |
+
return None, "⚠ 指定時間幅にデータなし", None
|
| 132 |
+
|
| 133 |
+
interval = df_window["timestamp"].diff().median()
|
| 134 |
+
if pd.isna(interval):
|
| 135 |
+
return None, "⚠ サンプリング間隔を検出できません", None
|
| 136 |
+
interval_minutes = interval.total_seconds() / 60
|
| 137 |
+
|
| 138 |
+
proc_thresholds = thresholds_df[(thresholds_df["ProcessNo_ProcessName"] == process_name) &
|
| 139 |
+
(thresholds_df["Important"] == True)]
|
| 140 |
+
if proc_thresholds.empty:
|
| 141 |
+
return None, f"⚠ プロセス {process_name} の重要項目なし", None
|
| 142 |
+
|
| 143 |
+
results = []
|
| 144 |
+
for _, thr in proc_thresholds.iterrows():
|
| 145 |
+
col_tuple = (thr["ColumnID"], thr["ItemName"], thr["ProcessNo_ProcessName"])
|
| 146 |
+
if col_tuple not in df.columns:
|
| 147 |
+
continue
|
| 148 |
+
series = df_window[col_tuple].dropna()
|
| 149 |
+
if len(series) < 3:
|
| 150 |
+
continue
|
| 151 |
+
|
| 152 |
+
x = np.arange(len(series)).reshape(-1, 1)
|
| 153 |
+
y = series.values.reshape(-1, 1)
|
| 154 |
+
model = LinearRegression().fit(x, y)
|
| 155 |
+
slope = model.coef_[0][0]
|
| 156 |
+
|
| 157 |
+
last_val = series.iloc[-1]
|
| 158 |
+
n = len(series)
|
| 159 |
+
|
| 160 |
+
forecast_steps = int(forecast_minutes / interval_minutes)
|
| 161 |
+
forecast_index = n + forecast_steps
|
| 162 |
+
forecast_val = model.predict([[forecast_index]])[0][0]
|
| 163 |
+
forecast_time = target_time + pd.Timedelta(minutes=forecast_minutes)
|
| 164 |
+
|
| 165 |
+
l, ll, h, hh = thr.get("L"), thr.get("LL"), thr.get("H"), thr.get("HH")
|
| 166 |
+
|
| 167 |
+
status = "安定"
|
| 168 |
+
if slope < 0 and pd.notna(ll):
|
| 169 |
+
if last_val > ll:
|
| 170 |
+
status = "LL接近下降傾向"
|
| 171 |
+
elif last_val <= ll:
|
| 172 |
+
status = "LL逸脱下降傾向"
|
| 173 |
+
if slope > 0 and pd.notna(hh):
|
| 174 |
+
if last_val < hh:
|
| 175 |
+
status = "HH接近上昇傾向"
|
| 176 |
+
elif last_val >= hh:
|
| 177 |
+
status = "HH逸脱上昇傾向"
|
| 178 |
+
|
| 179 |
+
forecast_status = "安定"
|
| 180 |
+
if pd.notna(ll) and forecast_val <= ll:
|
| 181 |
+
forecast_status = "LL逸脱予測"
|
| 182 |
+
elif pd.notna(hh) and forecast_val >= hh:
|
| 183 |
+
forecast_status = "HH逸脱予測"
|
| 184 |
+
|
| 185 |
+
results.append({
|
| 186 |
+
"ItemName": thr["ItemName"],
|
| 187 |
+
"傾向": status,
|
| 188 |
+
"傾き": round(slope, 4),
|
| 189 |
+
"最終値": round(float(last_val), 3) if pd.notna(last_val) else None,
|
| 190 |
+
"予測値": round(float(forecast_val), 3),
|
| 191 |
+
"予測時刻": str(forecast_time),
|
| 192 |
+
"予測傾向": forecast_status,
|
| 193 |
+
"サンプリング間隔(分)": interval_minutes,
|
| 194 |
+
"LL": ll, "L": l, "H": h, "HH": hh
|
| 195 |
+
})
|
| 196 |
|
| 197 |
+
result_df = pd.DataFrame(results)
|
| 198 |
+
result_json = json.dumps(results, ensure_ascii=False, indent=2)
|
| 199 |
+
return result_df, "✅ 傾向検出+未来予測完了", result_json
|
| 200 |
|
| 201 |
+
# ===== Gradio UI =====
|
| 202 |
+
with gr.Blocks(css=".gradio-container {overflow:auto !important}") as demo:
|
| 203 |
+
gr.Markdown("## 統合版アプリ (閾値診断 + 傾向検出)")
|
| 204 |
|
| 205 |
+
with gr.Row():
|
| 206 |
+
csv_input = gr.File(label="CSVファイルをアップロード", file_types=[".csv"], type="filepath")
|
| 207 |
+
excel_input = gr.File(label="Excel閾値ファイルをアップロード", file_types=[".xlsx"], type="filepath")
|
| 208 |
|
| 209 |
+
# ファイルを保持するステート
|
| 210 |
+
state_df = gr.State()
|
| 211 |
+
state_thresholds = gr.State()
|
| 212 |
+
|
| 213 |
+
def load_files(csv_file, excel_file):
|
| 214 |
+
try:
|
| 215 |
+
df = pd.read_csv(csv_file.name, header=[0,1,2])
|
| 216 |
+
timestamp_col = df.iloc[:,0]
|
| 217 |
+
df = df.drop(df.columns[0], axis=1)
|
| 218 |
+
df.insert(0,"timestamp", timestamp_col)
|
| 219 |
+
df["timestamp"] = pd.to_datetime(df["timestamp"], errors="coerce")
|
| 220 |
+
thresholds_df = pd.read_excel(excel_file.name)
|
| 221 |
+
thresholds_df["Important"] = thresholds_df["Important"].astype(str).str.upper().map({"TRUE":True,"FALSE":False})
|
| 222 |
+
for col in ["LL","L","H","HH"]:
|
| 223 |
+
if col in thresholds_df.columns:
|
| 224 |
+
thresholds_df[col] = pd.to_numeric(thresholds_df[col], errors="coerce")
|
| 225 |
+
return df, thresholds_df, "✅ ファイルを読み込みました"
|
| 226 |
+
except Exception as e:
|
| 227 |
+
return None, None, f"❌ 読み込み失敗: {e}"
|
| 228 |
+
|
| 229 |
+
load_btn = gr.Button("ファイルを読み込み")
|
| 230 |
+
load_status = gr.Textbox(label="ロード状態")
|
| 231 |
+
|
| 232 |
+
load_btn.click(load_files, inputs=[csv_input, excel_input], outputs=[state_df, state_thresholds, load_status])
|
| 233 |
|
| 234 |
with gr.Tabs():
|
|
|
|
| 235 |
with gr.Tab("閾値診断"):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
process_name = gr.Textbox(label="プロセス名", value="E018-A012_除害RO")
|
| 237 |
datetime_str = gr.Textbox(label="診断基準日時", value="2025/8/1 1:05")
|
| 238 |
window_minutes = gr.Number(label="さかのぼる時間幅(分)", value=60)
|
| 239 |
run_btn = gr.Button("診断を実行")
|
|
|
|
| 240 |
result_df_all = gr.Dataframe(label="全項目の状態集計結果")
|
| 241 |
result_df_imp = gr.Dataframe(label="重要項目全体の状態集計結果")
|
| 242 |
result_df_imp_items = gr.Dataframe(label="重要項目ごとの状態集計結果")
|
|
|
|
| 245 |
|
| 246 |
run_btn.click(
|
| 247 |
diagnose_process_range,
|
| 248 |
+
inputs=[state_df, state_thresholds, process_name, datetime_str, window_minutes],
|
| 249 |
outputs=[result_df_all, result_df_imp, result_df_imp_items, summary_output, json_output]
|
| 250 |
)
|
| 251 |
|
|
|
|
| 252 |
with gr.Tab("傾向検出"):
|
| 253 |
+
process_name2 = gr.Textbox(label="プロセス名", value="E018-A012_除害RO")
|
| 254 |
+
datetime_str2 = gr.Textbox(label="基準日時", value="2025/8/1 1:05")
|
| 255 |
+
window_minutes2 = gr.Number(label="さかのぼる時間幅(分)", value=60)
|
| 256 |
+
forecast_minutes2 = gr.Number(label="未来予測時間幅(分)", value=60)
|
| 257 |
+
run_btn2 = gr.Button("傾向検出を実行")
|
| 258 |
+
result_df2 = gr.Dataframe(label="傾向+未来予測結果")
|
| 259 |
+
summary_output2 = gr.Textbox(label="サマリー")
|
| 260 |
+
json_output2 = gr.Json(label="JSON結果")
|
| 261 |
|
| 262 |
+
run_btn2.click(
|
| 263 |
+
detect_trends_with_forecast,
|
| 264 |
+
inputs=[state_df, state_thresholds, process_name2, datetime_str2, window_minutes2, forecast_minutes2],
|
| 265 |
+
outputs=[result_df2, summary_output2, json_output2]
|
| 266 |
+
)
|
| 267 |
|
|
|
|
| 268 |
if __name__ == "__main__":
|
| 269 |
+
use_mcp = os.getenv("USE_MCP","0") == "1"
|
| 270 |
if use_mcp:
|
| 271 |
demo.launch(mcp_server=True)
|
| 272 |
else:
|