From 6628f663b636675bcaea316f2deaddf337de480e Mon Sep 17 00:00:00 2001
From: baoshiwei <baoshiwei@shlanbao.cn>
Date: 星期五, 13 三月 2026 10:23:31 +0800
Subject: [PATCH] feat(米重分析): 新增稳态识别和预测功能页面并优化现有模型
---
app/pages/metered_weight_steady_state.py | 463 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 463 insertions(+), 0 deletions(-)
diff --git a/app/pages/metered_weight_steady_state.py b/app/pages/metered_weight_steady_state.py
new file mode 100644
index 0000000..55d6535
--- /dev/null
+++ b/app/pages/metered_weight_steady_state.py
@@ -0,0 +1,463 @@
+import streamlit as st
+import plotly.express as px
+import plotly.graph_objects as go
+import pandas as pd
+import numpy as np
+from datetime import datetime, timedelta
+from app.services.extruder_service import ExtruderService
+from app.services.data_processing_service import DataProcessingService
+
+class SteadyStateDetector:
+ def __init__(self):
+ self.data_processor = DataProcessingService()
+
+ def preprocess_data(self, df, weight_col='metered_weight', window_size=20):
+ """
+ 鏁版嵁棰勫鐞嗭細浠呭鐞嗙己澶卞��
+ :param df: 鍘熷鏁版嵁妗�
+ :param weight_col: 绫抽噸鍒楀悕
+ :param window_size: 婊戝姩绐楀彛澶у皬
+ :return: 棰勫鐞嗗悗鐨勬暟鎹
+ """
+ if df is None or df.empty:
+ return df
+
+ # 澶嶅埗鏁版嵁閬垮厤淇敼鍘熷鏁版嵁
+ df_processed = df.copy()
+
+ # 澶勭悊缂哄け鍊�
+ df_processed[weight_col] = df_processed[weight_col].ffill().bfill()
+
+ # 鐩存帴浣跨敤鍘熷鏁版嵁锛屼笉杩涜寮傚父鍊兼浛鎹㈠拰骞虫粦澶勭悊
+ df_processed['smoothed_weight'] = df_processed[weight_col]
+
+ # 璁$畻绉诲姩鏍囧噯宸�
+ df_processed['rolling_std'] = df_processed[weight_col].rolling(window=window_size, min_periods=1).std()
+ df_processed['rolling_mean'] = df_processed[weight_col].rolling(window=window_size, min_periods=1).mean()
+
+ return df_processed
+
+ def detect_steady_state(self, df, weight_col='smoothed_weight', window_size=20, std_threshold=0.5, duration_threshold=60):
+ """
+ 绋虫�佽瘑鍒�昏緫
+ :param df: 棰勫鐞嗗悗鐨勬暟鎹
+ :param weight_col: 绫抽噸鍒楀悕锛堝凡骞虫粦锛�
+ :param window_size: 婊戝姩绐楀彛澶у皬锛堢锛�
+ :param std_threshold: 鏍囧噯宸槇鍊�
+ :param duration_threshold: 绋虫�佹寔缁椂闂撮槇鍊硷紙绉掞級
+ :return: 鍖呭惈绋虫�佹爣璁扮殑鏁版嵁妗嗗拰绋虫�佷俊鎭�
+ """
+ if df is None or df.empty:
+ return df, []
+
+ # 纭繚鏃堕棿鍒楁槸datetime绫诲瀷
+ df['time'] = pd.to_datetime(df['time'])
+
+ # 璁$畻鏃堕棿宸紙绉掞級
+ df['time_diff'] = df['time'].diff().dt.total_seconds().fillna(0)
+
+ # 鍒濆鍖栫ǔ鎬佹爣璁�
+ df['is_steady'] = 0
+
+ # 璁$畻姣忎釜绐楀彛鐨勭粺璁$壒寰�
+ df['window_std'] = df['smoothed_weight'].rolling(window=window_size, min_periods=5).std()
+ df['window_mean'] = df['smoothed_weight'].rolling(window=window_size, min_periods=5).mean()
+
+ # 璁$畻娉㈠姩鑼冨洿锛堢浉瀵逛簬鍧囧�肩殑鐧惧垎姣旓級
+ df['fluctuation_range'] = (df['window_std'] / df['window_mean']) * 100
+ df['fluctuation_range'] = df['fluctuation_range'].fillna(0)
+
+ # 鍒濇鏍囪绋虫�佺偣 - 鎺掗櫎绫抽噸灏忎簬0.1kg/m鐨勬暟鎹�
+ df.loc[(df['fluctuation_range'] < std_threshold) & (df['smoothed_weight'] >= 0.1), 'is_steady'] = 1
+
+ # 缁熻杩炵画绋虫�佹
+ steady_segments = []
+ current_segment = {}
+
+ for i, row in df.iterrows():
+ if row['is_steady'] == 1:
+ if not current_segment:
+ # 鏂扮殑绋虫�佹寮�濮�
+ current_segment = {
+ 'start_time': row['time'],
+ 'start_idx': i,
+ 'weights': [row['smoothed_weight']]
+ }
+ else:
+ # 缁х画褰撳墠绋虫�佹
+ current_segment['weights'].append(row['smoothed_weight'])
+ else:
+ if current_segment:
+ # 绋虫�佹缁撴潫锛岃绠楁寔缁椂闂�
+ current_segment['end_time'] = df.loc[i-1, 'time'] if i > 0 else df.loc[i, 'time']
+ current_segment['end_idx'] = i-1
+ duration = (current_segment['end_time'] - current_segment['start_time']).total_seconds()
+
+ if duration >= duration_threshold:
+ # 璁$畻绋虫�佹鐨勭粺璁℃寚鏍�
+ weights_array = np.array(current_segment['weights'])
+ current_segment['duration'] = duration
+ current_segment['mean_weight'] = np.mean(weights_array)
+ current_segment['std_weight'] = np.std(weights_array)
+ current_segment['min_weight'] = np.min(weights_array)
+ current_segment['max_weight'] = np.max(weights_array)
+ current_segment['fluctuation_range'] = (current_segment['std_weight'] / current_segment['mean_weight']) * 100
+
+ # 璁$畻缃俊搴︼紙鍩轰簬娉㈠姩鑼冨洿鍜屾寔缁椂闂达級
+ confidence = 100 - (current_segment['fluctuation_range'] / std_threshold) * 50
+ confidence = max(50, min(100, confidence)) # 缃俊搴﹁寖鍥�50-100
+ current_segment['confidence'] = confidence
+
+ steady_segments.append(current_segment)
+
+ # 閲嶇疆褰撳墠绋虫�佹
+ current_segment = {}
+
+ # 澶勭悊鏈�鍚庝竴涓ǔ鎬佹
+ if current_segment:
+ current_segment['end_time'] = df['time'].iloc[-1]
+ current_segment['end_idx'] = len(df) - 1
+ duration = (current_segment['end_time'] - current_segment['start_time']).total_seconds()
+
+ if duration >= duration_threshold:
+ weights_array = np.array(current_segment['weights'])
+ current_segment['duration'] = duration
+ current_segment['mean_weight'] = np.mean(weights_array)
+ current_segment['std_weight'] = np.std(weights_array)
+ current_segment['min_weight'] = np.min(weights_array)
+ current_segment['max_weight'] = np.max(weights_array)
+ current_segment['fluctuation_range'] = (current_segment['std_weight'] / current_segment['mean_weight']) * 100
+
+ confidence = 100 - (current_segment['fluctuation_range'] / std_threshold) * 50
+ confidence = max(50, min(100, confidence))
+ current_segment['confidence'] = confidence
+
+ steady_segments.append(current_segment)
+
+ # 鍦ㄦ暟鎹涓爣璁扮ǔ鎬佹
+ for segment in steady_segments:
+ df.loc[segment['start_idx']:segment['end_idx'], 'is_steady'] = 1
+
+ return df, steady_segments
+
+ def get_steady_state_metrics(self, steady_segments):
+ """
+ 璁$畻绋虫�佽瘑鍒殑閲忓寲鎸囨爣
+ :param steady_segments: 绋虫�佹鍒楄〃
+ :return: 绋虫�佺粺璁℃寚鏍囧瓧鍏�
+ """
+ if not steady_segments:
+ return {}
+
+ # 璁$畻骞冲潎绋虫�佹寔缁椂闂�
+ avg_duration = np.mean([seg['duration'] for seg in steady_segments])
+
+ # 璁$畻骞冲潎娉㈠姩鑼冨洿
+ avg_fluctuation = np.mean([seg['fluctuation_range'] for seg in steady_segments])
+
+ # 璁$畻骞冲潎缃俊搴�
+ avg_confidence = np.mean([seg['confidence'] for seg in steady_segments])
+
+ # 璁$畻绋虫�佹�绘椂闀�
+ total_steady_duration = sum([seg['duration'] for seg in steady_segments])
+
+ return {
+ 'total_steady_segments': len(steady_segments),
+ 'average_steady_duration': avg_duration,
+ 'average_fluctuation_range': avg_fluctuation,
+ 'average_confidence': avg_confidence,
+ 'total_steady_duration': total_steady_duration
+ }
+
+def show_metered_weight_steady_state():
+ # 鍒濆鍖栨湇鍔″拰妫�娴嬪櫒
+ extruder_service = ExtruderService()
+ steady_state_detector = SteadyStateDetector()
+
+ # 椤甸潰鏍囬
+ st.title("绫抽噸绋虫�佽瘑鍒垎鏋�")
+
+ # 鍒濆鍖栦細璇濈姸鎬�
+ if 'ss_start_date' not in st.session_state:
+ st.session_state['ss_start_date'] = datetime.now().date() - timedelta(days=1)
+ if 'ss_end_date' not in st.session_state:
+ st.session_state['ss_end_date'] = datetime.now().date()
+ if 'ss_quick_select' not in st.session_state:
+ st.session_state['ss_quick_select'] = "鏈�杩�24灏忔椂"
+ if 'ss_window_size' not in st.session_state:
+ st.session_state['ss_window_size'] = 20
+ if 'ss_std_threshold' not in st.session_state:
+ st.session_state['ss_std_threshold'] = 1.5
+ if 'ss_duration_threshold' not in st.session_state:
+ st.session_state['ss_duration_threshold'] = 60
+
+ # 瀹氫箟鍥炶皟鍑芥暟
+ def update_dates(qs):
+ st.session_state['ss_quick_select'] = qs
+ today = datetime.now().date()
+ if qs == "浠婂ぉ":
+ st.session_state['ss_start_date'] = today
+ st.session_state['ss_end_date'] = today
+ elif qs == "鏈�杩�24灏忔椂":
+ st.session_state['ss_start_date'] = today - timedelta(days=1)
+ st.session_state['ss_end_date'] = today
+ elif qs == "鏈�杩�7澶�":
+ st.session_state['ss_start_date'] = today - timedelta(days=7)
+ st.session_state['ss_end_date'] = today
+ elif qs == "鏈�杩�30澶�":
+ st.session_state['ss_start_date'] = today - timedelta(days=30)
+ st.session_state['ss_end_date'] = today
+
+ def on_date_change():
+ st.session_state['ss_quick_select'] = "鑷畾涔�"
+
+ # 鏌ヨ鏉′欢鍖哄煙
+ with st.expander("馃攳 鏌ヨ閰嶇疆", expanded=True):
+ # 鍒涘缓甯冨眬
+ cols = st.columns([1, 1, 1, 1, 1, 1.5, 1.5, 1])
+
+ options = ["浠婂ぉ", "鏈�杩�24灏忔椂", "鏈�杩�7澶�", "鏈�杩�30澶�", "鑷畾涔�"]
+ for i, option in enumerate(options):
+ with cols[i]:
+ button_type = "primary" if st.session_state['ss_quick_select'] == option else "secondary"
+ if st.button(option, key=f"btn_ss_{option}", width='stretch', type=button_type):
+ update_dates(option)
+ st.rerun()
+
+ with cols[5]:
+ start_date = st.date_input(
+ "寮�濮嬫棩鏈�",
+ label_visibility="collapsed",
+ key="ss_start_date",
+ on_change=on_date_change
+ )
+
+ with cols[6]:
+ end_date = st.date_input(
+ "缁撴潫鏃ユ湡",
+ label_visibility="collapsed",
+ key="ss_end_date",
+ on_change=on_date_change
+ )
+
+ with cols[7]:
+ query_button = st.button("馃殌 寮�濮嬪垎鏋�", key="ss_query", width='stretch')
+
+ # 绋虫�佸弬鏁伴厤缃�
+ st.markdown("---")
+ param_cols = st.columns(3)
+
+ with param_cols[0]:
+ st.write("鈿欙笍 **绋虫�佸弬鏁伴厤缃�**")
+ window_size = st.slider(
+ "婊戝姩绐楀彛澶у皬 (绉�)",
+ min_value=5,
+ max_value=60,
+ value=st.session_state['ss_window_size'],
+ step=5,
+ key="ss_window_size",
+ help="鐢ㄤ簬骞虫粦鏁版嵁鍜岃绠楃粺璁$壒寰佺殑婊戝姩绐楀彛澶у皬"
+ )
+
+ with param_cols[1]:
+ st.write("馃搹 **娉㈠姩闃堝�奸厤缃�**")
+ std_threshold = st.slider(
+ "鏍囧噯宸槇鍊�",
+ min_value=0.1,
+ max_value=2.0,
+ value=st.session_state['ss_std_threshold'],
+ step=0.1,
+ key="ss_std_threshold",
+ help="绫抽噸娉㈠姩鐨勬爣鍑嗗樊闃堝�硷紝浣庝簬姝ゅ�艰涓虹ǔ鎬�"
+ )
+
+ with param_cols[2]:
+ st.write("鈴憋笍 **鎸佺画鏃堕棿閰嶇疆**")
+ duration_threshold = st.slider(
+ "绋虫�佹寔缁椂闂� (绉�)",
+ min_value=30,
+ max_value=300,
+ value=st.session_state['ss_duration_threshold'],
+ step=10,
+ key="ss_duration_threshold",
+ help="绋虫�佹寔缁殑鏈�灏忔椂闂达紝浣庝簬姝ゅ�间笉瑙嗕负绋虫�佹"
+ )
+
+ # 杞崲涓篸atetime瀵硅薄
+ start_dt = datetime.combine(start_date, datetime.min.time())
+ end_dt = datetime.combine(end_date, datetime.max.time())
+
+ # 鏌ヨ澶勭悊
+ if query_button:
+ with st.spinner("姝e湪鑾峰彇鏁版嵁..."):
+ # 鑾峰彇鎸ゅ嚭鏈烘暟鎹�
+ df_extruder = extruder_service.get_extruder_data(start_dt, end_dt)
+
+ if df_extruder is None or df_extruder.empty:
+ st.warning("鎵�閫夋椂闂存鍐呮湭鎵惧埌浠讳綍鏁版嵁锛岃灏濊瘯璋冩暣鏌ヨ鏉′欢銆�")
+ return
+
+ # 缂撳瓨鏁版嵁鍒颁細璇濈姸鎬�
+ st.session_state['cached_extruder_ss'] = df_extruder
+ st.session_state['last_query_start_ss'] = start_dt
+ st.session_state['last_query_end_ss'] = end_dt
+
+ # 鏁版嵁澶勭悊鍜屽垎鏋�
+ if 'cached_extruder_ss' in st.session_state:
+ with st.spinner("姝e湪鍒嗘瀽鏁版嵁..."):
+ # 鑾峰彇缂撳瓨鏁版嵁
+ df_extruder = st.session_state['cached_extruder_ss']
+
+ # 鏁版嵁棰勫鐞�
+ df_processed = steady_state_detector.preprocess_data(df_extruder, window_size=st.session_state['ss_window_size'])
+
+ # 绋虫�佽瘑鍒�
+ df_with_steady, steady_segments = steady_state_detector.detect_steady_state(
+ df_processed,
+ window_size=st.session_state['ss_window_size'],
+ std_threshold=st.session_state['ss_std_threshold'],
+ duration_threshold=st.session_state['ss_duration_threshold']
+ )
+
+ # 璁$畻绋虫�佹寚鏍�
+ steady_metrics = steady_state_detector.get_steady_state_metrics(steady_segments)
+
+ # 鏁版嵁绫诲瀷妫�鏌ュ拰杞崲
+ df_with_steady['time'] = pd.to_datetime(df_with_steady['time'])
+ df_with_steady['metered_weight'] = pd.to_numeric(df_with_steady['metered_weight'], errors='coerce')
+ df_with_steady['smoothed_weight'] = pd.to_numeric(df_with_steady['smoothed_weight'], errors='coerce')
+
+ # 鍘婚櫎鍙兘瀛樺湪鐨凬aN鍊�
+ df_with_steady = df_with_steady.dropna(subset=['time', 'metered_weight', 'smoothed_weight'])
+
+ # 鏁版嵁鍙鍖栧尯鍩�
+ st.subheader("馃搳 绫抽噸绋虫�佽瘑鍒粨鏋�")
+
+ # 鍒涘缓鍥捐〃
+ fig = go.Figure()
+
+ # 娣诲姞鍘熷绫抽噸鏇茬嚎
+ fig.add_trace(go.Scatter(
+ x=df_with_steady['time'],
+ y=df_with_steady['metered_weight'],
+ name='鍘熷绫抽噸',
+ mode='lines',
+ opacity=0.6,
+ line=dict(color='lightgray', width=1)
+ ))
+
+ # 娣诲姞骞虫粦绫抽噸鏇茬嚎
+ fig.add_trace(go.Scatter(
+ x=df_with_steady['time'],
+ y=df_with_steady['smoothed_weight'],
+ name='骞虫粦绫抽噸',
+ mode='lines',
+ line=dict(color='blue', width=2)
+ ))
+
+ # 鏍囪绋虫�佸尯鍩�
+ for segment in steady_segments:
+ fig.add_shape(
+ type="rect",
+ x0=segment['start_time'],
+ y0=segment['min_weight'] * 0.95,
+ x1=segment['end_time'],
+ y1=segment['max_weight'] * 1.05,
+ fillcolor="rgba(0, 255, 0, 0.2)",
+ line=dict(color="rgba(0, 200, 0, 0.5)", width=1),
+ name="绋虫�佸尯鍩�"
+ )
+
+ # 閰嶇疆鍥捐〃甯冨眬
+ fig.update_layout(
+ title="绫抽噸绋虫�佽瘑鍒粨鏋�",
+ xaxis=dict(title="鏃堕棿"),
+ yaxis=dict(title="绫抽噸 (Kg/m)"),
+ legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
+ height=600
+ )
+
+ # 鏄剧ず鍥捐〃
+ st.plotly_chart(fig, use_container_width=True)
+
+ # 绋虫�佺粺璁℃寚鏍�
+ st.subheader("馃搱 绋虫�佺粺璁℃寚鏍�")
+ metrics_cols = st.columns(5)
+
+ with metrics_cols[0]:
+ st.metric(
+ "绋虫�佹鎬绘暟",
+ steady_metrics.get('total_steady_segments', 0),
+ help="璇嗗埆鍒扮殑绋虫�佹鏁伴噺"
+ )
+
+ with metrics_cols[1]:
+ st.metric(
+ "骞冲潎绋虫�佹椂闀�",
+ f"{steady_metrics.get('average_steady_duration', 0):.2f} 绉�",
+ help="鎵�鏈夌ǔ鎬佹鐨勫钩鍧囨寔缁椂闂�"
+ )
+
+ with metrics_cols[2]:
+ st.metric(
+ "骞冲潎娉㈠姩鑼冨洿",
+ f"{steady_metrics.get('average_fluctuation_range', 0):.2f}%",
+ help="绋虫�佹鍐呯背閲嶇殑骞冲潎娉㈠姩鑼冨洿锛堢浉瀵逛簬鍧囧�肩殑鐧惧垎姣旓級"
+ )
+
+ with metrics_cols[3]:
+ st.metric(
+ "骞冲潎缃俊搴�",
+ f"{steady_metrics.get('average_confidence', 0):.1f}%",
+ help="绋虫�佽瘑鍒粨鏋滅殑骞冲潎缃俊搴�"
+ )
+
+ with metrics_cols[4]:
+ st.metric(
+ "鎬荤ǔ鎬佹椂闀�",
+ f"{steady_metrics.get('total_steady_duration', 0)/60:.2f} 鍒嗛挓",
+ help="鎵�鏈夌ǔ鎬佹鐨勬�绘寔缁椂闂�"
+ )
+
+ # 绋虫�佹璇︽儏琛ㄦ牸
+ st.subheader("馃搵 绋虫�佹璇︽儏")
+ if steady_segments:
+ steady_df = pd.DataFrame(steady_segments)
+
+ # 閫夋嫨瑕佹樉绀虹殑鍒�
+ display_cols = ['start_time', 'end_time', 'duration', 'mean_weight', 'std_weight', 'fluctuation_range', 'confidence']
+ steady_df_display = steady_df[display_cols].copy()
+
+ # 鏍煎紡鍖栨樉绀�
+ steady_df_display['duration'] = steady_df_display['duration'].apply(lambda x: f"{x:.1f} 绉�")
+ steady_df_display['mean_weight'] = steady_df_display['mean_weight'].apply(lambda x: f"{x:.4f} Kg/m")
+ steady_df_display['std_weight'] = steady_df_display['std_weight'].apply(lambda x: f"{x:.4f} Kg/m")
+ steady_df_display['fluctuation_range'] = steady_df_display['fluctuation_range'].apply(lambda x: f"{x:.2f}%")
+ steady_df_display['confidence'] = steady_df_display['confidence'].apply(lambda x: f"{x:.1f}%")
+
+ st.dataframe(steady_df_display, use_container_width=True)
+
+ # 瀵煎嚭绋虫�佽瘑鍒粨鏋�
+ st.subheader("馃捑 瀵煎嚭鏁版嵁")
+
+ # 鍑嗗瀵煎嚭鏁版嵁
+ export_df = df_with_steady[['time', 'metered_weight', 'smoothed_weight', 'is_steady']].copy()
+ export_csv = export_df.to_csv(index=False)
+
+ # 鍒涘缓涓嬭浇鎸夐挳
+ st.download_button(
+ label="瀵煎嚭绋虫�佽瘑鍒粨鏋� (CSV)",
+ data=export_csv,
+ file_name=f"metered_weight_steady_state_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
+ mime="text/csv",
+ help="鐐瑰嚮鎸夐挳瀵煎嚭绫抽噸绋虫�佽瘑鍒粨鏋滄暟鎹�"
+ )
+ else:
+ st.info("鏈瘑鍒埌浠讳綍绋虫�佹锛岃灏濊瘯璋冩暣绋虫�佸弬鏁伴厤缃��")
+
+ # 鏁版嵁棰勮
+ st.subheader("馃攳 鏁版嵁棰勮")
+ st.dataframe(df_with_steady[['time', 'metered_weight', 'smoothed_weight', 'is_steady', 'fluctuation_range']].head(20), use_container_width=True)
+ else:
+ # 鎻愮ず鐢ㄦ埛鐐瑰嚮寮�濮嬪垎鏋愭寜閽�
+ st.info("璇烽�夋嫨鏃堕棿鑼冨洿骞剁偣鍑�'寮�濮嬪垎鏋�'鎸夐挳鑾峰彇鏁版嵁銆�")
--
Gitblit v1.9.3