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_regression.py |  300 +++++++++++++++++++++++++++++++++++++++++++++++++++++------
 1 files changed, 268 insertions(+), 32 deletions(-)

diff --git a/app/pages/metered_weight_regression.py b/app/pages/metered_weight_regression.py
index d07c22b..d06dc68 100644
--- a/app/pages/metered_weight_regression.py
+++ b/app/pages/metered_weight_regression.py
@@ -3,12 +3,117 @@
 import plotly.graph_objects as go
 import pandas as pd
 import numpy as np
+import joblib
+import os
 from datetime import datetime, timedelta
 from app.services.extruder_service import ExtruderService
 from app.services.main_process_service import MainProcessService
 from sklearn.linear_model import LinearRegression
 from sklearn.model_selection import train_test_split
 from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error
+
+
+# 瀵煎叆绋虫�佽瘑鍒姛鑳�
+class SteadyStateDetector:
+    def __init__(self):
+        pass
+    
+    def detect_steady_state(self, df, weight_col='绫抽噸', 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['rolling_std'] = df[weight_col].rolling(window=window_size, min_periods=5).std()
+        df['rolling_mean'] = df[weight_col].rolling(window=window_size, min_periods=5).mean()
+        
+        # 璁$畻娉㈠姩鑼冨洿
+        df['fluctuation_range'] = (df['rolling_std'] / df['rolling_mean']) * 100
+        df['fluctuation_range'] = df['fluctuation_range'].fillna(0)
+        
+        # 鏍囪绋虫�佺偣
+        df['is_steady'] = 0
+        steady_condition = (
+            (df['fluctuation_range'] < std_threshold) & 
+            (df[weight_col] >= 0.1) 
+        )
+        df.loc[steady_condition, '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[weight_col]]
+                    }
+                else:
+                    current_segment['weights'].append(row[weight_col])
+            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))
+                        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 show_metered_weight_regression():
@@ -33,6 +138,12 @@
             '铻烘潌杞��', '鏈哄ご鍘嬪姏', '娴佺▼涓婚��', '铻烘潌娓╁害', 
             '鍚庢満绛掓俯搴�', '鍓嶆満绛掓俯搴�', '鏈哄ご娓╁害'
         ]
+    if 'mr_use_steady_data' not in st.session_state:
+        st.session_state['mr_use_steady_data'] = True
+    if 'mr_steady_window' not in st.session_state:
+        st.session_state['mr_steady_window'] = 20
+    if 'mr_steady_threshold' not in st.session_state:
+        st.session_state['mr_steady_threshold'] = 0.5
 
     # 瀹氫箟鍥炶皟鍑芥暟
     def update_dates(qs):
@@ -123,6 +234,42 @@
             st.session_state['mr_time_offset'] = time_offset
         with offset_cols[2]:
             st.write(f"褰撳墠鍋忕Щ: {time_offset} 鍒嗛挓")
+
+        # 绋虫�佽瘑鍒厤缃�
+        st.markdown("---")
+        steady_cols = st.columns(3)
+        with steady_cols[0]:
+            st.write("鈿栵笍 **绋虫�佽瘑鍒厤缃�**")
+            st.checkbox(
+                "浠呬娇鐢ㄧǔ鎬佹暟鎹繘琛岃缁�",
+                value=st.session_state['mr_use_steady_data'],
+                key="mr_use_steady_data",
+                help="鍚敤鍚庯紝鍙娇鐢ㄧ背閲嶇ǔ鎬佹椂娈电殑鏁版嵁杩涜妯″瀷璁粌"
+            )
+        
+        with steady_cols[1]:
+            st.write("馃搹 **绋虫�佸弬鏁�**")
+            st.slider(
+                "婊戝姩绐楀彛澶у皬 (绉�)",
+                min_value=5,
+                max_value=60,
+                value=st.session_state['mr_steady_window'],
+                step=5,
+                key="mr_steady_window",
+                help="鐢ㄤ簬绋虫�佽瘑鍒殑婊戝姩绐楀彛澶у皬"
+            )
+        
+        with steady_cols[2]:
+            st.write("馃搳 **绋虫�侀槇鍊�**")
+            st.slider(
+                "娉㈠姩闃堝�� (%)",
+                min_value=0.1,
+                max_value=2.0,
+                value=st.session_state['mr_steady_threshold'],
+                step=0.1,
+                key="mr_steady_threshold",
+                help="绋虫�佽瘑鍒殑娉㈠姩鑼冨洿闃堝��"
+            )
 
         # 鐗瑰緛閫夋嫨
         st.markdown("---")
@@ -305,6 +452,82 @@
 
             # 閲嶅懡鍚嶇背閲嶅垪
             df_analysis.rename(columns={'metered_weight': '绫抽噸'}, inplace=True)
+            
+            # 绋虫�佽瘑鍒�
+            steady_detector = SteadyStateDetector()
+            
+            # 鑾峰彇绋虫�佽瘑鍒弬鏁�
+            use_steady_data = st.session_state.get('mr_use_steady_data', True)
+            steady_window = st.session_state.get('mr_steady_window', 20)
+            steady_threshold = st.session_state.get('mr_steady_threshold', 0.5)
+            
+            # 鎵ц绋虫�佽瘑鍒�
+            df_analysis_with_steady, steady_segments = steady_detector.detect_steady_state(
+                df_analysis, 
+                weight_col='绫抽噸',
+                window_size=steady_window,
+                std_threshold=steady_threshold
+            )
+            
+            # 鏇存柊df_analysis涓哄寘鍚ǔ鎬佹爣璁扮殑鏁版嵁
+            df_analysis = df_analysis_with_steady
+            
+            # 绋虫�佹暟鎹彲瑙嗗寲
+            st.subheader("馃搱 绋虫�佹暟鎹垎甯�")
+                        
+            # 鍒涘缓绋虫�佹暟鎹彲瑙嗗寲鍥捐〃
+            fig_steady = go.Figure()
+                        
+            # 娣诲姞鍘熷绫抽噸鏇茬嚎
+            fig_steady.add_trace(go.Scatter(
+                x=df_analysis['time'],
+                y=df_analysis['绫抽噸'],
+                name='鍘熷绫抽噸',
+                mode='lines',
+                line=dict(color='lightgray', width=1)
+            ))
+                        
+            # 娣诲姞绋虫�佹暟鎹偣
+            steady_data_points = df_analysis[df_analysis['is_steady'] == 1]
+            fig_steady.add_trace(go.Scatter(
+                x=steady_data_points['time'],
+                y=steady_data_points['绫抽噸'],
+                name='绋虫�佺背閲�',
+                mode='markers',
+                marker=dict(color='green', size=3, opacity=0.6)
+            ))
+                        
+            # 娣诲姞闈炵ǔ鎬佹暟鎹偣
+            non_steady_data_points = df_analysis[df_analysis['is_steady'] == 0]
+            fig_steady.add_trace(go.Scatter(
+                x=non_steady_data_points['time'],
+                y=non_steady_data_points['绫抽噸'],
+                name='闈炵ǔ鎬佺背閲�',
+                mode='markers',
+                marker=dict(color='red', size=3, opacity=0.6)
+            ))
+                        
+            # 閰嶇疆鍥捐〃甯冨眬
+            fig_steady.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=500
+            )
+                        
+            # 鏄剧ず鍥捐〃
+            st.plotly_chart(fig_steady, use_container_width=True)
+            
+            # 鏄剧ず绋虫�佺粺璁�
+            total_data = len(df_analysis)
+            steady_data = len(df_analysis[df_analysis['is_steady'] == 1])
+            steady_ratio = (steady_data / total_data * 100) if total_data > 0 else 0
+            
+            stats_cols = st.columns(3)
+            stats_cols[0].metric("鎬绘暟鎹噺", total_data)
+            stats_cols[1].metric("绋虫�佹暟鎹噺", steady_data)
+            stats_cols[2].metric("绋虫�佹暟鎹瘮渚�", f"{steady_ratio:.1f}%")
 
             # --- 鍘熷鏁版嵁瓒嬪娍鍥� ---
             st.subheader("馃搱 鍘熷鏁版嵁瓒嬪娍鍥�")
@@ -440,8 +663,16 @@
                     st.warning(f"鏁版嵁涓己灏戜互涓嬬壒寰�: {', '.join(missing_features)}")
                 else:
                     # 鍑嗗鏁版嵁
-                    X = df_analysis[st.session_state['mr_selected_features']]
-                    y = df_analysis['绫抽噸']
+                    # 鏍规嵁閰嶇疆鍐冲畾鏄惁鍙娇鐢ㄧǔ鎬佹暟鎹�
+                    use_steady_data = st.session_state.get('mr_use_steady_data', True)
+                    if use_steady_data:
+                        df_filtered = df_analysis[df_analysis['is_steady'] == 1]
+                        st.info(f"宸茶繃婊ら潪绋虫�佹暟鎹紝浣跨敤 {len(df_filtered)} 鏉$ǔ鎬佹暟鎹繘琛岃缁�")
+                    else:
+                        df_filtered = df_analysis.copy()
+                    
+                    X = df_filtered[st.session_state['mr_selected_features']]
+                    y = df_filtered['绫抽噸']
 
                     # 娓呯悊鏁版嵁涓殑NaN鍊�
                     combined = pd.concat([X, y], axis=1)
@@ -454,7 +685,7 @@
                         # 閲嶆柊鍒嗙X鍜寉
                         X_clean = combined_clean[st.session_state['mr_selected_features']]
                         y_clean = combined_clean['绫抽噸']
-                        
+                            
                         # 鍒嗗壊璁粌闆嗗拰娴嬭瘯闆�
                         X_train, X_test, y_train, y_test = train_test_split(X_clean, y_clean, test_size=0.2, random_state=42)
 
@@ -580,37 +811,42 @@
                         })
                         st.dataframe(coef_df, use_container_width=True)
 
-                        # --- 棰勬祴鍔熻兘 ---
-                        st.subheader("馃敭 绫抽噸棰勬祴")
+                        # --- 妯″瀷淇濆瓨鍔熻兘 ---
+                        st.subheader("馃捑 妯″瀷淇濆瓨")
 
-                        # 鍒涘缓棰勬祴琛ㄥ崟
-                        st.write("杈撳叆鐗瑰緛鍊艰繘琛岀背閲嶉娴�:")
-                        predict_cols = st.columns(2)
-                        input_features = {}
+                        # 鍒涘缓妯″瀷淇濆瓨琛ㄥ崟
+                        st.write("淇濆瓨璁粌濂界殑妯″瀷鏉冮噸:")
+                        model_name = st.text_input(
+                            "妯″瀷鍚嶇О",
+                            value=f"linear_regression_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
+                            help="璇疯緭鍏ユā鍨嬪悕绉帮紝妯″瀷灏嗕繚瀛樹负璇ュ悕绉扮殑.joblib鏂囦欢"
+                        )
 
-                        for i, feature in enumerate(st.session_state['mr_selected_features']):
-                            with predict_cols[i % 2]:
-                                # 鑾峰彇鐗瑰緛鐨勭粺璁′俊鎭�
-                                min_val = df_analysis[feature].min()
-                                max_val = df_analysis[feature].max()
-                                mean_val = df_analysis[feature].mean()
-
-                                input_features[feature] = st.number_input(
-                                    f"{feature}",
-                                    key=f"pred_{feature}",
-                                    value=float(mean_val),
-                                    min_value=float(min_val),
-                                    max_value=float(max_val),
-                                    step=0.1
-                                )
-
-                        if st.button("棰勬祴绫抽噸"):
-                            # 鍑嗗棰勬祴鏁版嵁
-                            input_data = [[input_features[feature] for feature in st.session_state['mr_selected_features']]]
-                            # 棰勬祴
-                            predicted_weight = model.predict(input_data)[0]
-                            # 鏄剧ず棰勬祴缁撴灉
-                            st.success(f"棰勬祴绫抽噸: {predicted_weight:.4f} Kg/m")
+                        if st.button("淇濆瓨妯″瀷"):
+                            # 纭繚妯″瀷鐩綍瀛樺湪
+                            model_dir = "saved_models"
+                            os.makedirs(model_dir, exist_ok=True)
+                            
+                            # 淇濆瓨妯″瀷
+                            model_path = os.path.join(model_dir, f"{model_name}.joblib")
+                            try:
+                                # 淇濆瓨妯″瀷鏉冮噸鍜岀浉鍏充俊鎭�
+                                model_info = {
+                                    'model': model,
+                                    'features': st.session_state['mr_selected_features'],
+                                    'scaler': None,  # 绾挎�у洖褰掍笉闇�瑕佹爣鍖栧櫒
+                                    'model_type': 'linear_regression',
+                                    'created_at': datetime.now(),
+                                    'r2_score': r2,
+                                    'mse': mse,
+                                    'mae': mae,
+                                    'rmse': rmse,
+                                    'use_steady_data': use_steady_data
+                                }
+                                joblib.dump(model_info, model_path)
+                                st.success(f"妯″瀷宸叉垚鍔熶繚瀛樺埌: {model_path}")
+                            except Exception as e:
+                                st.error(f"妯″瀷淇濆瓨澶辫触: {e}")
 
                         # --- 鏁版嵁棰勮 ---
                         st.subheader("馃攳 鏁版嵁棰勮")

--
Gitblit v1.9.3