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