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.main_process_service import MainProcessService
|
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):
|
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):
|
if df is None or df.empty:
|
return df, []
|
|
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)
|
|
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))
|
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):
|
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_correlation():
|
# 初始化服务
|
extruder_service = ExtruderService()
|
main_process_service = MainProcessService()
|
|
# 页面标题
|
st.title("米重相关性分析")
|
|
# 初始化服务
|
extruder_service = ExtruderService()
|
main_process_service = MainProcessService()
|
steady_state_detector = SteadyStateDetector()
|
|
# 初始化会话状态用于日期同步
|
if 'mc_start_date' not in st.session_state:
|
st.session_state['mc_start_date'] = datetime.now().date() - timedelta(days=7)
|
if 'mc_end_date' not in st.session_state:
|
st.session_state['mc_end_date'] = datetime.now().date()
|
if 'mc_quick_select' not in st.session_state:
|
st.session_state['mc_quick_select'] = "最近7天"
|
if 'mc_time_offset' not in st.session_state:
|
st.session_state['mc_time_offset'] = 0.0
|
|
# 初始化稳态识别相关参数
|
if 'mc_ss_window_size' not in st.session_state:
|
st.session_state['mc_ss_window_size'] = 20
|
if 'mc_ss_std_threshold' not in st.session_state:
|
st.session_state['mc_ss_std_threshold'] = 1.5
|
if 'mc_ss_duration_threshold' not in st.session_state:
|
st.session_state['mc_ss_duration_threshold'] = 60
|
if 'mc_use_steady_only' not in st.session_state:
|
st.session_state['mc_use_steady_only'] = False
|
|
# 定义回调函数
|
def update_dates(qs):
|
st.session_state['mc_quick_select'] = qs
|
today = datetime.now().date()
|
if qs == "今天":
|
st.session_state['mc_start_date'] = today
|
st.session_state['mc_end_date'] = today
|
elif qs == "最近3天":
|
st.session_state['mc_start_date'] = today - timedelta(days=3)
|
st.session_state['mc_end_date'] = today
|
elif qs == "最近7天":
|
st.session_state['mc_start_date'] = today - timedelta(days=7)
|
st.session_state['mc_end_date'] = today
|
elif qs == "最近30天":
|
st.session_state['mc_start_date'] = today - timedelta(days=30)
|
st.session_state['mc_end_date'] = today
|
|
def on_date_change():
|
st.session_state['mc_quick_select'] = "自定义"
|
|
# 查询条件区域
|
with st.expander("🔍 查询配置", expanded=True):
|
# 添加自定义 CSS 实现响应式换行
|
st.markdown("""
|
<style>
|
/* 强制列容器换行 */
|
[data-testid="stExpander"] [data-testid="column"] {
|
flex: 1 1 120px !important;
|
min-width: 120px !important;
|
}
|
/* 针对日期输入框列稍微加宽一点 */
|
@media (min-width: 768px) {
|
[data-testid="stExpander"] [data-testid="column"]:nth-child(6),
|
[data-testid="stExpander"] [data-testid="column"]:nth-child(7) {
|
flex: 2 1 180px !important;
|
min-width: 180px !important;
|
}
|
}
|
</style>
|
""", unsafe_allow_html=True)
|
|
# 创建布局
|
cols = st.columns([1, 1, 1, 1, 1, 1.5, 1.5, 1])
|
|
options = ["今天", "最近3天", "最近7天", "最近30天", "自定义"]
|
for i, option in enumerate(options):
|
with cols[i]:
|
# 根据当前选择状态决定按钮类型
|
button_type = "primary" if st.session_state['mc_quick_select'] == option else "secondary"
|
if st.button(option, key=f"btn_mc_{option}", width='stretch', type=button_type):
|
update_dates(option)
|
st.rerun()
|
|
with cols[5]:
|
start_date = st.date_input(
|
"开始日期",
|
label_visibility="collapsed",
|
key="mc_start_date",
|
on_change=on_date_change
|
)
|
|
with cols[6]:
|
end_date = st.date_input(
|
"结束日期",
|
label_visibility="collapsed",
|
key="mc_end_date",
|
on_change=on_date_change
|
)
|
|
with cols[7]:
|
query_button = st.button("🚀 开始分析", key="mc_query", width='stretch')
|
|
# 数据对齐调整
|
st.markdown("---")
|
offset_cols = st.columns([2, 4, 2])
|
with offset_cols[0]:
|
st.write("⏱️ **数据对齐调整**")
|
with offset_cols[1]:
|
time_offset = st.slider(
|
"时间偏移 (分钟)",
|
min_value=0.0,
|
max_value=5.0,
|
value=st.session_state['mc_time_offset'],
|
step=0.1,
|
help="调整主流程和温度数据的时间偏移,使其与挤出机米重数据对齐。"
|
)
|
st.session_state['mc_time_offset'] = time_offset
|
with offset_cols[2]:
|
st.write(f"当前偏移: {time_offset} 分钟")
|
|
# 稳态参数配置
|
st.markdown("---")
|
steady_state_cols = st.columns(4)
|
|
with steady_state_cols[0]:
|
st.write("⚙️ **稳态参数配置**")
|
window_size = st.slider(
|
"滑动窗口大小 (秒)",
|
min_value=5,
|
max_value=60,
|
value=st.session_state['mc_ss_window_size'],
|
step=5,
|
key="mc_ss_window_size",
|
help="用于平滑数据和计算统计特征的滑动窗口大小"
|
)
|
|
with steady_state_cols[1]:
|
st.write("📏 **波动阈值配置**")
|
std_threshold = st.slider(
|
"标准差阈值",
|
min_value=0.1,
|
max_value=2.0,
|
value=st.session_state['mc_ss_std_threshold'],
|
step=0.1,
|
key="mc_ss_std_threshold",
|
help="米重波动的标准差阈值,低于此值视为稳态"
|
)
|
|
with steady_state_cols[2]:
|
st.write("⏱️ **持续时间配置**")
|
duration_threshold = st.slider(
|
"稳态持续时间 (秒)",
|
min_value=30,
|
max_value=300,
|
value=st.session_state['mc_ss_duration_threshold'],
|
step=10,
|
key="mc_ss_duration_threshold",
|
help="稳态持续的最小时间,低于此值不视为稳态段"
|
)
|
|
with steady_state_cols[3]:
|
st.write("🎯 **分析数据选择**")
|
use_steady_only = st.checkbox(
|
"仅使用稳态数据",
|
value=st.session_state['mc_use_steady_only'],
|
key="mc_use_steady_only",
|
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("正在获取数据..."):
|
# 1. 获取完整的挤出机数据(包含所有时间点的螺杆转速和机头压力)
|
df_extruder_full = extruder_service.get_extruder_data(start_dt, end_dt)
|
|
# 2. 获取主流程控制数据
|
df_main_speed = main_process_service.get_cutting_setting_data(start_dt, end_dt)
|
|
df_temp = main_process_service.get_temperature_control_data(start_dt, end_dt)
|
|
# 检查是否有数据
|
has_data = any([
|
df_extruder_full is not None and not df_extruder_full.empty,
|
df_main_speed is not None and not df_main_speed.empty,
|
df_temp is not None and not df_temp.empty
|
])
|
|
if not has_data:
|
st.warning("所选时间段内未找到任何数据,请尝试调整查询条件。")
|
# 清除缓存数据
|
for key in ['cached_extruder_full', 'cached_main_speed', 'cached_temp', 'last_query_start', 'last_query_end']:
|
if key in st.session_state:
|
del st.session_state[key]
|
return
|
|
# 缓存数据到会话状态
|
st.session_state['cached_extruder_full'] = df_extruder_full
|
st.session_state['cached_main_speed'] = df_main_speed
|
st.session_state['cached_temp'] = df_temp
|
st.session_state['last_query_start'] = start_dt
|
st.session_state['last_query_end'] = end_dt
|
|
# 数据处理和图表渲染 - 每次应用重新运行时执行(包括调整时间偏移时)
|
if all(key in st.session_state for key in ['cached_extruder_full', 'cached_main_speed', 'cached_temp']):
|
with st.spinner("正在分析数据相关性..."):
|
# 获取缓存数据
|
df_extruder_full = st.session_state['cached_extruder_full']
|
df_main_speed = st.session_state['cached_main_speed']
|
df_temp = st.session_state['cached_temp']
|
|
# 获取当前时间偏移量
|
offset_delta = timedelta(minutes=st.session_state['mc_time_offset'])
|
|
# 处理数据
|
if df_extruder_full is not None and not df_extruder_full.empty:
|
# 过滤机头压力大于2的值
|
df_extruder_filtered = df_extruder_full[df_extruder_full['head_pressure'] <= 2]
|
|
# 为米重数据创建偏移后的时间列(只对米重数据进行时间偏移)
|
df_extruder_filtered['weight_time'] = df_extruder_filtered['time'] - offset_delta
|
else:
|
df_extruder_filtered = None
|
|
# 检查是否有数据
|
has_data = any([
|
df_extruder_filtered is not None and not df_extruder_filtered.empty,
|
df_main_speed is not None and not df_main_speed.empty,
|
df_temp is not None and not df_temp.empty
|
])
|
|
if not has_data:
|
st.warning("所选时间段内未找到任何数据,请尝试调整查询条件。")
|
return
|
|
# 数据整合与预处理
|
def integrate_data(df_extruder_filtered, df_main_speed, df_temp):
|
# 确保挤出机数据存在
|
if df_extruder_filtered is None or df_extruder_filtered.empty:
|
return None
|
|
# 创建只包含米重和偏移时间的主数据集
|
df_weight = df_extruder_filtered[['weight_time', 'metered_weight']].copy()
|
df_weight.rename(columns={'weight_time': 'time'}, inplace=True) # 将weight_time重命名为time作为基准时间
|
|
# 创建包含螺杆转速和原始时间的完整数据集
|
# 注意:这里使用完整的螺杆转速数据,而不仅仅是与米重对应的数据点
|
df_screw = df_extruder_filtered[['time', 'screw_speed_actual']].copy()
|
|
# 创建包含机头压力和原始时间的完整数据集
|
# 注意:这里使用完整的机头压力数据,而不仅仅是与米重对应的数据点
|
df_pressure = df_extruder_filtered[['time', 'head_pressure']].copy()
|
|
# 使用偏移后的米重时间整合螺杆转速数据
|
# 关键:使用merge_asof根据偏移后的米重时间查找最接近的螺杆转速数据
|
df_merged = pd.merge_asof(
|
df_weight.sort_values('time'),
|
df_screw.sort_values('time'),
|
on='time',
|
direction='nearest',
|
tolerance=pd.Timedelta('1min')
|
)
|
|
# 使用偏移后的米重时间整合机头压力数据
|
# 关键:使用merge_asof根据偏移后的米重时间查找最接近的机头压力数据
|
df_merged = pd.merge_asof(
|
df_merged.sort_values('time'),
|
df_pressure.sort_values('time'),
|
on='time',
|
direction='nearest',
|
tolerance=pd.Timedelta('1min')
|
)
|
|
# 整合主流程数据
|
if df_main_speed is not None and not df_main_speed.empty:
|
df_main_speed = df_main_speed[['time', 'process_main_speed']]
|
df_merged = pd.merge_asof(
|
df_merged.sort_values('time'),
|
df_main_speed.sort_values('time'),
|
on='time',
|
direction='nearest',
|
tolerance=pd.Timedelta('1min')
|
)
|
|
# 整合温度数据
|
if df_temp is not None and not df_temp.empty:
|
temp_cols = ['time', 'nakata_extruder_screw_display_temp',
|
'nakata_extruder_rear_barrel_display_temp',
|
'nakata_extruder_front_barrel_display_temp',
|
'nakata_extruder_head_display_temp']
|
df_temp_subset = df_temp[temp_cols].copy()
|
df_merged = pd.merge_asof(
|
df_merged.sort_values('time'),
|
df_temp_subset.sort_values('time'),
|
on='time',
|
direction='nearest',
|
tolerance=pd.Timedelta('1min')
|
)
|
|
# 重命名列以提高可读性
|
df_merged.rename(columns={
|
'screw_speed_actual': '螺杆转速',
|
'head_pressure': '机头压力',
|
'process_main_speed': '流程主速',
|
'nakata_extruder_screw_display_temp': '螺杆温度',
|
'nakata_extruder_rear_barrel_display_temp': '后机筒温度',
|
'nakata_extruder_front_barrel_display_temp': '前机筒温度',
|
'nakata_extruder_head_display_temp': '机头温度'
|
}, inplace=True)
|
|
# 清理数据
|
df_merged.dropna(subset=['metered_weight'], inplace=True)
|
|
return df_merged
|
|
# 执行稳态识别
|
df_extruder_steady, steady_segments = None, []
|
if df_extruder_filtered is not None and not df_extruder_filtered.empty:
|
# 数据预处理
|
df_processed = steady_state_detector.preprocess_data(
|
df_extruder_filtered,
|
weight_col='metered_weight',
|
window_size=st.session_state['mc_ss_window_size']
|
)
|
|
# 稳态检测
|
df_extruder_steady, steady_segments = steady_state_detector.detect_steady_state(
|
df_processed,
|
weight_col='smoothed_weight',
|
window_size=st.session_state['mc_ss_window_size'],
|
std_threshold=st.session_state['mc_ss_std_threshold'],
|
duration_threshold=st.session_state['mc_ss_duration_threshold']
|
)
|
|
# 将稳态标记添加到df_extruder_filtered中,以便在趋势图中使用
|
df_extruder_filtered = df_extruder_filtered.merge(
|
df_extruder_steady[['time', 'is_steady', 'smoothed_weight', 'fluctuation_range']],
|
on='time',
|
how='left'
|
)
|
|
# 执行数据整合
|
df_analysis = integrate_data(df_extruder_filtered, df_main_speed, df_temp)
|
|
if df_analysis is None or df_analysis.empty:
|
st.warning("数据整合失败,请检查数据质量或调整时间范围。")
|
return
|
|
# 重命名米重列
|
df_analysis.rename(columns={'metered_weight': '米重'}, inplace=True)
|
|
# 如果选择仅使用稳态数据,过滤掉非稳态数据
|
if st.session_state['mc_use_steady_only']:
|
# 确保df_analysis包含is_steady列
|
if 'is_steady' not in df_analysis.columns:
|
# 将稳态标记合并到分析数据中
|
df_analysis = df_analysis.merge(
|
df_extruder_steady[['time', 'is_steady']],
|
on='time',
|
how='left'
|
)
|
# 过滤稳态数据
|
df_analysis = df_analysis[df_analysis['is_steady'] == 1]
|
if df_analysis.empty:
|
st.warning("未找到稳态数据,请调整稳态参数或取消'仅使用稳态数据'选项。")
|
return
|
|
# 缓存稳态数据到会话状态
|
st.session_state['cached_steady_segments'] = steady_segments
|
st.session_state['cached_extruder_steady'] = df_extruder_steady
|
|
# --- 原始数据趋势图 ---
|
st.subheader("📈 原始数据趋势图")
|
|
# 创建趋势图
|
fig_trend = go.Figure()
|
|
# 添加米重数据(使用偏移后的时间)
|
if df_extruder_filtered is not None and not df_extruder_filtered.empty:
|
fig_trend.add_trace(go.Scatter(
|
x=df_extruder_filtered['weight_time'], # 使用偏移后的时间
|
y=df_extruder_filtered['metered_weight'],
|
name='米重 (Kg/m) [已偏移]',
|
mode='lines',
|
line=dict(color='blue', width=2)
|
))
|
|
# 添加螺杆转速(使用原始时间)
|
fig_trend.add_trace(go.Scatter(
|
x=df_extruder_filtered['time'], # 使用原始时间
|
y=df_extruder_filtered['screw_speed_actual'],
|
name='螺杆转速 (RPM)',
|
mode='lines',
|
line=dict(color='green', width=1.5),
|
yaxis='y2'
|
))
|
|
# 添加机头压力(使用原始时间,已过滤大于2的值)
|
fig_trend.add_trace(go.Scatter(
|
x=df_extruder_filtered['time'], # 使用原始时间
|
y=df_extruder_filtered['head_pressure'],
|
name='机头压力 (≤2)',
|
mode='lines',
|
line=dict(color='orange', width=1.5),
|
yaxis='y3'
|
))
|
|
# 添加流程主速
|
if df_main_speed is not None and not df_main_speed.empty:
|
fig_trend.add_trace(go.Scatter(
|
x=df_main_speed['time'],
|
y=df_main_speed['process_main_speed'],
|
name='流程主速 (M/Min)',
|
mode='lines',
|
line=dict(color='red', width=1.5),
|
yaxis='y4'
|
))
|
|
# 添加温度数据
|
if df_temp is not None and not df_temp.empty:
|
# 螺杆温度
|
fig_trend.add_trace(go.Scatter(
|
x=df_temp['time'],
|
y=df_temp['nakata_extruder_screw_display_temp'],
|
name='螺杆温度 (°C)',
|
mode='lines',
|
line=dict(color='purple', width=1),
|
yaxis='y5'
|
))
|
# 后机筒温度
|
fig_trend.add_trace(go.Scatter(
|
x=df_temp['time'],
|
y=df_temp['nakata_extruder_rear_barrel_display_temp'],
|
name='后机筒温度 (°C)',
|
mode='lines',
|
line=dict(color='pink', width=1),
|
yaxis='y5'
|
))
|
# 前机筒温度
|
fig_trend.add_trace(go.Scatter(
|
x=df_temp['time'],
|
y=df_temp['nakata_extruder_front_barrel_display_temp'],
|
name='前机筒温度 (°C)',
|
mode='lines',
|
line=dict(color='brown', width=1),
|
yaxis='y5'
|
))
|
# 机头温度
|
fig_trend.add_trace(go.Scatter(
|
x=df_temp['time'],
|
y=df_temp['nakata_extruder_head_display_temp'],
|
name='机头温度 (°C)',
|
mode='lines',
|
line=dict(color='gray', width=1),
|
yaxis='y5'
|
))
|
|
# 添加稳态区域标记
|
for segment in steady_segments:
|
# 获取米重数据的y轴范围,用于确定矩形高度
|
y_min = df_extruder_filtered['metered_weight'].min() * 0.95
|
y_max = df_extruder_filtered['metered_weight'].max() * 1.05
|
|
fig_trend.add_shape(
|
type="rect",
|
x0=segment['start_time'],
|
y0=y_min,
|
x1=segment['end_time'],
|
y1=y_max,
|
fillcolor="rgba(0, 255, 0, 0.2)",
|
line=dict(color="rgba(0, 200, 0, 0.5)", width=1),
|
name="稳态区域",
|
layer="below"
|
)
|
|
# 配置趋势图布局
|
fig_trend.update_layout(
|
title=f'原始数据趋势 (米重向前偏移 {st.session_state["mc_time_offset"]} 分钟)',
|
xaxis=dict(
|
title='时间',
|
rangeslider=dict(visible=True),
|
type='date'
|
),
|
yaxis=dict(
|
title='米重 (Kg/m)',
|
title_font=dict(color='blue'),
|
tickfont=dict(color='blue')
|
),
|
yaxis2=dict(
|
title='螺杆转速 (RPM)',
|
title_font=dict(color='green'),
|
tickfont=dict(color='green'),
|
overlaying='y',
|
side='right'
|
),
|
yaxis3=dict(
|
title='机头压力',
|
title_font=dict(color='orange'),
|
tickfont=dict(color='orange'),
|
overlaying='y',
|
side='right',
|
anchor='free',
|
position=0.85
|
),
|
yaxis4=dict(
|
title='流程主速 (M/Min)',
|
title_font=dict(color='red'),
|
tickfont=dict(color='red'),
|
overlaying='y',
|
side='right',
|
anchor='free',
|
position=0.75
|
),
|
yaxis5=dict(
|
title='温度 (°C)',
|
title_font=dict(color='purple'),
|
tickfont=dict(color='purple'),
|
overlaying='y',
|
side='left',
|
anchor='free',
|
position=0.15
|
),
|
legend=dict(
|
orientation="h",
|
yanchor="bottom",
|
y=1.02,
|
xanchor="right",
|
x=1
|
),
|
height=600,
|
margin=dict(l=100, r=200, t=100, b=100),
|
hovermode='x unified',
|
dragmode='select',
|
)
|
|
# 显示趋势图
|
selection = st.plotly_chart(fig_trend, width='stretch', config={'scrollZoom': True}, on_select='rerun' )
|
|
# 稳态统计指标
|
st.subheader("📊 稳态识别统计")
|
steady_metrics = steady_state_detector.get_steady_state_metrics(steady_segments)
|
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 df_extruder_steady is not None and not df_extruder_steady.empty:
|
fig_steady = go.Figure()
|
|
# 添加原始米重数据
|
fig_steady.add_trace(go.Scatter(
|
x=df_extruder_steady['time'],
|
y=df_extruder_steady['metered_weight'],
|
name='原始米重',
|
mode='lines',
|
opacity=0.6,
|
line=dict(color='lightblue', width=1)
|
))
|
|
# 添加平滑后的米重数据
|
fig_steady.add_trace(go.Scatter(
|
x=df_extruder_steady['time'],
|
y=df_extruder_steady['smoothed_weight'],
|
name='平滑米重',
|
mode='lines',
|
line=dict(color='blue', width=2)
|
))
|
|
# 添加波动范围(作为面积图)
|
fig_steady.add_trace(go.Scatter(
|
x=df_extruder_steady['time'],
|
y=df_extruder_steady['metered_weight'] + df_extruder_steady['rolling_std'],
|
name='波动上限',
|
mode='lines',
|
line=dict(color='rgba(255,0,0,0)'),
|
showlegend=True
|
))
|
|
fig_steady.add_trace(go.Scatter(
|
x=df_extruder_steady['time'],
|
y=df_extruder_steady['metered_weight'] - df_extruder_steady['rolling_std'],
|
name='波动下限',
|
mode='lines',
|
line=dict(color='rgba(255,0,0,0)'),
|
fill='tonexty',
|
fillcolor='rgba(255,0,0,0.1)'
|
))
|
|
# 添加稳态标记
|
fig_steady.add_trace(go.Scatter(
|
x=df_extruder_steady['time'],
|
y=df_extruder_steady['is_steady'] * (df_extruder_steady['metered_weight'].max() * 1.1),
|
name='稳态标记',
|
mode='lines',
|
line=dict(color='green', width=1, dash='dash')
|
))
|
|
# 配置稳态数据趋势图布局
|
fig_steady.update_layout(
|
title="稳态数据趋势分析",
|
xaxis=dict(
|
title='时间',
|
rangeslider=dict(visible=True),
|
type='date'
|
),
|
yaxis=dict(
|
title='米重 (Kg/m)',
|
title_font=dict(color='blue'),
|
tickfont=dict(color='blue')
|
),
|
yaxis2=dict(
|
title='稳态标记',
|
title_font=dict(color='green'),
|
tickfont=dict(color='green'),
|
overlaying='y',
|
side='right',
|
range=[-0.1, 1.1],
|
showgrid=False
|
),
|
legend=dict(
|
orientation="h",
|
yanchor="bottom",
|
y=1.02,
|
xanchor="right",
|
x=1
|
),
|
height=500,
|
margin=dict(l=100, r=100, t=100, b=100),
|
hovermode='x unified'
|
)
|
|
# 显示稳态数据趋势图
|
st.plotly_chart(fig_steady, width='stretch', config={'scrollZoom': True})
|
|
# --- 稳态参数相关性趋势图 ---
|
st.subheader("📈 稳态参数相关性趋势图")
|
|
if df_analysis is not None and not df_analysis.empty:
|
# 创建稳态参数相关性趋势图
|
fig_steady_corr = go.Figure()
|
|
# 选择相关系数最高的前3个参数
|
if '米重' in df_analysis.columns:
|
# 计算各参数与米重的相关系数
|
corr_with_weight = df_analysis.corr()[['米重']].sort_values('米重', ascending=False)
|
top_params = corr_with_weight.index[1:4] # 排除米重本身,取前3个
|
|
# 添加米重数据
|
fig_steady_corr.add_trace(go.Scatter(
|
x=df_analysis['time'],
|
y=df_analysis['米重'],
|
name='米重',
|
mode='lines',
|
line=dict(color='blue', width=2),
|
yaxis='y'
|
))
|
|
# 添加相关参数数据
|
colors = ['red', 'green', 'orange']
|
for i, param in enumerate(top_params):
|
if param in df_analysis.columns:
|
fig_steady_corr.add_trace(go.Scatter(
|
x=df_analysis['time'],
|
y=df_analysis[param],
|
name=param,
|
mode='lines',
|
line=dict(color=colors[i], width=1.5),
|
yaxis=f'y{i+2}'
|
))
|
|
# 配置图表布局
|
layout = {
|
'title': f'米重与相关参数趋势(前3个相关参数)',
|
'xaxis': {
|
'title': '时间',
|
'rangeslider': dict(visible=True),
|
'type': 'date'
|
},
|
'yaxis': {
|
'title': '米重 (Kg/m)',
|
'title_font': dict(color='blue'),
|
'tickfont': dict(color='blue')
|
},
|
'legend': {
|
'orientation': "h",
|
'yanchor': "bottom",
|
'y': 1.02,
|
'xanchor': "right",
|
'x': 1
|
},
|
'height': 600,
|
'margin': dict(l=100, r=200, t=100, b=100),
|
'hovermode': 'x unified'
|
}
|
|
# 添加额外的y轴配置
|
for i, param in enumerate(top_params):
|
layout[f'yaxis{i+2}'] = {
|
'title': param,
|
'title_font': dict(color=colors[i]),
|
'tickfont': dict(color=colors[i]),
|
'overlaying': 'y',
|
'side': 'right',
|
'anchor': 'free',
|
'position': 1 - (i+1)*0.15
|
}
|
|
fig_steady_corr.update_layout(layout)
|
st.plotly_chart(fig_steady_corr, width='stretch', config={'scrollZoom': True})
|
|
# 调试输出
|
# st.write("原始 selection 对象:", selection)
|
|
# 定义分析列
|
analysis_cols = ['米重', '螺杆转速', '机头压力', '流程主速', '螺杆温度', '后机筒温度', '前机筒温度', '机头温度']
|
|
# 定义要分析的参数
|
params = [
|
('螺杆转速', 'RPM'),
|
('机头压力', ''),
|
('流程主速', 'M/Min'),
|
('螺杆温度', '°C'),
|
('后机筒温度', '°C'),
|
('前机筒温度', '°C'),
|
('机头温度', '°C')
|
]
|
|
# 正确提取
|
selected_data = None
|
if selection.selection and selection.selection.box:
|
boxs = selection.selection.box
|
# 获取选中框的x轴范围
|
x_range = boxs[0]['x'][0], boxs[0]['x'][1]
|
st.write("x轴范围:", x_range)
|
|
# 过滤出在x轴范围内的数据
|
# 注意:这里需要使用df_analysis的time列进行过滤
|
# 首先需要确保df_analysis有time列
|
if 'time' in df_analysis.columns:
|
selected_data = df_analysis[
|
(df_analysis['time'] >= x_range[0]) &
|
(df_analysis['time'] <= x_range[1])
|
].copy() # 使用copy()避免切片警告
|
st.write(f"选中范围内的数据点数量: {len(selected_data)}")
|
# 显示可用的列名,帮助调试
|
st.write("可用列名:", list(selected_data.columns))
|
else:
|
st.warning("数据中缺少time列,无法进行范围过滤")
|
|
else:
|
st.info("请使用矩形框选工具选择时间范围(已自动启用选择模式)")
|
|
# 添加细节分析按钮
|
if selected_data is not None and not selected_data.empty:
|
if st.button("🔍 细节分析"):
|
st.subheader("📊 框选范围细节分析")
|
|
# 计算选中范围内的相关系数矩阵
|
selected_corr_matrix = selected_data[analysis_cols].corr()
|
|
# 创建选中范围的热力图
|
selected_fig_heatmap = px.imshow(
|
selected_corr_matrix,
|
text_auto=True,
|
aspect="auto",
|
title="框选范围参数相关性矩阵",
|
color_continuous_scale=["#0000FF", "#FFFFFF", "#FF0000"],
|
color_continuous_midpoint=0,
|
labels=dict(color="相关系数")
|
)
|
|
# 自定义布局
|
selected_fig_heatmap.update_layout(
|
height=400,
|
margin=dict(l=80, r=80, t=80, b=80),
|
xaxis=dict(tickangle=-45),
|
yaxis=dict(tickangle=0)
|
)
|
|
# 显示选中范围的热力图
|
st.plotly_chart(selected_fig_heatmap, width='stretch')
|
|
# 显示选中范围的参数与米重散点图
|
st.subheader("📈 框选范围参数与米重散点图")
|
|
# 创建选中范围的散点图
|
for i in range(0, len(params), 2):
|
row_cols = st.columns(2)
|
for j in range(2):
|
if i + j < len(params):
|
param_name, unit = params[i + j]
|
with row_cols[j]:
|
if param_name in selected_data.columns:
|
# 计算相关系数(添加错误处理)
|
try:
|
# 过滤掉NaN值
|
valid_data = selected_data[[param_name, '米重']].dropna()
|
if len(valid_data) >= 2: # 至少需要2个数据点
|
corr_coef = np.corrcoef(valid_data['米重'], valid_data[param_name])[0, 1]
|
else:
|
corr_coef = None
|
except Exception as e:
|
corr_coef = None
|
|
# 创建散点图
|
fig_scatter = px.scatter(
|
selected_data,
|
x=param_name,
|
y='米重',
|
title=f"{param_name} vs 米重(框选范围)",
|
labels={param_name: f"{param_name} ({unit})" if unit else param_name, '米重': '米重 (Kg/m)'}
|
)
|
|
# 添加趋势线(添加错误处理)
|
try:
|
# 过滤掉NaN值
|
valid_data = selected_data[[param_name, '米重']].dropna()
|
if len(valid_data) >= 2: # 至少需要2个数据点
|
trend_line = np.poly1d(np.polyfit(valid_data[param_name], valid_data['米重'], 1))(valid_data[param_name])
|
fig_scatter.add_trace(go.Scatter(
|
x=valid_data[param_name],
|
y=trend_line,
|
mode='lines',
|
name='趋势线',
|
line=dict(color='red', width=2)
|
))
|
except Exception as e:
|
# 如果趋势线计算失败,跳过添加趋势线
|
pass
|
|
# 添加相关系数注释(添加错误处理)
|
if corr_coef is not None:
|
fig_scatter.add_annotation(
|
x=0.05, y=0.95,
|
xref='paper', yref='paper',
|
text=f"相关系数: {corr_coef:.4f}",
|
showarrow=False,
|
font=dict(size=12, color="black"),
|
bgcolor="white",
|
bordercolor="black",
|
borderwidth=1
|
)
|
else:
|
fig_scatter.add_annotation(
|
x=0.05, y=0.95,
|
xref='paper', yref='paper',
|
text="相关系数: 无法计算",
|
showarrow=False,
|
font=dict(size=12, color="black"),
|
bgcolor="white",
|
bordercolor="black",
|
borderwidth=1
|
)
|
|
# 显示散点图
|
st.plotly_chart(fig_scatter, use_container_width=True)
|
else:
|
st.warning(f"数据中缺少 {param_name} 列")
|
|
# 显示选中范围的数据摘要
|
st.subheader("📊 框选范围数据摘要")
|
selected_summary_cols = st.columns(4)
|
|
with selected_summary_cols[0]:
|
if '米重' in selected_data.columns:
|
st.metric("平均米重", f"{selected_data['米重'].mean():.2f} Kg/m")
|
|
with selected_summary_cols[1]:
|
if '螺杆转速' in selected_data.columns:
|
st.metric("平均螺杆转速", f"{selected_data['螺杆转速'].mean():.2f} RPM")
|
|
with selected_summary_cols[2]:
|
if '流程主速' in selected_data.columns:
|
st.metric("平均流程主速", f"{selected_data['流程主速'].mean():.2f} M/Min")
|
|
with selected_summary_cols[3]:
|
if '机头压力' in selected_data.columns:
|
st.metric("平均机头压力", f"{selected_data['机头压力'].mean():.2f}")
|
|
# 显示选中范围的数据预览
|
st.subheader("🔍 框选范围数据预览")
|
st.dataframe(selected_data[analysis_cols].head(10), use_container_width=True)
|
|
# --- 相关性矩阵热力图 ---
|
st.subheader("📊 相关性矩阵热力图")
|
|
# 重命名米重列
|
df_analysis.rename(columns={'metered_weight': '米重'}, inplace=True)
|
|
# 计算相关系数矩阵
|
corr_matrix = df_analysis[analysis_cols].corr()
|
|
# 创建热力图
|
fig_heatmap = px.imshow(
|
corr_matrix,
|
text_auto=True,
|
aspect="auto",
|
title="参数相关性矩阵",
|
color_continuous_scale=["#0000FF", "#FFFFFF", "#FF0000"],
|
color_continuous_midpoint=0,
|
labels=dict(color="相关系数")
|
)
|
|
# 自定义布局
|
fig_heatmap.update_layout(
|
height=500,
|
margin=dict(l=100, r=100, t=100, b=100),
|
xaxis=dict(tickangle=-45),
|
yaxis=dict(tickangle=0)
|
)
|
|
# 显示热力图
|
st.plotly_chart(fig_heatmap, width='stretch')
|
|
# --- 参数与米重散点图 ---
|
st.subheader("📈 参数与米重散点图")
|
|
# 创建散点图
|
for i in range(0, len(params), 2):
|
row_cols = st.columns(2)
|
for j in range(2):
|
if i + j < len(params):
|
param_name, unit = params[i + j]
|
with row_cols[j]:
|
if param_name in df_analysis.columns:
|
# 计算相关系数(添加错误处理)
|
try:
|
# 过滤掉NaN值
|
valid_data = df_analysis[[param_name, '米重']].dropna()
|
if len(valid_data) >= 2: # 至少需要2个数据点
|
corr_coef = np.corrcoef(valid_data['米重'], valid_data[param_name])[0, 1]
|
else:
|
corr_coef = None
|
except Exception as e:
|
corr_coef = None
|
|
# 创建散点图
|
fig_scatter = px.scatter(
|
df_analysis,
|
x=param_name,
|
y='米重',
|
title=f"{param_name} vs 米重",
|
labels={param_name: f"{param_name} ({unit})" if unit else param_name, '米重': '米重 (Kg/m)'}
|
)
|
|
# 添加趋势线(添加错误处理)
|
try:
|
# 过滤掉NaN值
|
valid_data = df_analysis[[param_name, '米重']].dropna()
|
if len(valid_data) >= 2: # 至少需要2个数据点
|
trend_line = np.poly1d(np.polyfit(valid_data[param_name], valid_data['米重'], 1))(valid_data[param_name])
|
fig_scatter.add_trace(go.Scatter(
|
x=valid_data[param_name],
|
y=trend_line,
|
mode='lines',
|
name='趋势线',
|
line=dict(color='red', width=2)
|
))
|
except Exception as e:
|
# 如果趋势线计算失败,跳过添加趋势线
|
pass
|
|
# 添加相关系数注释(添加错误处理)
|
if corr_coef is not None:
|
fig_scatter.add_annotation(
|
x=0.05, y=0.95,
|
xref='paper', yref='paper',
|
text=f"相关系数: {corr_coef:.4f}",
|
showarrow=False,
|
font=dict(size=12, color="black"),
|
bgcolor="white",
|
bordercolor="black",
|
borderwidth=1
|
)
|
else:
|
fig_scatter.add_annotation(
|
x=0.05, y=0.95,
|
xref='paper', yref='paper',
|
text="相关系数: 无法计算",
|
showarrow=False,
|
font=dict(size=12, color="black"),
|
bgcolor="white",
|
bordercolor="black",
|
borderwidth=1
|
)
|
|
# 显示散点图
|
st.plotly_chart(fig_scatter, use_container_width=True)
|
else:
|
st.warning(f"数据中缺少 {param_name} 列")
|
|
# --- 相关性统计表格 ---
|
st.subheader("📋 相关性统计")
|
|
# 计算每个参数与米重的相关系数(添加错误处理)
|
corr_stats = []
|
for param_name, _ in params:
|
if param_name in df_analysis.columns:
|
try:
|
# 过滤掉NaN值
|
valid_data = df_analysis[[param_name, '米重']].dropna()
|
if len(valid_data) >= 2: # 至少需要2个数据点
|
corr_coef = np.corrcoef(valid_data['米重'], valid_data[param_name])[0, 1]
|
corr_stats.append({
|
'参数': param_name,
|
'相关系数': corr_coef,
|
'相关程度': '强' if abs(corr_coef) > 0.7 else '中等' if abs(corr_coef) > 0.3 else '弱'
|
})
|
else:
|
corr_stats.append({
|
'参数': param_name,
|
'相关系数': None,
|
'相关程度': '无法计算'
|
})
|
except Exception as e:
|
corr_stats.append({
|
'参数': param_name,
|
'相关系数': None,
|
'相关程度': '无法计算'
|
})
|
|
# 创建统计表格
|
corr_df = pd.DataFrame(corr_stats)
|
# 按相关系数绝对值排序(处理None值)
|
try:
|
# 计算相关系数绝对值,对于None值使用-1(这样会排在最后)
|
corr_df['相关系数绝对值'] = corr_df['相关系数'].apply(lambda x: abs(x) if x is not None else -1)
|
corr_df.sort_values('相关系数绝对值', ascending=False, inplace=True)
|
corr_df.drop('相关系数绝对值', axis=1, inplace=True)
|
except Exception as e:
|
# 如果排序失败,保持原始顺序
|
pass
|
|
# 显示表格
|
st.dataframe(corr_df, use_container_width=True)
|
|
# --- 数据摘要 ---
|
# st.subheader("📊 数据摘要")
|
# summary_cols = st.columns(4)
|
|
# with summary_cols[0]:
|
# if '米重' in df_analysis.columns:
|
# st.metric("平均米重", f"{df_analysis['米重'].mean():.2f} Kg/m")
|
|
# with summary_cols[1]:
|
# if '螺杆转速' in df_analysis.columns:
|
# st.metric("平均螺杆转速", f"{df_analysis['螺杆转速'].mean():.2f} RPM")
|
|
# with summary_cols[2]:
|
# if '流程主速' in df_analysis.columns:
|
# st.metric("平均流程主速", f"{df_analysis['流程主速'].mean():.2f} M/Min")
|
|
# with summary_cols[3]:
|
# if '机头压力' in df_analysis.columns:
|
# st.metric("平均机头压力", f"{df_analysis['机头压力'].mean():.2f}")
|
|
# --- 数据预览 ---
|
st.subheader("🔍 数据预览")
|
st.dataframe(df_analysis[analysis_cols].head(20), use_container_width=True)
|
else:
|
# 提示用户点击开始分析按钮
|
st.info("请选择时间范围并点击'开始分析'按钮获取数据。")
|