协作头脑风暴
brainstorming
在实现前澄清需求、探索设计选择,并形成清晰的开发方向。
systematic-debugging【开发与工程】系统化调试助手,帮助科研人员使用结构化方法定位和修复代码问题。支持4阶段调试流程、根因追踪技术、日志和断点策略、条件等待方法、防御式编程。当用户需要:(1) 调试科研代码错误、(2) 追踪数据分析问题、(3) 定位统计计算错误、(4) 解决实验代码bug、(5) 优化代码性能时触发。关键词:debugging、调试、bug修复、错误追踪、问题定位、troubleshooting。
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
帮助科研人员使用系统化、可重复的方法定位和修复代码问题,避免随机猜测和无效尝试。
目标:可靠地触发问题
## Bug报告模板
### 问题描述
简要描述问题是什么
### 预期行为
代码应该做什么
### 实际行为
代码实际做了什么
### 重现步骤
1. 运行 `python analysis.py`
2. 使用数据集 `data/experiment_001.csv`
3. 观察输出
### 环境信息
- Python版本: 3.11.0
- 操作系统: Ubuntu 22.04
- 关键依赖: numpy==1.24.0, pandas==2.0.0
### 错误消息
ValueError: could not convert string to float: 'NA' File "analysis.py", line 45, in process_data
### 数据样本
提供最小化的测试数据
# ❌ BAD - 完整复杂的代码
def analyze_experiment():
# 100+ 行代码
data = load_data()
preprocessed = preprocess(data)
results = complex_analysis(preprocessed)
# 错误在某处发生...
return results
# ✅ GOOD - 最小可重现示例
import pandas as pd
# 错误:无法将'NA'转换为浮点数
data = pd.DataFrame({
'value': ['1.0', '2.0', 'NA', '4.0']
})
# 这行会失败
numeric_data = data['value'].astype(float) # ValueError!
创建MRE的原则:
import numpy as np
import random
# 固定随机种子确保可重现
np.random.seed(42)
random.seed(42)
# 使用固定的测试数据
test_data = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
# 记录环境
print(f"NumPy版本: {np.__version__}")
print(f"测试数据: {test_data}")
重现检查清单:
目标:找到导致问题的具体代码行
def process_data(data):
"""使用打印语句进行二分查找"""
print("开始处理数据") # 检查点 1
# 步骤 1: 加载
loaded = load_data(data)
print(f"加载完成: {len(loaded)} 行") # 检查点 2
# 步骤 2: 清理
cleaned = clean_data(loaded)
print(f"清理完成: {len(cleaned)} 行") # 检查点 3
# 步骤 3: 转换
transformed = transform_data(cleaned)
print(f"转换完成: shape = {transformed.shape}") # 检查点 4
# 步骤 4: 分析
results = analyze(transformed)
print(f"分析完成: {results}") # 检查点 5
return results
# 运行并观察哪个检查点后出现问题
二分法原则:
import logging
# 配置日志
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('debug.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
def analyze_data(data):
"""使用日志记录详细信息"""
logger.debug(f"开始分析, 数据形状: {data.shape}")
try:
# 记录输入
logger.debug(f"输入数据样本:\n{data.head()}")
# 计算
mean_value = data['value'].mean()
logger.info(f"计算均值: {mean_value}")
# 检查异常值
std_value = data['value'].std()
outliers = data[abs(data['value'] - mean_value) > 3 * std_value]
if len(outliers) > 0:
logger.warning(f"发现 {len(outliers)} 个异常值")
return mean_value
except Exception as e:
logger.error(f"分析失败: {str(e)}", exc_info=True)
raise
日志级别使用:
DEBUG: 详细的诊断信息INFO: 正常的操作信息WARNING: 警告但不影响运行ERROR: 错误导致功能失败CRITICAL: 严重错误导致程序崩溃# 使用Python调试器 (pdb)
import pdb
def problematic_function(data):
# 在此处设置断点
pdb.set_trace()
# 调试时可用命令:
# n (next) - 执行下一行
# s (step) - 进入函数
# c (continue) - 继续执行
# p variable - 打印变量
# l - 显示当前代码
# q - 退出调试器
result = process(data)
return result
Jupyter Notebook 调试:
# 在单元格中使用
%debug # 在异常后进入调试器
# 或使用 IPython 调试器
from IPython.core.debugger import set_trace
set_trace() # 设置断点
VS Code / PyCharm 断点:
import pandas as pd
import numpy as np
def inspect_data(data, name="data"):
"""全面检查数据状态"""
print(f"\n{'='*60}")
print(f"检查 {name}")
print(f"{'='*60}")
# 基本信息
print(f"类型: {type(data)}")
print(f"形状: {data.shape if hasattr(data, 'shape') else 'N/A'}")
# DataFrame 特定检查
if isinstance(data, pd.DataFrame):
print(f"\n列: {list(data.columns)}")
print(f"\n数据类型:\n{data.dtypes}")
print(f"\n缺失值:\n{data.isnull().sum()}")
print(f"\n描述性统计:\n{data.describe()}")
print(f"\n前5行:\n{data.head()}")
# NumPy 数组检查
elif isinstance(data, np.ndarray):
print(f"数据类型: {data.dtype}")
print(f"最小值: {data.min()}")
print(f"最大值: {data.max()}")
print(f"均值: {data.mean()}")
print(f"NaN数量: {np.isnan(data).sum()}")
print(f"Inf数量: {np.isinf(data).sum()}")
# 列表或其他
else:
print(f"长度: {len(data) if hasattr(data, '__len__') else 'N/A'}")
print(f"样本: {data[:5] if hasattr(data, '__getitem__') else data}")
# 使用示例
data = pd.read_csv('data.csv')
inspect_data(data, "原始数据")
processed = preprocess(data)
inspect_data(processed, "处理后数据")
目标:理解为什么会出现问题
## 根因分析模板
### 问题症状
ValueError: could not convert string to float: 'NA'
### 假设 1: 数据包含非数值字符串
**证据需求**: 检查数据中的唯一值
**验证代码**:
```python
print(data['value'].unique())
# 输出: ['1.0', '2.0', 'NA', '4.0']
结论: ✅ 确认 - 数据中确实有 'NA' 字符串
证据需求: 检查 read_csv 的 na_values 参数 验证代码:
# 当前代码
data = pd.read_csv('data.csv')
# 正确代码
data = pd.read_csv('data.csv', na_values=['NA', 'N/A', 'null'])
结论: ✅ 确认 - 未指定 na_values 参数
数据加载时未将 'NA' 识别为缺失值,导致被当作字符串处理, 后续类型转换时失败。
#### 技术 3.2:差异分析
```python
# 比较工作版本和问题版本
def compare_versions(working_data, broken_data):
"""比较两个版本的差异"""
print("差异分析:")
# 形状差异
if working_data.shape != broken_data.shape:
print(f"形状不同: {working_data.shape} vs {broken_data.shape}")
# 数据类型差异
if not working_data.dtypes.equals(broken_data.dtypes):
print("\n数据类型差异:")
for col in working_data.columns:
if working_data[col].dtype != broken_data[col].dtype:
print(f" {col}: {working_data[col].dtype} -> {broken_data[col].dtype}")
# 值差异
differences = working_data != broken_data
if differences.any().any():
print(f"\n发现 {differences.sum().sum()} 个不同的值")
print("\n不同值的位置:")
print(differences[differences.any(axis=1)])
import traceback
try:
result = problematic_function()
except Exception as e:
# 打印完整的调用栈
print("完整错误追溯:")
traceback.print_exc()
# 获取追溯对象
tb = traceback.extract_tb(e.__traceback__)
# 分析每一层
print("\n调用栈分析:")
for frame in tb:
print(f"\n文件: {frame.filename}")
print(f"函数: {frame.name}")
print(f"行号: {frame.lineno}")
print(f"代码: {frame.line}")
## 五个为什么分析
### 问题: 统计分析结果不正确
**为什么1**: 为什么结果不正确?
→ 因为p值异常小 (p < 1e-50)
**为什么2**: 为什么p值异常小?
→ 因为样本量被错误计算为100000而不是100
**为什么3**: 为什么样本量计算错误?
→ 因为使用了 len(data) 而不是 len(data.dropna())
**为什么4**: 为什么使用了错误的长度?
→ 因为没有考虑缺失值的影响
**为什么5**: 为什么没有考虑缺失值?
→ 因为没有在数据加载后立即检查数据质量
### 根本原因
缺乏数据质量检查步骤
### 预防措施
在分析前添加数据验证函数
目标:修复问题并确保不再发生
# ❌ BAD - 大规模重写
def analyze_data(data):
# 完全重写了整个函数
# 引入了新的潜在问题
pass
# ✅ GOOD - 最小化修改
def analyze_data(data):
# 原有代码保持不变
# ...
# 只修复具体问题
# 之前: numeric_data = data['value'].astype(float)
# 之后:
numeric_data = pd.to_numeric(data['value'], errors='coerce')
# 原有代码继续
# ...
import pytest
import pandas as pd
def test_handle_missing_values():
"""测试缺失值处理"""
# 创建包含缺失值的测试数据
test_data = pd.DataFrame({
'value': ['1.0', '2.0', 'NA', '4.0', 'N/A']
})
# 加载并处理
result = process_data(test_data)
# 验证
assert result['value'].dtype == 'float64'
assert result['value'].isna().sum() == 2 # 应该有2个NaN
def test_edge_case_empty_data():
"""测试空数据的边界情况"""
test_data = pd.DataFrame({'value': []})
result = process_data(test_data)
assert len(result) == 0
def test_edge_case_all_na():
"""测试全为缺失值的情况"""
test_data = pd.DataFrame({'value': ['NA', 'NA', 'NA']})
result = process_data(test_data)
assert result['value'].isna().all()
# 运行所有测试确保修复没有破坏其他功能
pytest tests/ -v
# 检查测试覆盖率
pytest tests/ --cov=src --cov-report=html
# 查看覆盖率报告
# open htmlcov/index.html
# 添加输入验证
def analyze_data(data):
"""分析数据(带防御性检查)"""
# 验证输入
if not isinstance(data, pd.DataFrame):
raise TypeError(f"期望 DataFrame, 得到 {type(data)}")
if 'value' not in data.columns:
raise ValueError("数据必须包含 'value' 列")
if len(data) == 0:
raise ValueError("数据不能为空")
# 处理缺失值
numeric_data = pd.to_numeric(data['value'], errors='coerce')
# 检查转换后的数据
if numeric_data.isna().all():
raise ValueError("所有值都无法转换为数字")
# 继续分析
result = numeric_data.mean()
# 验证输出
if not isinstance(result, (int, float)):
raise TypeError(f"结果应为数值, 得到 {type(result)}")
return result
## 修复记录
### Bug ID: #123
**标题**: ValueError when processing data with 'NA' strings
### 根本原因
数据加载时未将 'NA' 识别为缺失值,导致类型转换失败
### 修复方案
在 `pd.read_csv()` 中添加 `na_values=['NA', 'N/A']` 参数
### 代码变更
```diff
- data = pd.read_csv('data.csv')
+ data = pd.read_csv('data.csv', na_values=['NA', 'N/A', 'null'])
test_handle_missing_values
---
## 常见问题类型和调试策略
### 类型 1: 数据相关问题
#### 问题:数据格式错误
```python
# 症状:类型转换失败
# TypeError: unsupported operand type(s) for +: 'int' and 'str'
# 诊断工具
def diagnose_data_types(df):
"""诊断数据类型问题"""
print("数据类型检查:")
print(df.dtypes)
print("\n每列的唯一类型:")
for col in df.columns:
unique_types = df[col].apply(type).unique()
print(f"{col}: {unique_types}")
print("\n可能的问题值:")
for col in df.columns:
if df[col].dtype == 'object':
# 尝试转换为数值
try:
pd.to_numeric(df[col])
except:
problem_values = df[col][pd.to_numeric(df[col], errors='coerce').isna()]
if len(problem_values) > 0:
print(f"{col}: {problem_values.unique()[:5]}")
# 症状:计算结果为 NaN
# result = data.mean() # returns NaN
# 诊断
print(f"缺失值数量: {data.isna().sum()}")
print(f"缺失值比例: {data.isna().mean():.2%}")
# 修复选项
# 1. 删除缺失值
clean_data = data.dropna()
# 2. 填充缺失值
filled_data = data.fillna(data.mean())
# 3. 插值
interpolated_data = data.interpolate()
# ❌ BAD - 索引错误
for i in range(len(data)):
if data[i] > threshold:
data[i+1] = 0 # IndexError when i = len(data)-1!
# ✅ GOOD - 正确的边界检查
for i in range(len(data) - 1): # 注意范围
if data[i] > threshold:
data[i+1] = 0
# ❌ BAD - 逻辑错误
if age >= 18 and age <= 65:
category = 'adult'
elif age >= 65:
category = 'senior'
else:
category = 'minor'
# 问题:65岁既满足第一个也满足第二个条件
# ✅ GOOD - 互斥条件
if age < 18:
category = 'minor'
elif age < 65:
category = 'adult'
else:
category = 'senior'
# 症状:相等比较失败
a = 0.1 + 0.2
print(a == 0.3) # False!
print(a) # 0.30000000000000004
# 修复:使用容差比较
import math
print(math.isclose(a, 0.3)) # True
# 或使用 numpy
import numpy as np
print(np.isclose(a, 0.3)) # True
# Python 2 风格的整数除法
result = 5 / 2 # Python 3: 2.5, Python 2: 2
# 确保浮点除法
result = 5 / 2.0
# 或
result = 5 / float(2)
# 或
from __future__ import division # Python 2
import time
# 性能分析
start = time.time()
result = slow_function(data)
end = time.time()
print(f"执行时间: {end - start:.2f} 秒")
# 使用 cProfile
import cProfile
cProfile.run('slow_function(data)')
# 使用 line_profiler
# pip install line_profiler
# kernprof -l -v script.py
@profile
def slow_function(data):
# 代码
pass
# 1. pdb - Python调试器
python -m pdb script.py
# 2. ipdb - IPython调试器
pip install ipdb
ipdb.set_trace()
# 3. pudb - 全屏调试器
pip install pudb
pudb.set_trace()
# 4. 性能分析
python -m cProfile -o output.prof script.py
python -m pstats output.prof
# 5. 内存分析
pip install memory_profiler
python -m memory_profiler script.py
# 魔法命令
%pdb # 自动进入调试器
%debug # 在异常后进入调试器
%time statement # 计时单个语句
%timeit statement # 多次运行并计时
%lprun -f func func(args) # 行级性能分析
%memit statement # 内存使用
# 1. 基本调试
debug(function_name) # 启用调试
undebug(function_name) # 禁用调试
# 2. 浏览器调试
browser() # 在代码中设置断点
# 3. 追踪
traceback() # 显示调用栈
# 4. 条件断点
if (x > 100) browser()
# 5. 性能分析
Rprof("output.out")
# ... 运行代码 ...
Rprof(NULL)
summaryRprof("output.out")