diff --git a/CHANGELOG.md b/CHANGELOG.md index d7cdd69..31665ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## [1.1.0] - 2025-08-28 ### Added -- Integrated GPT-based analysis for comprehensive traffic safety insights +- Integrated AI-based analysis for comprehensive traffic safety insights - Added automated report generation with AI-powered recommendations - Implemented natural language query processing for data exploration - Added export functionality for analysis reports (PDF/CSV formats) @@ -22,7 +22,7 @@ - Addressed memory leaks in large dataset processing ### Documentation -- Updated README with new GPT analysis features and usage examples +- Updated README with new AI analysis features and usage examples - Added API documentation for extended functionality - Included sample datasets and tutorial guides @@ -44,4 +44,4 @@ ### Fixed -- Resolved session state KeyError. \ No newline at end of file +- Resolved session state KeyError. diff --git a/app.py b/app.py index 22d4e1b..2d74aea 100644 --- a/app.py +++ b/app.py @@ -294,9 +294,9 @@ def run_streamlit_app(): # Add OpenAI API key input in sidebar st.sidebar.markdown("---") - st.sidebar.subheader("GPT API 配置") - openai_api_key = st.sidebar.text_input("GPT API Key", value='sk-sXY934yPqjh7YKKC08380b198fEb47308cDa09BeE23d9c8a', type="password", help="用于GPT分析结果的API密钥") - open_ai_base_url = st.sidebar.text_input("GPT Base Url", value='https://aihubmix.com/v1', type='default') + st.sidebar.subheader("AI API 配置") + openai_api_key = st.sidebar.text_input("AI API Key", value='sk-sXY934yPqjh7YKKC08380b198fEb47308cDa09BeE23d9c8a', type="password", help="用于 AI 分析结果的 API 密钥") + open_ai_base_url = st.sidebar.text_input("AI Base Url", value='https://aihubmix.com/v1', type='default') # Process data only when Apply button is clicked if apply_button and accident_file and strategy_file: @@ -404,14 +404,14 @@ def run_streamlit_app(): tab_labels = [ "🏠 总览", + "📍 事故热点", + "🔍 AI 分析", "📈 预测模型", "📊 模型评估", "⚠️ 异常检测", "📝 策略评估", "⚖️ 策略对比", "🧪 情景模拟", - "🔍 GPT 分析", - "📍 事故热点", ] default_tab = st.session_state.get("active_tab", tab_labels[0]) if default_tab not in tab_labels: @@ -426,17 +426,94 @@ def run_streamlit_app(): st.session_state["active_tab"] = selected_tab - if selected_tab == "📍 事故热点": + if selected_tab == "🏠 总览": + if render_overview is not None: + render_overview(base, region_sel, start_dt, end_dt, strat_filter) + else: + st.warning("概览模块未能加载,请检查 `ui_sections/overview.py`。") + + elif selected_tab == "📍 事故热点": if render_hotspot is not None: render_hotspot(accident_records, accident_source_name) else: st.warning("事故热点模块未能加载,请检查 `ui_sections/hotspot.py`。") - elif selected_tab == "🏠 总览": - if render_overview is not None: - render_overview(base, region_sel, start_dt, end_dt, strat_filter) + elif selected_tab == "🔍 AI 分析": + from openai import OpenAI + st.subheader("AI 数据分析与改进建议") + if not HAS_OPENAI: + st.warning("未安装 `openai` 库。请安装后重试。") + elif not openai_api_key: + st.info("请在左侧边栏输入 OpenAI API Key 以启用 AI 分析。") else: - st.warning("概览模块未能加载,请检查 `ui_sections/overview.py`。") + if all_strategy_types: + # Generate results if not already + results, recommendation = generate_output_and_recommendations(base, all_strategy_types, + region=region_sel if region_sel != '全市' else '全市') + df_res = pd.DataFrame(results).T + kpi_json = json.dumps(kpi, ensure_ascii=False, indent=2) + results_json = df_res.to_json(orient="records", force_ascii=False) + recommendation_text = recommendation + + # Prepare data to send + data_to_analyze = { + "kpis": kpi_json, + "strategy_results": results_json, + "recommendation": recommendation_text + } + data_str = json.dumps(data_to_analyze, ensure_ascii=False) + + prompt = ( + "你是一名资深交通安全数据分析顾问。请基于以下结构化数据输出一份专业报告,需包含:\n" + "1. 核心指标洞察:按要点总结事故趋势、显著波动及可能原因。\n" + "2. 策略绩效评估:对比主要策略的优势、短板与适用场景。\n" + "3. 优化建议:为短期(0-3个月)、中期(3-12个月)与长期(12个月以上)分别给出2-3条可操作措施。\n" + "请保持正式语气,引用关键数值支撑结论,并用清晰的小节或列表呈现。\n" + f"数据摘要:{data_str}\n" + ) + if st.button("上传数据至 AI 并获取分析"): + if not openai_api_key.strip(): + st.info("请提供有效的 AI API Key。") + elif not open_ai_base_url.strip(): + st.info("请提供可访问的 AI Base Url。") + else: + try: + client = OpenAI( + base_url=open_ai_base_url, + # sk-xxx替换为自己的key + api_key=openai_api_key + ) + st.markdown("### AI 分析结果与改进思路") + placeholder = st.empty() + accumulated_response: list[str] = [] + with st.spinner("AI 正在生成专业报告,请稍候…"): + stream = client.chat.completions.create( + model="gpt-5-mini", + messages=[ + { + "role": "system", + "content": "You are a professional traffic safety analyst who writes concise, well-structured Chinese reports." + }, + {"role": "user", "content": prompt}, + ], + stream=True, + ) + for chunk in stream: + delta = chunk.choices[0].delta if chunk.choices else None + piece = getattr(delta, "content", None) if delta else None + if piece: + accumulated_response.append(piece) + placeholder.markdown("".join(accumulated_response), unsafe_allow_html=True) + final_text = "".join(accumulated_response) + if not final_text: + placeholder.info("AI 未返回可用内容,请稍后重试或检查凭据配置。") + except Exception as e: + st.error(f"调用 OpenAI API 失败:{str(e)}") + else: + st.warning("没有策略数据可供分析。") + + # Update refresh time + st.session_state['last_refresh'] = datetime.now() elif selected_tab == "📈 预测模型": if render_forecast is not None: @@ -652,67 +729,6 @@ def run_streamlit_app(): else: st.info("请设置模拟参数并点击“应用模拟参数”按钮。") - # --- New Tab 8: GPT 分析 - elif selected_tab == "🔍 GPT 分析": - from openai import OpenAI - st.subheader("GPT 数据分析与改进建议") - # open_ai_key = f"sk-dQhKOOG48iVEfgJfAb14458dA4474fB09aBbE8153d4aB3Fc" - if not HAS_OPENAI: - st.warning("未安装 `openai` 库。请安装后重试。") - elif not openai_api_key: - st.info("请在左侧边栏输入 OpenAI API Key 以启用 GPT 分析。") - else: - if all_strategy_types: - # Generate results if not already - results, recommendation = generate_output_and_recommendations(base, all_strategy_types, - region=region_sel if region_sel != '全市' else '全市') - df_res = pd.DataFrame(results).T - kpi_json = json.dumps(kpi, ensure_ascii=False, indent=2) - results_json = df_res.to_json(orient="records", force_ascii=False) - recommendation_text = recommendation - - # Prepare data to send - data_to_analyze = { - "kpis": kpi_json, - "strategy_results": results_json, - "recommendation": recommendation_text - } - data_str = json.dumps(data_to_analyze, ensure_ascii=False) - - prompt = str(f""" - 请分析以下交通安全分析结果,包括KPI指标、策略评估结果和推荐。 - 提供数据结果的详细分析,以及改进思路和建议。 - 数据:{str(data_str)} - """) - if st.button("上传数据至 GPT 并获取分析"): - if False: - st.info("请将 GPT Base Url 更新为实际可访问的接口地址。") - else: - try: - client = OpenAI( - base_url=open_ai_base_url, - # sk-xxx替换为自己的key - api_key=openai_api_key - ) - response = client.chat.completions.create( - model="gpt-5-mini", - messages=[ - {"role": "system", "content": "You are a helpful assistant that analyzes traffic safety data."}, - {"role": "user", "content": prompt} - ], - stream=False - ) - gpt_response = response.choices[0].message.content - st.markdown("### GPT 分析结果与改进思路") - st.markdown(gpt_response, unsafe_allow_html=True) - except Exception as e: - st.error(f"调用 OpenAI API 失败:{str(e)}") - else: - st.warning("没有策略数据可供分析。") - - # Update refresh time - st.session_state['last_refresh'] = datetime.now() - else: st.info("请先在左侧上传事故数据与策略数据,并点击“应用数据与筛选”按钮。") diff --git a/docs/usage.md b/docs/usage.md index f5b366a..c20b079 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -41,7 +41,7 @@ Use the sidebar form labelled “数据与筛选”. - **📝 策略评估 (Strategy evaluation)** — Aggregates metrics per strategy type, recommends the best option, writes `strategy_evaluation_results.csv`, and updates `recommendation.txt`. - **⚖️ 策略对比 (Strategy comparison)** — side-by-side metrics for selected strategies, useful for “what worked best last month” reviews. - **🧪 情景模拟 (Scenario simulation)** — apply intervention models (persistent/decay, lagged effects) to test potential roll-outs. -- **🔍 GPT 分析** — enter your own OpenAI-compatible API key and base URL in the sidebar to generate narrative insights. Keys are read at runtime only. +- **🔍 AI 分析** — 默认示例 API Key/Base URL 已预填,可直接体验;如需切换自有凭据,可在侧边栏更新后生成洞察(运行时读取,不会写入磁盘)。 - **📍 事故热点 (Hotspot)** — reuse the already uploaded accident data to identify high-risk intersections and produce targeted mitigation ideas; no separate hotspot upload is required. Each tab remembers the active filters from the sidebar so results stay consistent. diff --git a/readme.md b/readme.md index eb25124..492e334 100644 --- a/readme.md +++ b/readme.md @@ -9,7 +9,7 @@ - 检测异常事故点 - 评估交通策略效果并提供推荐 - 识别事故热点路口并生成风险分级与整治建议 -- 支持 GPT 分析生成自然语言洞察 +- 支持 AI 分析生成自然语言洞察 ## 安装步骤 @@ -91,6 +91,7 @@ openai>=2.0.0 - **环境变量**(可选): - `LOG_LEVEL=DEBUG`:启用详细日志 - 示例:`export LOG_LEVEL=DEBUG`(Linux/macOS)或 `set LOG_LEVEL=DEBUG`(Windows) +- **AI 分析凭据**:应用内已预填可用的示例 API Key 与 Base URL,可直接体验;如需使用自有服务,可在侧边栏替换后即时生效。 ## 示例数据 diff --git a/services/hotspot.py b/services/hotspot.py index 3c8a52b..d005a82 100644 --- a/services/hotspot.py +++ b/services/hotspot.py @@ -1,7 +1,7 @@ from __future__ import annotations from datetime import datetime -from typing import Iterable +from typing import Iterable, Optional import numpy as np import pandas as pd @@ -211,11 +211,24 @@ def generate_hotspot_strategies( return strategies -def serialise_datetime_columns(df: pd.DataFrame, columns: Iterable[str]) -> pd.DataFrame: +def serialise_datetime_columns(df: pd.DataFrame, columns: Optional[Iterable[str]] = None) -> pd.DataFrame: result = df.copy() + if columns is None: + columns = result.columns for column in columns: - if column in result.columns and pd.api.types.is_datetime64_any_dtype(result[column]): - result[column] = result[column].dt.strftime("%Y-%m-%d %H:%M:%S") + if column not in result.columns: + continue + series = result[column] + if pd.api.types.is_datetime64_any_dtype(series): + result[column] = series.dt.strftime("%Y-%m-%d %H:%M:%S") + else: + has_timestamp = series.map(lambda value: isinstance(value, (datetime, pd.Timestamp))).any() + if has_timestamp: + result[column] = series.map( + lambda value: value.strftime("%Y-%m-%d %H:%M:%S") + if isinstance(value, (datetime, pd.Timestamp)) + else value + ) return result @@ -224,4 +237,3 @@ def _mode_fallback(series: pd.Series) -> str: return "" mode = series.mode() return str(mode.iloc[0]) if not mode.empty else str(series.iloc[0]) - diff --git a/ui_sections/hotspot.py b/ui_sections/hotspot.py index 34c6606..d2e5e53 100644 --- a/ui_sections/hotspot.py +++ b/ui_sections/hotspot.py @@ -154,10 +154,7 @@ def render_hotspot(accident_records, accident_source_name: str | None) -> None: ) 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], - ) + serializable = serialise_datetime_columns(top_hotspots.reset_index()) report_payload = { "analysis_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "time_window": time_window, @@ -186,4 +183,3 @@ def render_hotspot(accident_records, accident_source_name: str | None) -> None: preview_cols = ["事故时间", "所在街道", "事故类型", "事故具体地点", "道路类型"] preview_df = hotspot_data[preview_cols].copy() st.dataframe(preview_df.head(10), use_container_width=True) -