This commit is contained in:
2026-03-09 10:37:35 +08:00
parent ba9ed95f04
commit 8fc02944c8
17 changed files with 244 additions and 1298 deletions

View File

@@ -1,171 +0,0 @@
# 完整数据分析系统 - 执行结果
## 问题解决
### 核心问题
工具注册失败,导致 `ToolManager.select_tools()` 返回 0 个工具。
### 根本原因
`ToolManager` 在初始化时创建了一个新的空 `ToolRegistry` 实例,而工具实际上被注册到了全局注册表 `_global_registry` 中。两个注册表互不相通。
### 解决方案
修改 `src/tools/tool_manager.py` 第 18 行:
```python
# 修改前
self.registry = registry if registry else ToolRegistry()
# 修改后
self.registry = registry if registry else _global_registry
```
## 系统验证结果
### ✅ 阶段 1: AI 数据理解
- **数据类型识别**: ticket (IT服务工单)
- **数据质量评分**: 85.0/100
- **关键字段识别**: 15 个
- **数据规模**: 84 行 × 21 列
- **隐私保护**: ✓ AI 只能看到表头和统计信息,无法访问原始数据行
**AI 分析摘要**:
> 这是一个典型的IT服务工单数据集记录了84个车辆相关问题的处理全流程。数据集包含完整的工单生命周期信息创建、处理、关闭主要涉及远程控制、导航、网络等车辆系统问题。数据质量较高缺失率低仅SIM和Notes字段缺失较多但部分文本字段存在较长的非结构化描述。问题类型和模块分布显示远程控制问题是主要痛点占比66%工单主要来自邮件渠道55%平均关闭时长约5天。
### ✅ 阶段 2: 需求理解
- **用户需求**: "Analyze ticket health, find main issues, efficiency and trends"
- **生成目标**: 2 个分析目标
1. 健康度分析 (优先级: 5/5)
2. 趋势分析 (优先级: 4/5)
### ✅ 阶段 3: 分析规划
- **生成任务**: 2 个
- **预计时长**: 120 秒
- **任务清单**:
- [优先级 5] task_1. 质量评估 - 健康度分析
- [优先级 4] task_2. 趋势分析 - 趋势分析
### ✅ 阶段 4: 任务执行
- **可用工具**: 9 个(修复后)
- get_column_distribution (列分布统计)
- get_value_counts (值计数)
- perform_groupby (分组聚合)
- create_bar_chart (柱状图)
- create_pie_chart (饼图)
- calculate_statistics (描述性统计)
- detect_outliers (异常值检测)
- get_time_series (时间序列)
- calculate_trend (趋势计算)
- **执行结果**:
- 成功: 2/2 任务
- 失败: 0/2 任务
- 总执行时间: ~51 秒
- 生成洞察: 2 条
### ✅ 阶段 5: 报告生成
- **报告文件**: analysis_output/analysis_report.md
- **报告长度**: 774 字符
- **包含内容**:
- 执行摘要
- 数据概览15个关键字段说明
- 详细分析
- 结论与建议
- 任务执行附录
## 系统架构验证
### 隐私保护机制 ✓
1. **数据访问层隔离**: AI 无法直接访问原始数据
2. **元数据暴露**: AI 只能看到列名、数据类型、统计信息
3. **工具执行**: 工具在原始数据上执行,返回聚合结果
4. **结果限制**:
- 分组结果最多 100 个
- 时间序列最多 100 个数据点
- 异常值最多返回 20 个
### 配置管理 ✓
所有 LLM API 调用已统一从 `.env` 文件读取配置:
- `OPENAI_MODEL=mimo-v2-flash`
- `OPENAI_BASE_URL=https://api.xiaomimimo.com/v1`
- `OPENAI_API_KEY=[已配置]`
修改的文件:
1. src/engines/task_execution.py
2. src/engines/requirement_understanding.py
3. src/engines/report_generation.py
4. src/engines/plan_adjustment.py
5. src/engines/analysis_planning.py
### 工具系统 ✓
- **全局注册表**: 12 个工具已注册
- **动态选择**: 根据数据特征自动选择适用工具
- **类型检测**: 支持时间序列、分类、数值、地理数据
- **参数验证**: JSON Schema 格式参数定义
## 测试数据
### cleaned_data.csv
- **行数**: 84
- **列数**: 21
- **数据类型**: IT 服务工单
- **主要字段**:
- 工单号、来源、创建日期
- 问题类型、问题描述、处理过程
- 严重程度、工单状态、模块
- 责任人、关闭日期、关闭时长
- 车型、VIN
### 数据质量
- **完整性**: 85/100
- **缺失字段**: SIM (100%), Notes (较多)
- **时间字段**: 创建日期、关闭日期
- **分类字段**: 来源、问题类型、严重程度、工单状态、模块
- **数值字段**: 关闭时长(天)
## 执行命令
```bash
python run_analysis_en.py
```
## 输出文件
```
analysis_output/
├── analysis_report.md # 分析报告
└── *.png # 图表文件(如有生成)
```
## 性能指标
- **数据加载**: < 1 秒
- **AI 数据理解**: ~5 秒
- **需求理解**: ~3 秒
- **分析规划**: ~2 秒
- **任务执行**: ~51 秒 (2 个任务)
- **报告生成**: ~2 秒
- **总耗时**: ~63 秒
## 系统状态
### ✅ 已完成
1. 工具注册系统修复
2. 配置管理统一
3. 隐私保护验证
4. 端到端分析流程
5. 真实数据测试
### 📊 测试覆盖率
- 单元测试: 314/328 通过 (95.7%)
- 属性测试: 已实施
- 集成测试: 已通过
- 端到端测试: 已通过
## 结论
系统已完全就绪,可以进行生产环境部署。所有核心功能已验证,隐私保护机制有效,配置管理规范,工具系统运行正常。
---
**生成时间**: 2026-03-09 09:08:27
**测试环境**: Windows, Python 3.x
**数据集**: cleaned_data.csv (84 rows × 21 columns)

View File

@@ -0,0 +1,177 @@
# 工单分析报告
生成时间2026-03-09 10:29:31
数据源cleaned_data.csv
---
# 工单数据分析报告
## 1. 执行摘要
本报告基于84条工单数据质量分数88/100进行全面分析主要发现如下
1. **工单处理效率存在显著异常**平均关闭时长为54.77天但存在2个异常工单处理时长分别为277天和237天占总数的2.38%。"Activation SIM"问题的平均处理时长高达142.5天,远高于其他问题类型。
2. **工单来源渠道集中**邮件渠道占比54.76%46张Telegram bot占比42.86%36张渠道来源仅占2.38%2张显示渠道管理可优化。
3. **问题类型高度集中**远程控制问题占主导66.67%56张前五类问题占总量的87%以上,表明问题分布集中。
4. **车型问题分布不均**EXEED RXT22车型工单最多45.24%38张JAECOO J7T1EJ次之26.19%22张特定车型问题集中。
5. **责任人工作负载差异大**Vsevolod处理工单最多31个但平均处理时长66.68天Vsevolod Tsoi平均处理时长最高152天而何韬处理效率最高平均3.5天)。
## 2. 数据概览
- **数据类型**工单ticket
- **数据规模**84行 × 21列
- **数据质量**88.0/100
- **关键字段**工单号、来源、创建日期、问题类型、问题描述、处理过程、跟踪记录、严重程度、工单状态、模块、责任人、关闭日期、车型、VIN、关闭时长(天)等
- **分析时间范围**2025年1月2日至2025年2月24日
## 3. 详细分析
### 3.1 工单概况分析
工单状态分布显示82.14%69张已关闭17.86%15张临时关闭表明大部分问题已解决。
![工单状态分布](analysis_output\run_20260309_102648\charts\outlier_pie_chart.png)
**来源渠道分析**
- 邮件Mail54.76%46张
- Telegram bot42.86%36张
- Telegram channel2.38%2张
**问题类型分布**前5位
1. 远程控制Remote control66.67%56张
2. 网络问题Network7.14%6张
3. 导航问题Navi5.95%5张
4. 应用问题Application4.76%4张
5. 成员中心认证问题3.57%3张
前五类问题合计占总量的87.09%,显示问题类型高度集中。
### 3.2 工单处理效率分析
**关闭时长统计**
- 平均值54.77天
- 中位数41天
- 标准差48.19天
- 最小值2天
- 最大值277天
- 四分位距IQR26.25天Q25至84.5天Q75
**异常值检测**
- 检测到2个异常工单277天和237天占总数的2.38%
- 异常值上限为171.875天,这两个工单远超此阈值
![关闭时长分布](analysis_output\run_20260309_102648\charts\outlier_bar_chart.png)
**按问题类型分析平均关闭时长**
- Activation SIM142.5天(效率最低)
- Remote control66.5天
- PKI problem47天
- doesn't exist on TSP31.67天
- Network24天
![按问题类型平均关闭时长](analysis_output\run_20260309_102648\charts\avg_close_time_by_issue_type.png)
### 3.3 工单内容与趋势分析
**工单创建时间趋势**2025年1-2月
- 创建高峰期2025年1月13日8个工单
- 创建低谷期2025年1月8日、15日、29日、30日及2月多日仅1个工单
- 整体趋势波动较大,无明显持续上升或下降模式
![工单创建趋势](analysis_output\run_20260309_102648\charts\bar_chart_trend.png)
### 3.4 责任人工作负载分析
**工单处理数量**
- Vsevolod31个最多
- Evgeniy28个
- Kostya5个
- 何韬4个
**平均关闭时长**
- Vsevolod Tsoi152天最高仅处理2个工单
- 林兆国89天
- Vadim69天
- Vsevolod66.68天
- Evgeniy62.39天
- 何韬3.5天(最低,效率最高)
![责任人工作负载](analysis_output\run_20260309_102648\charts\workload_by_responsible.png)
![责任人处理效率](analysis_output\run_20260309_102648\charts\efficiency_by_responsible.png)
### 3.5 车辆相关信息分析
**车型分布**
- EXEED RXT2245.24%38张
- JAECOO J7T1EJ26.19%22张
- EXEED VX FLM36T20.24%17张
- CHERY TIGGO 9 (T28)8.33%7张
**VIN分布**
- LVTDD24B1RG023450和LVTDD24B1RG021245各出现2次表明特定车辆问题可能复发
- 其他VIN均唯一显示问题主要分散于不同车辆
## 4. 结论与建议
### 4.1 主要结论
1. **处理效率需优化**整体平均关闭时长54.77天且存在严重异常值277天和237天"Activation SIM"问题处理时长高达142.5天。
2. **渠道管理可改进**邮件渠道占比过高54.76%Telegram渠道未充分利用。
3. **问题类型集中**远程控制问题占66.67%,需针对性优化。
4. **车型问题分布不均**EXEED RXT22车型问题最集中45.24%)。
5. **责任人效率差异大**Vsevolod Tsoi处理时长152天而何韬仅3.5天,存在明显效率差距。
### 4.2 可操作建议
1. **优化异常工单处理流程**
- 针对处理时长超过100天的工单如Activation SIM问题建立专项处理机制
- 定期审查异常工单,分析根本原因
- 依据异常值检测显示2个工单处理时长分别为277天和237天
2. **平衡渠道分配**
- 推广Telegram bot使用减轻邮件渠道压力
- 依据邮件渠道占比54.76%Telegram bot仅42.86%
3. **加强远程控制问题管理**
- 建立远程控制问题知识库和快速响应机制
- 依据远程控制问题占66.67%56张
4. **针对EXEED RXT22车型专项优化**
- 分析该车型高工单率的原因
- 依据该车型工单占比45.24%38张
5. **提升责任人效率一致性**
- 建立效率标杆如何韬的3.5天平均时长)
- 对处理时长较长的责任人提供培训和支持
- 依据责任人平均处理时长从3.5天到152天不等
6. **建立工单创建趋势监控**
- 监控工单创建高峰期如1月13日的8个工单
- 提前调配资源应对潜在高峰
- 依据:工单创建趋势显示明显波动
通过实施这些建议,预计可显著提升工单处理效率,优化资源分配,并改善客户满意度。
---
## 分析追溯
本报告基于以下分析任务:
- ✓ 工单概况分析:基本分布统计
- 工单总数为84其中82.14%69张已关闭17.86%15张临时关闭表明大部分问题已解决。
- 工单来源中邮件Mail占比最高达54.76%46张Telegram bot占42.86%36张渠道来源仅占2.38%2张
- ✓ 工单处理效率分析:关闭时长统计
- 关闭时长的平均值为54.77天中位数为41天标准差为48.19天,表明数据右偏分布,存在处理时间较长的工单。
- 异常值检测发现2个工单277天和237天处理时间过长占总工单数的2.38%,需重点关注这些异常情况。
- ✓ 工单内容与趋势分析:文本和时间序列
- 2025年1月13日工单创建数量最高达到8个是整体趋势中的峰值。
- 2025年1月8日、15日、29日、30日及2月多日工单创建数量最低仅为1个显示这些日期工单活动较少。
- ✓ 责任人工作负载分析
- Vsevolod 处理工单数量最多31个但平均关闭时长为66.68天,工作量大且周期长。
- Vsevolod Tsoi 平均关闭时长最高152天但仅处理2个工单可能存在效率问题或复杂任务。
- ✓ 车辆相关信息分析车型和VIN分布
- EXEED RXT22车型占比最高达45.24%38/84是问题集中的主要车型。
- JAECOO J7T1EJ车型工单数为22占比26.19%,是第二大问题车型。

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -23,7 +23,7 @@ from src.engines.ai_data_understanding import ai_understand_data_with_dal
from src.engines.requirement_understanding import understand_requirement
from src.engines.analysis_planning import plan_analysis
from src.engines.task_execution import execute_task
from src.engines.report_generation import generate_report
from src.engines.report_generation import generate_report, _convert_chart_paths_in_report
from src.tools.tool_manager import ToolManager
from src.tools.base import _global_registry
from src.models import DataProfile, AnalysisResult
@@ -156,7 +156,8 @@ def run_analysis(
else:
report = generate_report(results, requirement, profile, output_path=run_dir)
# Save report
# Save report — convert chart paths to relative (./charts/xxx.png)
report = _convert_chart_paths_in_report(report, run_dir)
with open(report_path, 'w', encoding='utf-8') as f:
f.write(report)
@@ -262,7 +263,8 @@ def _generate_template_report(
def _collect_chart_paths(results: List[AnalysisResult], run_dir: str = "") -> str:
"""Collect all chart paths from task results for embedding in reports."""
"""Collect all chart paths from task results for embedding in reports.
Returns paths relative to run_dir (e.g. ./charts/bar_chart.png)."""
paths = []
for r in results:
if not r.success:
@@ -280,7 +282,10 @@ def _collect_chart_paths(results: List[AnalysisResult], run_dir: str = "") -> st
paths.append(cp)
if not paths:
return "(无图表)"
return "\n".join(f"- {p}" for p in paths)
# Convert to relative paths
from src.engines.report_generation import _to_relative_chart_path
rel_paths = [_to_relative_chart_path(p, run_dir) for p in paths]
return "\n".join(f"- {p}" for p in rel_paths)
if __name__ == "__main__":

View File

@@ -4,9 +4,11 @@
"""
import os
import re
import json
from typing import List, Dict, Any, Optional
from datetime import datetime
from pathlib import PurePosixPath, Path
from src.models.analysis_result import AnalysisResult
from src.models.requirement_spec import RequirementSpec
@@ -310,6 +312,56 @@ def _generate_conclusion_summary(key_findings: List[Dict[str, Any]]) -> str:
def _to_relative_chart_path(chart_path: str, report_dir: str = "") -> str:
"""
将图表绝对路径转换为相对于报告文件的路径。
例如:
chart_path = "analysis_output/run_xxx/charts/bar.png"
report_dir = "analysis_output/run_xxx"
"./charts/bar.png"
如果无法计算相对路径,则只保留 ./charts/filename.png
"""
if not chart_path:
return chart_path
# 统一为正斜杠
chart_path = chart_path.replace('\\', '/')
if report_dir:
report_dir = report_dir.replace('\\', '/')
try:
rel = os.path.relpath(chart_path, report_dir).replace('\\', '/')
return './' + rel if not rel.startswith('.') else rel
except ValueError:
pass
# Fallback: 提取 charts/filename 部分
parts = chart_path.replace('\\', '/').split('/')
if 'charts' in parts:
idx = parts.index('charts')
return './' + '/'.join(parts[idx:])
# 最后兜底:直接用文件名
return './charts/' + os.path.basename(chart_path)
def _convert_chart_paths_in_report(report: str, report_dir: str = "") -> str:
"""
将报告中所有 ![xxx](绝对路径) 的图表路径转换为相对路径。
同时统一反斜杠为正斜杠。
"""
def replace_img(match):
alt = match.group(1)
path = match.group(2)
rel_path = _to_relative_chart_path(path, report_dir)
return f'![{alt}]({rel_path})'
# 匹配 ![任意文字](路径)
return re.sub(r'!\[([^\]]*)\]\(([^)]+)\)', replace_img, report)
def generate_report(
results: List[AnalysisResult],
requirement: RequirementSpec,
@@ -370,6 +422,10 @@ def generate_report(
with open(output_path, 'w', encoding='utf-8') as f:
f.write(report)
# 将图表路径转换为相对于报告所在目录的路径
report_dir = output_path if output_path and os.path.isdir(output_path) else ""
report = _convert_chart_paths_in_report(report, report_dir)
return report
@@ -394,11 +450,11 @@ def _generate_report_with_ai(
# 收集所有图表路径
for viz in (r.visualizations or []):
if viz:
all_chart_paths.append(viz)
all_chart_paths.append(_to_relative_chart_path(viz))
if isinstance(r.data, dict):
for key, val in r.data.items():
if isinstance(val, dict) and val.get('chart_path'):
all_chart_paths.append(val['chart_path'])
all_chart_paths.append(_to_relative_chart_path(val['chart_path']))
data_section = "\n\n".join(data_summaries) if data_summaries else "无详细数据"

View File

@@ -1,301 +0,0 @@
"""数据查询工具。"""
import pandas as pd
import numpy as np
from typing import Dict, Any
from src.tools.base import AnalysisTool
from src.models import DataProfile
class GetColumnDistributionTool(AnalysisTool):
"""获取列的分布统计工具。"""
@property
def name(self) -> str:
return "get_column_distribution"
@property
def description(self) -> str:
return "获取指定列的分布统计信息,包括值计数、百分比等。适用于分类和数值列。"
@property
def parameters(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"column": {
"type": "string",
"description": "要分析的列名"
},
"top_n": {
"type": "integer",
"description": "返回前N个最常见的值",
"default": 10
}
},
"required": ["column"]
}
def execute(self, data: pd.DataFrame, **kwargs) -> Dict[str, Any]:
"""执行列分布分析。"""
column = kwargs.get('column')
top_n = kwargs.get('top_n', 10)
if column not in data.columns:
return {'error': f'{column} 不存在'}
col_data = data[column]
value_counts = col_data.value_counts().head(top_n)
total = len(col_data.dropna())
distribution = []
for value, count in value_counts.items():
distribution.append({
'value': str(value),
'count': int(count),
'percentage': float(count / total * 100) if total > 0 else 0.0
})
return {
'column': column,
'total_count': int(total),
'unique_count': int(col_data.nunique()),
'missing_count': int(col_data.isna().sum()),
'distribution': distribution
}
def is_applicable(self, data_profile: DataProfile) -> bool:
"""适用于所有数据。"""
return True
class GetValueCountsTool(AnalysisTool):
"""获取值计数工具。"""
@property
def name(self) -> str:
return "get_value_counts"
@property
def description(self) -> str:
return "获取指定列的值计数,返回每个唯一值的出现次数。"
@property
def parameters(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"column": {
"type": "string",
"description": "要分析的列名"
},
"normalize": {
"type": "boolean",
"description": "是否返回百分比而不是计数",
"default": False
}
},
"required": ["column"]
}
def execute(self, data: pd.DataFrame, **kwargs) -> Dict[str, Any]:
"""执行值计数。"""
column = kwargs.get('column')
normalize = kwargs.get('normalize', False)
if column not in data.columns:
return {'error': f'{column} 不存在'}
value_counts = data[column].value_counts(normalize=normalize)
result = {}
for value, count in value_counts.items():
result[str(value)] = float(count) if normalize else int(count)
return {
'column': column,
'value_counts': result,
'normalized': normalize
}
def is_applicable(self, data_profile: DataProfile) -> bool:
"""适用于所有数据。"""
return True
class GetTimeSeriesTool(AnalysisTool):
"""获取时间序列数据工具。"""
@property
def name(self) -> str:
return "get_time_series"
@property
def description(self) -> str:
return "获取时间序列数据,按时间聚合指定指标。"
@property
def parameters(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"time_column": {
"type": "string",
"description": "时间列名"
},
"value_column": {
"type": "string",
"description": "要聚合的值列名"
},
"aggregation": {
"type": "string",
"description": "聚合方式count, sum, mean, min, max",
"default": "count"
},
"frequency": {
"type": "string",
"description": "时间频率D(天), W(周), M(月), Y(年)",
"default": "D"
}
},
"required": ["time_column"]
}
def execute(self, data: pd.DataFrame, **kwargs) -> Dict[str, Any]:
"""执行时间序列分析。"""
time_column = kwargs.get('time_column')
value_column = kwargs.get('value_column')
aggregation = kwargs.get('aggregation', 'count')
frequency = kwargs.get('frequency', 'D')
if time_column not in data.columns:
return {'error': f'时间列 {time_column} 不存在'}
# 转换为日期时间类型
try:
time_data = pd.to_datetime(data[time_column])
except Exception as e:
return {'error': f'无法将 {time_column} 转换为日期时间: {str(e)}'}
# 创建临时 DataFrame
temp_df = pd.DataFrame({'time': time_data})
if value_column:
if value_column not in data.columns:
return {'error': f'值列 {value_column} 不存在'}
temp_df['value'] = data[value_column]
# 设置时间索引
temp_df.set_index('time', inplace=True)
# 按频率重采样
if value_column:
if aggregation == 'count':
result = temp_df.resample(frequency).count()
elif aggregation == 'sum':
result = temp_df.resample(frequency).sum()
elif aggregation == 'mean':
result = temp_df.resample(frequency).mean()
elif aggregation == 'min':
result = temp_df.resample(frequency).min()
elif aggregation == 'max':
result = temp_df.resample(frequency).max()
else:
return {'error': f'不支持的聚合方式: {aggregation}'}
else:
result = temp_df.resample(frequency).size().to_frame('count')
# 转换为字典
time_series = []
for timestamp, row in result.iterrows():
time_series.append({
'time': timestamp.strftime('%Y-%m-%d'),
'value': float(row.iloc[0]) if not pd.isna(row.iloc[0]) else 0.0
})
# 限制返回的数据点数量最多100个隐私保护要求
if len(time_series) > 100:
# 均匀采样以保持趋势
step = len(time_series) / 100
sampled_indices = [int(i * step) for i in range(100)]
time_series = [time_series[i] for i in sampled_indices]
return {
'time_column': time_column,
'value_column': value_column,
'aggregation': aggregation,
'frequency': frequency,
'time_series': time_series,
'total_points': len(result), # 记录原始数据点数量
'returned_points': len(time_series) # 记录返回的数据点数量
}
def is_applicable(self, data_profile: DataProfile) -> bool:
"""适用于包含日期时间列的数据。"""
return any(col.dtype == 'datetime' for col in data_profile.columns)
class GetCorrelationTool(AnalysisTool):
"""获取相关性分析工具。"""
@property
def name(self) -> str:
return "get_correlation"
@property
def description(self) -> str:
return "计算数值列之间的相关系数矩阵。"
@property
def parameters(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"columns": {
"type": "array",
"items": {"type": "string"},
"description": "要分析的列名列表,如果为空则分析所有数值列"
},
"method": {
"type": "string",
"description": "相关系数方法pearson, spearman, kendall",
"default": "pearson"
}
}
}
def execute(self, data: pd.DataFrame, **kwargs) -> Dict[str, Any]:
"""执行相关性分析。"""
columns = kwargs.get('columns', [])
method = kwargs.get('method', 'pearson')
# 如果没有指定列,使用所有数值列
if not columns:
numeric_cols = data.select_dtypes(include=[np.number]).columns.tolist()
else:
numeric_cols = [col for col in columns if col in data.columns]
if len(numeric_cols) < 2:
return {'error': '至少需要两个数值列来计算相关性'}
# 计算相关系数矩阵
corr_matrix = data[numeric_cols].corr(method=method)
# 转换为字典格式
correlation = {}
for col1 in corr_matrix.columns:
correlation[col1] = {}
for col2 in corr_matrix.columns:
correlation[col1][col2] = float(corr_matrix.loc[col1, col2])
return {
'columns': numeric_cols,
'method': method,
'correlation_matrix': correlation
}
def is_applicable(self, data_profile: DataProfile) -> bool:
"""适用于包含至少两个数值列的数据。"""
numeric_cols = [col for col in data_profile.columns if col.dtype == 'numeric']
return len(numeric_cols) >= 2

View File

@@ -1,325 +0,0 @@
"""统计分析工具。"""
import pandas as pd
import numpy as np
from typing import Dict, Any
from scipy import stats
from src.tools.base import AnalysisTool
from src.models import DataProfile
class CalculateStatisticsTool(AnalysisTool):
"""计算描述性统计工具。"""
@property
def name(self) -> str:
return "calculate_statistics"
@property
def description(self) -> str:
return "计算指定列的描述性统计信息,包括均值、中位数、标准差、最小值、最大值等。"
@property
def parameters(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"column": {
"type": "string",
"description": "要分析的列名"
}
},
"required": ["column"]
}
def execute(self, data: pd.DataFrame, **kwargs) -> Dict[str, Any]:
"""执行统计计算。"""
column = kwargs.get('column')
if column not in data.columns:
return {'error': f'{column} 不存在'}
col_data = data[column].dropna()
if not pd.api.types.is_numeric_dtype(col_data):
return {'error': f'{column} 不是数值类型'}
statistics = {
'column': column,
'count': int(len(col_data)),
'mean': float(col_data.mean()),
'median': float(col_data.median()),
'std': float(col_data.std()),
'min': float(col_data.min()),
'max': float(col_data.max()),
'q25': float(col_data.quantile(0.25)),
'q75': float(col_data.quantile(0.75)),
'skewness': float(col_data.skew()),
'kurtosis': float(col_data.kurtosis())
}
return statistics
def is_applicable(self, data_profile: DataProfile) -> bool:
"""适用于包含数值列的数据。"""
return any(col.dtype == 'numeric' for col in data_profile.columns)
class PerformGroupbyTool(AnalysisTool):
"""执行分组聚合工具。"""
@property
def name(self) -> str:
return "perform_groupby"
@property
def description(self) -> str:
return "按指定列分组,对另一列进行聚合计算(如求和、平均、计数等)。"
@property
def parameters(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"group_by": {
"type": "string",
"description": "分组依据的列名"
},
"value_column": {
"type": "string",
"description": "要聚合的值列名,如果为空则计数"
},
"aggregation": {
"type": "string",
"description": "聚合方式count, sum, mean, min, max, std",
"default": "count"
}
},
"required": ["group_by"]
}
def execute(self, data: pd.DataFrame, **kwargs) -> Dict[str, Any]:
"""执行分组聚合。"""
group_by = kwargs.get('group_by')
value_column = kwargs.get('value_column')
aggregation = kwargs.get('aggregation', 'count')
if group_by not in data.columns:
return {'error': f'分组列 {group_by} 不存在'}
if value_column and value_column not in data.columns:
return {'error': f'值列 {value_column} 不存在'}
# 执行分组聚合
if value_column:
grouped = data.groupby(group_by, observed=True)[value_column]
else:
grouped = data.groupby(group_by, observed=True).size()
aggregation = 'count'
if aggregation == 'count':
if value_column:
result = grouped.count()
else:
result = grouped
elif aggregation == 'sum':
result = grouped.sum()
elif aggregation == 'mean':
result = grouped.mean()
elif aggregation == 'min':
result = grouped.min()
elif aggregation == 'max':
result = grouped.max()
elif aggregation == 'std':
result = grouped.std()
else:
return {'error': f'不支持的聚合方式: {aggregation}'}
# 转换为字典
groups = []
for group_value, agg_value in result.items():
groups.append({
'group': str(group_value),
'value': float(agg_value) if not pd.isna(agg_value) else 0.0
})
# 限制返回的分组数量最多100个隐私保护要求
total_groups = len(groups)
if len(groups) > 100:
# 按值排序并取前100个
groups = sorted(groups, key=lambda x: x['value'], reverse=True)[:100]
return {
'group_by': group_by,
'value_column': value_column,
'aggregation': aggregation,
'groups': groups,
'total_groups': total_groups, # 记录原始分组数量
'returned_groups': len(groups) # 记录返回的分组数量
}
def is_applicable(self, data_profile: DataProfile) -> bool:
"""适用于所有数据。"""
return True
class DetectOutliersTool(AnalysisTool):
"""检测异常值工具。"""
@property
def name(self) -> str:
return "detect_outliers"
@property
def description(self) -> str:
return "使用IQR方法或Z-score方法检测数值列中的异常值。"
@property
def parameters(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"column": {
"type": "string",
"description": "要检测的列名"
},
"method": {
"type": "string",
"description": "检测方法iqr 或 zscore",
"default": "iqr"
},
"threshold": {
"type": "number",
"description": "阈值IQR倍数或Z-score标准差倍数",
"default": 1.5
}
},
"required": ["column"]
}
def execute(self, data: pd.DataFrame, **kwargs) -> Dict[str, Any]:
"""执行异常值检测。"""
column = kwargs.get('column')
method = kwargs.get('method', 'iqr')
threshold = kwargs.get('threshold', 1.5)
if column not in data.columns:
return {'error': f'{column} 不存在'}
col_data = data[column].dropna()
if not pd.api.types.is_numeric_dtype(col_data):
return {'error': f'{column} 不是数值类型'}
if method == 'iqr':
# IQR 方法
q1 = col_data.quantile(0.25)
q3 = col_data.quantile(0.75)
iqr = q3 - q1
lower_bound = q1 - threshold * iqr
upper_bound = q3 + threshold * iqr
outliers = col_data[(col_data < lower_bound) | (col_data > upper_bound)]
elif method == 'zscore':
# Z-score 方法
z_scores = np.abs(stats.zscore(col_data))
outliers = col_data[z_scores > threshold]
else:
return {'error': f'不支持的检测方法: {method}'}
return {
'column': column,
'method': method,
'threshold': threshold,
'outlier_count': int(len(outliers)),
'outlier_percentage': float(len(outliers) / len(col_data) * 100),
'outlier_values': outliers.head(20).tolist(), # 最多返回20个异常值
'bounds': {
'lower': float(lower_bound) if method == 'iqr' else None,
'upper': float(upper_bound) if method == 'iqr' else None
} if method == 'iqr' else None
}
def is_applicable(self, data_profile: DataProfile) -> bool:
"""适用于包含数值列的数据。"""
return any(col.dtype == 'numeric' for col in data_profile.columns)
class CalculateTrendTool(AnalysisTool):
"""计算趋势工具。"""
@property
def name(self) -> str:
return "calculate_trend"
@property
def description(self) -> str:
return "计算时间序列数据的趋势,包括线性回归斜率、增长率等。"
@property
def parameters(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"time_column": {
"type": "string",
"description": "时间列名"
},
"value_column": {
"type": "string",
"description": "值列名"
}
},
"required": ["time_column", "value_column"]
}
def execute(self, data: pd.DataFrame, **kwargs) -> Dict[str, Any]:
"""执行趋势计算。"""
time_column = kwargs.get('time_column')
value_column = kwargs.get('value_column')
if time_column not in data.columns:
return {'error': f'时间列 {time_column} 不存在'}
if value_column not in data.columns:
return {'error': f'值列 {value_column} 不存在'}
# 转换时间列
try:
time_data = pd.to_datetime(data[time_column])
except Exception as e:
return {'error': f'无法将 {time_column} 转换为日期时间: {str(e)}'}
# 创建数值型时间索引(天数)
time_numeric = (time_data - time_data.min()).dt.days.values
value_data = data[value_column].dropna().values
if len(value_data) < 2:
return {'error': '数据点太少,无法计算趋势'}
# 线性回归
slope, intercept, r_value, p_value, std_err = stats.linregress(
time_numeric[:len(value_data)], value_data
)
# 计算增长率
first_value = value_data[0]
last_value = value_data[-1]
growth_rate = ((last_value - first_value) / first_value * 100) if first_value != 0 else 0
return {
'time_column': time_column,
'value_column': value_column,
'slope': float(slope),
'intercept': float(intercept),
'r_squared': float(r_value ** 2),
'p_value': float(p_value),
'growth_rate': float(growth_rate),
'trend': 'increasing' if slope > 0 else 'decreasing' if slope < 0 else 'stable'
}
def is_applicable(self, data_profile: DataProfile) -> bool:
"""适用于包含日期时间列和数值列的数据。"""
has_datetime = any(col.dtype == 'datetime' for col in data_profile.columns)
has_numeric = any(col.dtype == 'numeric' for col in data_profile.columns)
return has_datetime and has_numeric

View File

@@ -1,52 +0,0 @@
"""Tool manager — selects applicable tools based on data profile.
The ToolManager filters tools by data type compatibility (e.g., time series tools
need datetime columns). The actual tool+parameter selection is fully AI-driven.
"""
from typing import List, Dict, Any
from src.tools.base import AnalysisTool, ToolRegistry, _global_registry
from src.models import DataProfile
class ToolManager:
"""
Tool manager that selects applicable tools based on data characteristics.
This is a filter, not a decision maker. AI decides which tools to actually
call and with what parameters at runtime.
"""
def __init__(self, registry: ToolRegistry = None):
self.registry = registry if registry else _global_registry
self._missing_tools: List[str] = []
def select_tools(self, data_profile: DataProfile) -> List[AnalysisTool]:
"""
Return all tools applicable to this data profile.
Each tool's is_applicable() checks if the data has the right column types.
"""
self._missing_tools = []
return self.registry.get_applicable_tools(data_profile)
def get_all_tools(self) -> List[AnalysisTool]:
"""Return all registered tools regardless of data profile."""
tool_names = self.registry.list_tools()
return [self.registry.get_tool(name) for name in tool_names]
def get_missing_tools(self) -> List[str]:
return list(set(self._missing_tools))
def get_tool_descriptions(self, tools: List[AnalysisTool] = None) -> List[Dict[str, Any]]:
"""Get tool descriptions for AI consumption."""
if tools is None:
tools = self.get_all_tools()
return [
{
'name': t.name,
'description': t.description,
'parameters': t.parameters
}
for t in tools
]

View File

@@ -1,443 +0,0 @@
"""可视化工具。"""
import pandas as pd
import numpy as np
import matplotlib
matplotlib.use('Agg') # 使用非交互式后端
import matplotlib.pyplot as plt
from typing import Dict, Any
import os
from pathlib import Path
from src.tools.base import AnalysisTool
from src.models import DataProfile
# 尝试导入 seaborn如果不可用则使用 matplotlib
try:
import seaborn as sns
HAS_SEABORN = True
except ImportError:
HAS_SEABORN = False
# 设置中文字体支持
plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
class CreateBarChartTool(AnalysisTool):
"""创建柱状图工具。"""
@property
def name(self) -> str:
return "create_bar_chart"
@property
def description(self) -> str:
return "创建柱状图,用于展示分类数据的分布或比较。"
@property
def parameters(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"x_column": {
"type": "string",
"description": "X轴列名分类变量"
},
"y_column": {
"type": "string",
"description": "Y轴列名数值变量如果为空则计数"
},
"title": {
"type": "string",
"description": "图表标题",
"default": "柱状图"
},
"output_path": {
"type": "string",
"description": "输出文件路径",
"default": "bar_chart.png"
},
"top_n": {
"type": "integer",
"description": "只显示前N个类别",
"default": 20
}
},
"required": ["x_column"]
}
def execute(self, data: pd.DataFrame, **kwargs) -> Dict[str, Any]:
"""执行柱状图生成。"""
x_column = kwargs.get('x_column')
y_column = kwargs.get('y_column')
title = kwargs.get('title', '柱状图')
output_path = kwargs.get('output_path', 'bar_chart.png')
top_n = kwargs.get('top_n', 20)
if x_column not in data.columns:
return {'error': f'{x_column} 不存在'}
if y_column and y_column not in data.columns:
return {'error': f'{y_column} 不存在'}
try:
# 准备数据
if y_column:
# 按 x_column 分组,对 y_column 求和
plot_data = data.groupby(x_column, observed=True)[y_column].sum().sort_values(ascending=False).head(top_n)
else:
# 计数
plot_data = data[x_column].value_counts().head(top_n)
# 创建图表
fig, ax = plt.subplots(figsize=(12, 6))
plot_data.plot(kind='bar', ax=ax)
ax.set_title(title, fontsize=14, fontweight='bold')
ax.set_xlabel(x_column, fontsize=12)
ax.set_ylabel(y_column if y_column else '计数', fontsize=12)
ax.tick_params(axis='x', rotation=45)
plt.tight_layout()
# 确保输出目录存在
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
# 保存图表
plt.savefig(output_path, dpi=100, bbox_inches='tight')
plt.close(fig)
return {
'success': True,
'chart_path': output_path,
'chart_type': 'bar',
'data_points': len(plot_data),
'x_column': x_column,
'y_column': y_column
}
except Exception as e:
return {'error': f'生成柱状图失败: {str(e)}'}
def is_applicable(self, data_profile: DataProfile) -> bool:
"""适用于所有数据。"""
return True
class CreateLineChartTool(AnalysisTool):
"""创建折线图工具。"""
@property
def name(self) -> str:
return "create_line_chart"
@property
def description(self) -> str:
return "创建折线图,用于展示时间序列数据或趋势变化。"
@property
def parameters(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"x_column": {
"type": "string",
"description": "X轴列名通常是时间"
},
"y_column": {
"type": "string",
"description": "Y轴列名数值变量"
},
"title": {
"type": "string",
"description": "图表标题",
"default": "折线图"
},
"output_path": {
"type": "string",
"description": "输出文件路径",
"default": "line_chart.png"
}
},
"required": ["x_column", "y_column"]
}
def execute(self, data: pd.DataFrame, **kwargs) -> Dict[str, Any]:
"""执行折线图生成。"""
x_column = kwargs.get('x_column')
y_column = kwargs.get('y_column')
title = kwargs.get('title', '折线图')
output_path = kwargs.get('output_path', 'line_chart.png')
if x_column not in data.columns:
return {'error': f'{x_column} 不存在'}
if y_column not in data.columns:
return {'error': f'{y_column} 不存在'}
try:
# 准备数据
plot_data = data[[x_column, y_column]].copy()
plot_data = plot_data.sort_values(x_column)
# 如果数据点太多,采样
if len(plot_data) > 1000:
step = len(plot_data) // 1000
plot_data = plot_data.iloc[::step]
# 创建图表
fig, ax = plt.subplots(figsize=(12, 6))
ax.plot(plot_data[x_column], plot_data[y_column], marker='o', markersize=3, linewidth=2)
ax.set_title(title, fontsize=14, fontweight='bold')
ax.set_xlabel(x_column, fontsize=12)
ax.set_ylabel(y_column, fontsize=12)
ax.grid(True, alpha=0.3)
plt.xticks(rotation=45)
plt.tight_layout()
# 确保输出目录存在
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
# 保存图表
plt.savefig(output_path, dpi=100, bbox_inches='tight')
plt.close(fig)
return {
'success': True,
'chart_path': output_path,
'chart_type': 'line',
'data_points': len(plot_data),
'x_column': x_column,
'y_column': y_column
}
except Exception as e:
return {'error': f'生成折线图失败: {str(e)}'}
def is_applicable(self, data_profile: DataProfile) -> bool:
"""适用于包含数值列的数据。"""
return any(col.dtype == 'numeric' for col in data_profile.columns)
class CreatePieChartTool(AnalysisTool):
"""创建饼图工具。"""
@property
def name(self) -> str:
return "create_pie_chart"
@property
def description(self) -> str:
return "创建饼图,用于展示各部分占整体的比例。"
@property
def parameters(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"column": {
"type": "string",
"description": "要分析的列名"
},
"title": {
"type": "string",
"description": "图表标题",
"default": "饼图"
},
"output_path": {
"type": "string",
"description": "输出文件路径",
"default": "pie_chart.png"
},
"top_n": {
"type": "integer",
"description": "只显示前N个类别其余归为'其他'",
"default": 10
}
},
"required": ["column"]
}
def execute(self, data: pd.DataFrame, **kwargs) -> Dict[str, Any]:
"""执行饼图生成。"""
column = kwargs.get('column')
title = kwargs.get('title', '饼图')
output_path = kwargs.get('output_path', 'pie_chart.png')
top_n = kwargs.get('top_n', 10)
if column not in data.columns:
return {'error': f'{column} 不存在'}
try:
# 准备数据
value_counts = data[column].value_counts()
if len(value_counts) > top_n:
# 只保留前 N 个,其余归为"其他"
top_values = value_counts.head(top_n)
other_sum = value_counts.iloc[top_n:].sum()
plot_data = pd.concat([top_values, pd.Series({'其他': other_sum})])
else:
plot_data = value_counts
# 创建图表
fig, ax = plt.subplots(figsize=(10, 8))
colors = plt.cm.Set3(range(len(plot_data)))
wedges, texts, autotexts = ax.pie(
plot_data,
labels=plot_data.index,
autopct='%1.1f%%',
colors=colors,
startangle=90
)
# 设置文本样式
for text in texts:
text.set_fontsize(10)
for autotext in autotexts:
autotext.set_color('white')
autotext.set_fontweight('bold')
autotext.set_fontsize(9)
ax.set_title(title, fontsize=14, fontweight='bold')
plt.tight_layout()
# 确保输出目录存在
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
# 保存图表
plt.savefig(output_path, dpi=100, bbox_inches='tight')
plt.close(fig)
return {
'success': True,
'chart_path': output_path,
'chart_type': 'pie',
'categories': len(plot_data),
'column': column
}
except Exception as e:
return {'error': f'生成饼图失败: {str(e)}'}
def is_applicable(self, data_profile: DataProfile) -> bool:
"""适用于所有数据。"""
return True
class CreateHeatmapTool(AnalysisTool):
"""创建热力图工具。"""
@property
def name(self) -> str:
return "create_heatmap"
@property
def description(self) -> str:
return "创建热力图,用于展示数值矩阵或相关性矩阵。"
@property
def parameters(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"columns": {
"type": "array",
"items": {"type": "string"},
"description": "要分析的列名列表,如果为空则使用所有数值列"
},
"title": {
"type": "string",
"description": "图表标题",
"default": "相关性热力图"
},
"output_path": {
"type": "string",
"description": "输出文件路径",
"default": "heatmap.png"
},
"method": {
"type": "string",
"description": "相关系数方法pearson, spearman, kendall",
"default": "pearson"
}
}
}
def execute(self, data: pd.DataFrame, **kwargs) -> Dict[str, Any]:
"""执行热力图生成。"""
columns = kwargs.get('columns', [])
title = kwargs.get('title', '相关性热力图')
output_path = kwargs.get('output_path', 'heatmap.png')
method = kwargs.get('method', 'pearson')
try:
# 如果没有指定列,使用所有数值列
if not columns:
numeric_cols = data.select_dtypes(include=[np.number]).columns.tolist()
else:
numeric_cols = [col for col in columns if col in data.columns]
if len(numeric_cols) < 2:
return {'error': '至少需要两个数值列来创建热力图'}
# 计算相关系数矩阵
corr_matrix = data[numeric_cols].corr(method=method)
# 创建图表
fig, ax = plt.subplots(figsize=(10, 8))
if HAS_SEABORN:
# 使用 seaborn 创建更美观的热力图
sns.heatmap(
corr_matrix,
annot=True,
fmt='.2f',
cmap='coolwarm',
center=0,
square=True,
linewidths=1,
cbar_kws={"shrink": 0.8},
ax=ax
)
else:
# 使用 matplotlib 创建基本热力图
im = ax.imshow(corr_matrix, cmap='coolwarm', aspect='auto', vmin=-1, vmax=1)
ax.set_xticks(range(len(corr_matrix.columns)))
ax.set_yticks(range(len(corr_matrix.columns)))
ax.set_xticklabels(corr_matrix.columns, rotation=45, ha='right')
ax.set_yticklabels(corr_matrix.columns)
# 添加数值标注
for i in range(len(corr_matrix.columns)):
for j in range(len(corr_matrix.columns)):
text = ax.text(j, i, f'{corr_matrix.iloc[i, j]:.2f}',
ha="center", va="center", color="black", fontsize=9)
plt.colorbar(im, ax=ax, shrink=0.8)
ax.set_title(title, fontsize=14, fontweight='bold')
plt.tight_layout()
# 确保输出目录存在
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
# 保存图表
plt.savefig(output_path, dpi=100, bbox_inches='tight')
plt.close(fig)
return {
'success': True,
'chart_path': output_path,
'chart_type': 'heatmap',
'columns': numeric_cols,
'method': method
}
except Exception as e:
return {'error': f'生成热力图失败: {str(e)}'}
def is_applicable(self, data_profile: DataProfile) -> bool:
"""适用于包含至少两个数值列的数据。"""
numeric_cols = [col for col in data_profile.columns if col.dtype == 'numeric']
return len(numeric_cols) >= 2