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="稳态持续的最小时间,低于此值不视为稳态段" ) # 转换为datetime对象 start_dt = datetime.combine(start_date, datetime.min.time()) end_dt = datetime.combine(end_date, datetime.max.time()) # 查询处理 if query_button: with st.spinner("正在获取数据..."): # 获取挤出机数据 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("正在分析数据..."): # 获取缓存数据 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') # 去除可能存在的NaN值 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("请选择时间范围并点击'开始分析'按钮获取数据。")