190 lines
7.3 KiB
Python
190 lines
7.3 KiB
Python
from __future__ import annotations
|
||
|
||
import json
|
||
from datetime import datetime
|
||
|
||
import plotly.express as px
|
||
import streamlit as st
|
||
|
||
from services.hotspot import (
|
||
analyze_hotspot_frequency,
|
||
calculate_hotspot_risk_score,
|
||
generate_hotspot_strategies,
|
||
prepare_hotspot_dataset,
|
||
serialise_datetime_columns,
|
||
)
|
||
|
||
|
||
@st.cache_data(show_spinner=False)
|
||
def _prepare_hotspot_data(df):
|
||
return prepare_hotspot_dataset(df)
|
||
|
||
|
||
def render_hotspot(accident_records, accident_source_name: str | None) -> None:
|
||
st.header("📍 事故多发路口分析")
|
||
st.markdown("独立分析事故数据,识别高风险路口并生成针对性策略。")
|
||
|
||
if accident_records is None:
|
||
st.info("请在左侧上传事故数据并点击“应用数据与筛选”后再执行热点分析。")
|
||
st.markdown(
|
||
"""
|
||
### 📝 支持的数据格式要求:
|
||
- **文件格式**:Excel (.xlsx)
|
||
- **必要字段**:
|
||
- `事故时间`
|
||
- `事故类型`
|
||
- `事故具体地点`
|
||
- `所在街道`
|
||
- `道路类型`
|
||
- `路口路段类型`
|
||
"""
|
||
)
|
||
return
|
||
|
||
with st.spinner("正在准备事故热点数据…"):
|
||
hotspot_data = _prepare_hotspot_data(accident_records)
|
||
|
||
st.success(f"✅ 成功加载数据:{len(hotspot_data)} 条事故记录")
|
||
|
||
metric_cols = st.columns(3)
|
||
with metric_cols[0]:
|
||
st.metric(
|
||
"数据时间范围",
|
||
f"{hotspot_data['事故时间'].min().strftime('%Y-%m-%d')} 至 {hotspot_data['事故时间'].max().strftime('%Y-%m-%d')}",
|
||
)
|
||
with metric_cols[1]:
|
||
st.metric(
|
||
"事故类型分布",
|
||
f"财损: {len(hotspot_data[hotspot_data['事故类型'] == '财损'])}起",
|
||
)
|
||
with metric_cols[2]:
|
||
st.metric("涉及区域", f"{hotspot_data['所在街道'].nunique()}个街道")
|
||
|
||
st.subheader("🔧 分析参数设置")
|
||
settings_cols = st.columns(3)
|
||
with settings_cols[0]:
|
||
time_window = st.selectbox(
|
||
"统计时间窗口",
|
||
options=["7D", "15D", "30D"],
|
||
index=0,
|
||
key="hotspot_window",
|
||
)
|
||
with settings_cols[1]:
|
||
min_accidents = st.number_input(
|
||
"最小事故数", min_value=1, max_value=50, value=3, key="hotspot_min_accidents"
|
||
)
|
||
with settings_cols[2]:
|
||
top_n = st.slider("显示热点数量", min_value=3, max_value=20, value=8, key="hotspot_top_n")
|
||
|
||
if not st.button("🚀 开始热点分析", type="primary"):
|
||
return
|
||
|
||
with st.spinner("正在分析事故热点分布…"):
|
||
hotspots = analyze_hotspot_frequency(hotspot_data, time_window=time_window)
|
||
hotspots = hotspots[hotspots["accident_count"] >= min_accidents]
|
||
|
||
if hotspots.empty:
|
||
st.warning("⚠️ 未找到符合条件的事故热点数据,请调整筛选参数。")
|
||
return
|
||
|
||
hotspots_with_risk = calculate_hotspot_risk_score(hotspots.head(top_n * 3))
|
||
top_hotspots = hotspots_with_risk.head(top_n)
|
||
|
||
st.subheader("📊 事故多发路口排名(前{0}个)".format(top_n))
|
||
display_columns = {
|
||
"accident_count": "累计事故数",
|
||
"recent_count": "近期事故数",
|
||
"trend_ratio": "趋势比例",
|
||
"main_accident_type": "主要类型",
|
||
"main_intersection_type": "路口类型",
|
||
"risk_score": "风险评分",
|
||
"risk_level": "风险等级",
|
||
}
|
||
display_df = top_hotspots[list(display_columns.keys())].rename(columns=display_columns)
|
||
styled_df = display_df.style.format({"趋势比例": "{:.2f}", "风险评分": "{:.1f}"}).background_gradient(
|
||
subset=["风险评分"], cmap="Reds"
|
||
)
|
||
st.dataframe(styled_df, use_container_width=True)
|
||
|
||
st.subheader("🎯 针对性策略建议")
|
||
strategies = generate_hotspot_strategies(top_hotspots, time_period="本周")
|
||
for index, strategy_info in enumerate(strategies, start=1):
|
||
message = f"**{index}. {strategy_info['strategy']}**"
|
||
risk_level = strategy_info["risk_level"]
|
||
if risk_level == "高风险":
|
||
st.error(f"🚨 {message}")
|
||
elif risk_level == "中风险":
|
||
st.warning(f"⚠️ {message}")
|
||
else:
|
||
st.info(f"✅ {message}")
|
||
|
||
st.subheader("📈 数据分析可视化")
|
||
chart_cols = st.columns(2)
|
||
with chart_cols[0]:
|
||
fig_hotspots = px.bar(
|
||
top_hotspots.head(10),
|
||
x=top_hotspots.head(10).index,
|
||
y=["accident_count", "recent_count"],
|
||
title="事故频次TOP10分布",
|
||
labels={"value": "事故数量", "variable": "类型", "index": "路口名称"},
|
||
barmode="group",
|
||
)
|
||
fig_hotspots.update_layout(xaxis_tickangle=-45)
|
||
st.plotly_chart(fig_hotspots, use_container_width=True)
|
||
|
||
with chart_cols[1]:
|
||
risk_distribution = top_hotspots["risk_level"].value_counts()
|
||
fig_risk = px.pie(
|
||
values=risk_distribution.values,
|
||
names=risk_distribution.index,
|
||
title="风险等级分布",
|
||
color_discrete_map={"高风险": "red", "中风险": "orange", "低风险": "green"},
|
||
)
|
||
st.plotly_chart(fig_risk, use_container_width=True)
|
||
|
||
st.subheader("💾 数据导出")
|
||
download_cols = st.columns(2)
|
||
with download_cols[0]:
|
||
hotspot_csv = top_hotspots.to_csv().encode("utf-8-sig")
|
||
st.download_button(
|
||
"📥 下载热点数据CSV",
|
||
data=hotspot_csv,
|
||
file_name=f"accident_hotspots_{datetime.now().strftime('%Y%m%d')}.csv",
|
||
mime="text/csv",
|
||
)
|
||
|
||
with download_cols[1]:
|
||
serializable = serialise_datetime_columns(
|
||
top_hotspots.reset_index(),
|
||
columns=[col for col in top_hotspots.columns if "time" in col or "date" in col],
|
||
)
|
||
report_payload = {
|
||
"analysis_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||
"time_window": time_window,
|
||
"data_source": accident_source_name or "事故数据",
|
||
"total_records": int(len(hotspot_data)),
|
||
"analysis_parameters": {"min_accidents": int(min_accidents), "top_n": int(top_n)},
|
||
"top_hotspots": serializable.to_dict("records"),
|
||
"recommended_strategies": strategies,
|
||
"summary": {
|
||
"high_risk_count": int((top_hotspots["risk_level"] == "高风险").sum()),
|
||
"medium_risk_count": int((top_hotspots["risk_level"] == "中风险").sum()),
|
||
"total_analyzed_locations": int(len(hotspots)),
|
||
"most_dangerous_location": top_hotspots.index[0]
|
||
if len(top_hotspots)
|
||
else "无",
|
||
},
|
||
}
|
||
st.download_button(
|
||
"📄 下载完整分析报告",
|
||
data=json.dumps(report_payload, ensure_ascii=False, indent=2),
|
||
file_name=f"hotspot_analysis_report_{datetime.now().strftime('%Y%m%d_%H%M')}.json",
|
||
mime="application/json",
|
||
)
|
||
|
||
with st.expander("📋 查看原始数据预览"):
|
||
preview_cols = ["事故时间", "所在街道", "事故类型", "事故具体地点", "道路类型"]
|
||
preview_df = hotspot_data[preview_cols].copy()
|
||
st.dataframe(preview_df.head(10), use_container_width=True)
|
||
|