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("请选择时间范围并点击'开始分析'按钮获取数据。")
|