feat(dc/tool-c): Add pivot column ordering and NA handling features

Major features:
1. Pivot transformation enhancements:
   - Add option to keep unselected columns with 3 aggregation methods
   - Maintain original column order after pivot (aligned with source file)
   - Preserve pivot value order (first appearance order)

2. NA handling across 4 core functions:
   - Recode: Support keep/map/drop for NA values
   - Filter: Already supports is_null/not_null operators
   - Binning: Support keep/label/assign for NA values (fix nan display)
   - Conditional: Add is_null/not_null operators

3. UI improvements:
   - Enable column header tooltips with custom header component
   - Add closeable alert for 50-row preview
   - Fix page scrollbar issues

Modified files:
Python: pivot.py, recode.py, binning.py, conditional.py, main.py
Backend: SessionController, QuickActionController, QuickActionService
Frontend: PivotDialog, RecodeDialog, BinningDialog, ConditionalDialog, DataGrid, index

Status: Ready for testing
This commit is contained in:
2025-12-09 14:40:14 +08:00
parent 75ceeb0653
commit f4f1d09837
19 changed files with 2314 additions and 123 deletions

View File

@@ -0,0 +1,339 @@
# 工具C - NA处理功能开发总结
## 📋 概述
**目标**在4个核心功能中添加对NA空值/缺失值)的显式处理,让用户能够明确看到并处理缺失值。
**NA显示名称**`空值/NA`(中英文结合)
---
## ✅ 已完成Python后端100%
### 1. recode.py - 数值映射 ✅
**新增参数**
- `na_handling`: 'keep' | 'map' | 'drop'
- `keep`: 保持为NA默认
- `map`: 映射为指定值
- `drop`: 删除包含NA的行
- `na_value`: NA映射值当na_handling='map'时使用)
**实现逻辑**
```python
if original_na_count > 0:
na_mask = result[column].isna()
if na_handling == 'keep':
# 保持为NA已经是NA无需操作
print(f'📊 NA处理保持为NA{original_na_count}个)')
elif na_handling == 'map':
# 映射为指定值
result.loc[na_mask, target_column] = na_value
print(f'📊 NA处理映射为 {na_value}{original_na_count}个)')
elif na_handling == 'drop':
# 删除包含NA的行
result = result[~na_mask].copy()
```
### 2. filter.py - 高级筛选 ✅
**已支持**`is_null``not_null` 运算符
无需修改,原有代码已经支持!
```python
elif operator == 'is_null':
mask = df[column].isna()
elif operator == 'not_null':
mask = df[column].notna()
```
### 3. binning.py - 生成分类变量 ✅
**新增参数**
- `na_handling`: 'keep' | 'label' | 'assign'
- `keep`: 保持为NA默认
- `label`: 标记为指定标签(如"缺失"
- `assign`: 分配到指定组
- `na_label`: NA标签当na_handling='label'时使用)
- `na_assign_to`: NA分配到的组索引当na_handling='assign'时使用)
**实现逻辑**
```python
if original_na_count > 0:
na_mask = result[column].isna()
if na_handling == 'keep':
# 保持为NA
print(f'📊 NA处理保持为NA{original_na_count}个)')
elif na_handling == 'label':
# 标记为指定标签
label_to_use = na_label if na_label else '空值/NA'
result.loc[na_mask, new_column_name] = label_to_use
print(f'📊 NA处理标记为 "{label_to_use}"{original_na_count}个)')
elif na_handling == 'assign':
# 分配到指定组
if labels and na_assign_to is not None:
result.loc[na_mask, new_column_name] = labels[na_assign_to]
```
### 4. conditional.py - 条件生成列 ✅
**新增支持**`is_null``not_null` 运算符
```python
elif operator == 'is_null': # ✨ 新增:为空
mask = result[column].isna()
elif operator == 'not_null': # ✨ 新增:不为空
mask = result[column].notna()
```
### 5. main.py - API请求模型 ✅
**RecodeRequest**
```python
na_handling: str = 'keep'
na_value: Any = None
```
**BinningRequest**
```python
na_handling: str = 'keep'
na_label: str = None
na_assign_to: int = None
```
**FilterRequest 和 ConditionalRequest**
无需修改,已支持
---
## 🔄 待完成Node.js后端
### QuickActionService.ts
**需要更新的接口**
1. **RecodeParams**
```typescript
interface RecodeParams {
column: string;
mapping: Record<string, any>;
createNewColumn?: boolean;
newColumnName?: string;
naHandling?: 'keep' | 'map' | 'drop'; // ✨ 新增
naValue?: any; // ✨ 新增
}
```
2. **BinningParams**
```typescript
interface BinningParams {
column: string;
method: 'custom' | 'equal_width' | 'equal_freq';
newColumnName: string;
bins?: number[];
labels?: string[];
numBins?: number;
naHandling?: 'keep' | 'label' | 'assign'; // ✨ 新增
naLabel?: string; // ✨ 新增
naAssignTo?: number; // ✨ 新增
}
```
**API调用**(自动传递所有参数,无需特殊处理)
---
## 🎨 待完成前端UI
### 1. RecodeDialog.tsx - 数值映射
**UI设计**
```
┌─────────────────────────────────────┐
│ 数值映射 [X] │
├─────────────────────────────────────┤
│ 选择列:[婚姻状况▼] │
│ │
│ 唯一值映射: │
│ ┌──────────────────────────────┐ │
│ │ 原始值 → 新值 │ │
│ │ 已婚 → [1 ] │ │
│ │ 未婚 → [0 ] │ │
│ │ 空值/NA → [▼] │ ⭐│
│ │ ├─ 保持为NA默认 │ │
│ │ ├─ 映射为:[____] │ │
│ │ └─ 删除该行 │ │
│ └──────────────────────────────┘ │
│ │
当前有125个空值15.6%
└─────────────────────────────────────┘
```
**实现要点**
1. 调用`/api/v1/dc/tool-c/sessions/:id/unique-values`检测是否有NA
2. 如果有NA显示"空值/NA"特殊行
3. 提供3种选择保持NA / 映射为指定值 / 删除行
### 2. FilterDialog.tsx - 高级筛选
**UI设计**
```
条件:
[婚姻状况▼] [运算符▼]
• 等于
• 不等于
• 为空 ← ✨ 新增
• 不为空 ← ✨ 新增
• ...
```
**实现要点**
1. 在运算符下拉菜单中添加"为空"和"不为空"选项
2. 当选择这两个运算符时,隐藏"值"输入框(不需要输入值)
### 3. BinningDialog.tsx - 生成分类变量
**UI设计**
```
┌─────────────────────────────────────┐
│ 生成分类变量 [X] │
├─────────────────────────────────────┤
│ 原始列:[年龄▼] │
│ ...分组规则... │
│ │
│ ⚠️ 空值处理: │ ⭐
│ ⚪ 保持为空(默认) │
│ ⚪ 标记为:[缺失___] │
│ ⚪ 分配到组:[第1组▼] │
│ │
当前有25个空值3.1%
└─────────────────────────────────────┘
```
**实现要点**
1. 添加Radio Group for NA处理方式
2. 根据选择显示相应的输入框
3. 传递`naHandling``naLabel``naAssignTo`参数
### 4. ConditionalDialog.tsx - 条件生成列
**UI设计**
```
规则1
如果 [婚姻状况▼] [运算符▼]
• 等于
• 不等于
• 为空 ← ✨ 新增
• 不为空 ← ✨ 新增
• ...
则填充:[低风险 ]
```
**实现要点**
1. 与FilterDialog类似在运算符下拉菜单中添加"为空"和"不为空"
2. 这两个运算符不需要输入值
---
## 🧪 测试用例
### 测试数据准备
```csv
ID,婚姻状况,年龄,收缩压
1,已婚,45,120
2,未婚,35,130
3,,50, # ← NA
4,离异,60,
5,,NA,140
```
### 测试场景
| 编号 | 功能 | 测试场景 | 预期结果 |
|------|------|----------|----------|
| TC-1 | 数值映射 - 保持NA | 婚姻状况:已婚=1未婚=0NA=保持 | NA行的新列为NA ✅ |
| TC-2 | 数值映射 - 映射NA | 婚姻状况:已婚=1未婚=0NA=映射为9 | NA行的新列为9 ✅ |
| TC-3 | 数值映射 - 删除NA | 婚姻状况:已婚=1未婚=0NA=删除 | NA行被删除总行数减少 ✅ |
| TC-4 | 高级筛选 - 为空 | 筛选"婚姻状况"为空 | 只保留NA行 ✅ |
| TC-5 | 高级筛选 - 不为空 | 筛选"婚姻状况"不为空 | 只保留非NA行 ✅ |
| TC-6 | 生成分类变量 - 保持NA | 年龄分组NA保持 | NA行的新列为NA ✅ |
| TC-7 | 生成分类变量 - 标记NA | 年龄分组NA标记为"缺失" | NA行的新列为"缺失" ✅ |
| TC-8 | 生成分类变量 - 分配NA | 年龄分组NA分配到第1组 | NA行的新列为第1组标签 ✅ |
| TC-9 | 条件生成列 - 为空 | 如果婚姻状况为空,则"未知" | NA行的新列为"未知" ✅ |
| TC-10 | 条件生成列 - 不为空 | 如果婚姻状况不为空,则"已知" | 非NA行的新列为"已知" ✅ |
---
## 📊 开发进度
| 阶段 | 状态 | 备注 |
|------|------|------|
| Python后端 - recode.py | ✅ 100% | 已完成 |
| Python后端 - filter.py | ✅ 100% | 已支持(无需修改) |
| Python后端 - binning.py | ✅ 100% | 已完成 |
| Python后端 - conditional.py | ✅ 100% | 已完成 |
| Python后端 - main.py | ✅ 100% | 已完成 |
| Node.js后端 | ✅ 100% | 已完成(参数传递) |
| 前端 - RecodeDialog | ✅ 100% | 已完成NA处理下拉菜单 |
| 前端 - FilterDialog | ✅ 100% | 已完成已支持is_null/not_null |
| 前端 - BinningDialog | ✅ 100% | 已完成NA处理Radio Group |
| 前端 - ConditionalDialog | ✅ 100% | 已完成添加is_null/not_null |
| 测试 | ⏳ 待测试 | 等待用户测试验证 |
---
## 🎯 下一步行动
1. **Node.js后端**预计15分钟
- 更新RecodeParams接口
- 更新BinningParams接口
- FilterParams和ConditionalParams无需修改
2. **前端UI**预计2小时
- RecodeDialog添加NA处理下拉菜单45分钟
- FilterDialog添加"为空"/"不为空"运算符15分钟
- BinningDialog添加NA处理Radio Group30分钟
- ConditionalDialog添加"为空"/"不为空"运算符30分钟
3. **测试**预计30分钟
- 执行10个测试用例
- 修复发现的问题
**总计剩余时间约3小时**
---
## 📝 技术要点
### Python端
- 使用`df[column].isna()`检测NA
- 使用`df.loc[mask, col] = value`填充NA
- 使用`df[~mask]`删除NA行
- 统计并打印NA处理信息
### 前端端
- 在获取unique values时检测NA
- 使用`<空值/NA>`作为显示名称
- 根据用户选择构造请求参数
- 显示NA统计信息如"当前有125个空值"
### 验收标准
- ✅ 用户能明确看到NA的存在
- ✅ 用户能选择如何处理NA
- ✅ 处理后的结果符合用户选择
- ✅ 所有功能的NA处理方式清晰统一
---
**文档创建时间**2025-12-09
**Python后端开发状态**:✅ 已完成
**剩余工作**Node.js后端 + 前端UI + 测试

View File

@@ -0,0 +1,190 @@
# 工具C - Pivot列顺序优化总结
## 📋 问题描述
**用户需求**:长宽转换后,列的排序应该与上传文件时的列顺序保持一致。
**当前问题**:系统按字母顺序排列转换后的列,导致顺序与原文件不一致。
---
## 🎯 解决方案方案A - Python端排序
### 核心思路
1. Node.js后端从session获取**原始列顺序**
2. Node.js后端从数据中提取**透视列值的原始顺序**(按首次出现顺序)
3. 传递给Python
4. Python在pivot后按原始顺序重排列
---
## 🛠️ 实现细节
### 1. Python端pivot.py
**新增参数**
- `original_column_order: List[str]`:原始列顺序(如`['Record ID', 'Event Name', 'FMA', '体重', '收缩压', ...]`
- `pivot_value_order: List[str]`:透视列值的原始顺序(如`['基线', '1个月', '2个月', ...]`
**排序逻辑**
```python
if original_column_order:
# 1. 索引列始终在最前面
final_cols = [index_column]
# 2. 按原始列顺序添加转换后的列
for orig_col in original_column_order:
if orig_col in value_columns:
# 找出所有属于这个原列的新列
related_cols = [c for c in df_pivot.columns if c.startswith(f'{orig_col}___')]
# ✨ 按透视列的原始顺序排序
if pivot_value_order:
pivot_order_map = {val: idx for idx, val in enumerate(pivot_value_order)}
related_cols_sorted = sorted(
related_cols,
key=lambda c: pivot_order_map.get(c.split('___')[1], 999)
)
else:
related_cols_sorted = sorted(related_cols)
final_cols.extend(related_cols_sorted)
# 3. 添加未选择的列(保持原始顺序)
if keep_unused_columns:
for orig_col in original_column_order:
if orig_col in df_pivot.columns and orig_col not in final_cols:
final_cols.append(orig_col)
# 4. 重排列
df_pivot = df_pivot[final_cols]
```
### 2. Python端main.py
**PivotRequest模型**
```python
class PivotRequest(BaseModel):
# ... 原有字段 ...
original_column_order: List[str] = [] # ✨ 新增
pivot_value_order: List[str] = [] # ✨ 新增
```
**调用pivot_long_to_wide**
```python
result_df = pivot_long_to_wide(
df,
request.index_column,
request.pivot_column,
request.value_columns,
request.aggfunc,
request.column_mapping,
request.keep_unused_columns,
request.unused_agg_method,
request.original_column_order, # ✨ 新增
request.pivot_value_order # ✨ 新增
)
```
### 3. Node.js后端QuickActionController.ts
**获取原始列顺序**
```typescript
const originalColumnOrder = session.columns || [];
```
**获取透视列值的原始顺序**
```typescript
const pivotColumn = params.pivotColumn;
const seenPivotValues = new Set();
const pivotValueOrder: string[] = [];
for (const row of fullData) {
const pivotValue = row[pivotColumn];
if (pivotValue !== null && pivotValue !== undefined && !seenPivotValues.has(pivotValue)) {
seenPivotValues.add(pivotValue);
pivotValueOrder.push(String(pivotValue));
}
}
```
**传递给QuickActionService**
```typescript
executeResult = await quickActionService.executePivot(
fullData,
params,
session.columnMapping,
originalColumnOrder, // ✨ 新增
pivotValueOrder // ✨ 新增
);
```
### 4. Node.js后端QuickActionService.ts
**方法签名**
```typescript
async executePivot(
data: any[],
params: PivotParams,
columnMapping?: any[],
originalColumnOrder?: string[], // ✨ 新增
pivotValueOrder?: string[] // ✨ 新增
): Promise<OperationResult>
```
**传递给Python**
```typescript
const response = await axios.post(`${PYTHON_SERVICE_URL}/api/operations/pivot`, {
// ... 原有参数 ...
original_column_order: originalColumnOrder || [], // ✨ 新增
pivot_value_order: pivotValueOrder || [], // ✨ 新增
});
```
---
## 📊 效果对比
### 修改前(按字母顺序)
```
Record ID | FMA___基线 | FMA___1个月 | 收缩压___基线 | 收缩压___1个月 | 体重___基线 | 体重___1个月
↑ ↑ ↑ ↑ ↑ ↑ ↑
索引列 F开头 F开头 S开头(拼音) S开头 T开头 T开头
```
### 修改后(按原始顺序)
```
Record ID | FMA___基线 | FMA___1个月 | 体重___基线 | 体重___1个月 | 收缩压___基线 | 收缩压___1个月
↑ ↑ ↑ ↑ ↑ ↑ ↑
索引列 原文件第3列 原文件第3列 原文件第4列 原文件第4列 原文件第5列 原文件第5列
```
### 透视值内部顺序(按原始出现顺序)
```
FMA___基线 | FMA___1个月 | FMA___2个月
↑ ↑ ↑
首次出现 第二次出现 第三次出现
(而不是按"1个月"、"2个月"、"基线"的字母顺序)
```
---
## ✅ 开发完成
### 修改文件清单
1.`extraction_service/operations/pivot.py`
2.`extraction_service/main.py`
3.`backend/src/modules/dc/tool-c/controllers/QuickActionController.ts`
4.`backend/src/modules/dc/tool-c/services/QuickActionService.ts`
### 优势
- ✅ 列顺序与原文件一致(用户熟悉)
- ✅ 透视值顺序按时间顺序基线→1个月→2个月
- ✅ 未选择的列也保持原始顺序
- ✅ 导出Excel时顺序正确
---
**开发时间**2025-12-09
**状态**:✅ 已完成,等待测试

View File

@@ -0,0 +1,348 @@
# 工具C - 方案B实施总结列名特殊字符解决方案
> **日期**: 2025-12-09
> **版本**: v1.0
> **实施方案**: 方案B - Python负责列名替换
---
## 📋 问题背景
### 原始问题
用户上传的Excel文件表头包含特殊字符导致计算列功能失败
**示例表头**:
- `体重kg`
- `1.高血压病(无=0有=1不知道=2`
- `身高cm`
**报错信息**:
```
计算列失败:公式验证失败: 公式包含不允许的字符
```
---
## 🎯 方案选择
### 方案对比
| 方案 | 描述 | 优点 | 缺点 | 评分 |
|------|------|------|------|------|
| **方案A** | 用户使用序号引用col_0, col_1 | 技术最安全 | 用户体验差,不直观 | ⭐⭐ |
| **方案B** | 用户使用原列名Python负责替换 | 用户体验好,技术可靠 | 需实现替换逻辑 | ⭐⭐⭐⭐⭐ |
| **方案C** | 前端替换列名 | 减少网络传输 | 边界识别困难,不可靠 | ⭐⭐⭐ |
**最终选择**: **方案B**
---
## 🏗️ 架构设计
### 数据流
```
用户输入公式(原列名)
前端体重kg / (身高cm/100)**2
后端:获取 columnMapping
传递给Python: {
formula: "体重kg / (身高cm/100)**2",
column_mapping: [
{"originalName": "体重kg", "safeName": "col_0"},
{"originalName": "身高cm", "safeName": "col_1"}
]
}
Python替换: col_0 / (col_1/100)**2
执行计算 ✅
```
### 职责划分
| 层级 | 职责 | 关键点 |
|------|------|--------|
| **前端** | UI交互、数据收集 | 用户看到和输入原列名 |
| **后端** | 获取columnMapping、传递给Python | 从Session获取映射 |
| **Python** | 列名替换、公式执行 | 按长度排序、精确替换 |
---
## 💻 实施细节
### 1. 前端ComputeDialog.tsx
**保持不变** - 已经使用原列名方式
```typescript
// 用户点击列名标签,插入到公式框
<Tag onClick={() => setFormula(formula + col.name)}>
{col.name} {/* 显示原列名体重kg */}
</Tag>
// 提交时直接传递原公式
onApply({
newColumnName: "BMI",
formula: "体重kg / (身高cm/100)**2", // 原列名
});
```
### 2. 后端QuickActionController.ts
**修改**: 获取session并传递columnMapping
```typescript
// 获取session包含columnMapping
session = await sessionService.getSession(sessionId);
// 传递给QuickActionService
executeResult = await quickActionService.executeCompute(
fullData,
params,
session.columnMapping // ✅ 传递映射
);
```
### 3. 后端QuickActionService.ts
**修改**: 接收并传递columnMapping给Python
```typescript
async executeCompute(
data: any[],
params: ComputeParams,
columnMapping?: any[] // ✅ 新增参数
): Promise<OperationResult> {
const response = await axios.post(`${PYTHON_SERVICE_URL}/api/operations/compute`, {
data,
new_column_name: params.newColumnName,
formula: params.formula,
column_mapping: columnMapping || [], // ✅ 传递映射
});
return response.data;
}
```
### 4. Pythonmain.py
**修改**: 更新请求模型
```python
class ComputeRequest(BaseModel):
data: List[Dict[str, Any]]
new_column_name: str
formula: str
column_mapping: List[Dict[str, str]] = [] # ✅ 新增字段
@app.post("/api/operations/compute")
async def operation_compute(request: ComputeRequest):
result_df = compute_column(
df,
request.new_column_name,
request.formula,
request.column_mapping # ✅ 传递映射
)
```
### 5. Pythoncompute.py
**核心实现**: 列名替换逻辑
```python
def replace_column_names_in_formula(
formula: str,
column_mapping: List[Dict[str, str]]
) -> str:
"""
✅ 核心算法:可靠的列名替换
"""
safe_formula = formula
# 关键1按列名长度倒序排序
# 避免子串问题:先替换"高血压病史",再替换"高血压"
sorted_mapping = sorted(
column_mapping,
key=lambda x: len(x['originalName']),
reverse=True
)
# 关键2逐个精确替换不使用正则
for item in sorted_mapping:
original = item['originalName']
safe = item['safeName']
if original in safe_formula:
safe_formula = safe_formula.replace(original, safe)
return safe_formula
def compute_column(
df: pd.DataFrame,
new_column_name: str,
formula: str,
column_mapping: Optional[List[Dict[str, str]]] = None
) -> pd.DataFrame:
"""
✅ 方案BPython负责替换
"""
# 1. 替换列名
if column_mapping:
safe_formula = replace_column_names_in_formula(formula, column_mapping)
else:
safe_formula = formula
# 2. 准备执行环境
env = {}
for item in column_mapping:
env[item['safeName']] = df[item['originalName']]
env.update(ALLOWED_FUNCTIONS)
# 3. 执行(不需要字符验证!)
result = eval(safe_formula, {"__builtins__": {}}, env)
return df.assign(**{new_column_name: result})
```
---
## ✅ 解决的问题
### 1. 特殊字符问题 ✅
- **问题**: `体重kg` 包含中文括号
- **解决**: Python使用安全列名 `col_0`,不受特殊字符影响
### 2. 子串包含问题 ✅
- **问题**: "高血压" 和 "高血压病史" 可能误替换
- **解决**: 按长度倒序排序,先替换长列名
### 3. 边界识别问题 ✅
- **问题**: 正则`\b`对中文字符不可靠
- **解决**: 使用Python字符串`replace`,简单可靠
### 4. 字符白名单问题 ✅
- **问题**: 需要枚举所有允许的字符
- **解决**: 不需要验证Python只处理安全列名
---
## 🧪 测试用例
### 测试1基本功能
```python
column_mapping = [
{"originalName": "体重kg", "safeName": "col_0"},
{"originalName": "身高cm", "safeName": "col_1"}
]
formula = "体重kg / (身高cm/100)**2"
# 预期: col_0 / (col_1/100)**2 ✅
```
### 测试2子串包含
```python
column_mapping = [
{"originalName": "高血压", "safeName": "col_0"},
{"originalName": "高血压病史", "safeName": "col_1"}
]
formula = "高血压病史 + 高血压"
# 预期: col_1 + col_0 ✅(因为按长度排序)
```
### 测试3复杂特殊字符
```python
column_mapping = [
{"originalName": "1.高血压病(无=0有=1不知道=2", "safeName": "col_0"}
]
formula = "1.高血压病(无=0有=1不知道=2 * 2"
# 预期: col_0 * 2 ✅
```
### 测试4嵌套括号
```python
column_mapping = [
{"originalName": "FMA总分0-100", "safeName": "col_0"}
]
formula = "FMA总分0-100 / 100"
# 预期: col_0 / 100 ✅
```
---
## 📊 性能影响
| 指标 | 影响 | 说明 |
|------|------|------|
| **网络传输** | +5KB | columnMapping约5KB100列 |
| **替换时间** | <1ms | 字符串替换非常快 |
| **总体性能** | 可忽略 | 相比数据处理时间(秒级)可忽略 |
---
## 🎯 优势总结
### 用户体验 ⭐⭐⭐⭐⭐
- ✅ 用户看到和输入原列名
- ✅ 公式直观易懂
- ✅ 历史记录清晰
### 技术可靠性 ⭐⭐⭐⭐⭐
- ✅ 不依赖正则边界识别
- ✅ 按长度排序避免子串问题
- ✅ Python字符串操作简单可靠
### 可维护性 ⭐⭐⭐⭐⭐
- ✅ 职责清晰前端UI、Python逻辑
- ✅ 易于调试(可打印替换日志)
- ✅ 未来不会再有字符问题
---
## 📝 后续工作
### 已完成 ✅
- [x] 前端保持使用原列名
- [x] 后端传递columnMapping
- [x] Python实现替换逻辑
- [x] 移除字符验证
- [x] 更新Pivot操作
### 待测试 ⏳
- [ ] 用户实际测试
- [ ] 边界情况验证
- [ ] 性能测试
### 未来优化 💡
- [ ] 添加公式语法高亮
- [ ] 列名自动补全
- [ ] 公式错误提示优化
---
## 🔗 相关文件
### 修改的文件
1. `backend/src/modules/dc/tool-c/controllers/QuickActionController.ts`
2. `backend/src/modules/dc/tool-c/services/QuickActionService.ts`
3. `extraction_service/main.py`
4. `extraction_service/operations/compute.py`
5. `extraction_service/operations/pivot.py`
### 文档
- 本文档:`工具C_方案B实施总结_2025-12-09.md`
- 原Bug报告`工具C_Bug修复总结_2025-12-08.md`
---
## ✨ 总结
方案B成功实现了
1. **用户体验优秀** - 使用原列名,直观易懂
2. **技术可靠** - Python替换简单可控
3. **彻底解决** - 不再有特殊字符问题
**下一步**: 等待用户测试验证 ✅

View File

@@ -0,0 +1,697 @@
# 工具C - 缺失值处理功能开发计划
## 📋 概述
**目标**:将现有的"删除缺失值"功能升级为综合的"缺失值处理"功能,包括删除、填补、高级填补三种策略。
**设计方案**方案B - 合并对话框 + Tab切换
**核心原则**
-**填补操作创建新列**(保留原始数据,便于对比)
-**新列紧邻原列**(方便用户查看和比较)
-**MICE功能必须实现**(医学研究核心需求)
-**无需撤销功能**(原始数据未被修改)
---
## 🎯 功能需求
### Phase 1必备功能本次开发
#### Tab 1删除缺失值 ✅ 已有
- 保留现有功能
- 删除包含缺失值的行
- 删除缺失率过高的列
#### Tab 2填补缺失值 ⭐ 新增
1. **均值填补**Mean Imputation
- 适用于:数值型变量,正态分布
- 实现:`df[column].fillna(df[column].mean())`
2. **中位数填补**Median Imputation
- 适用于:数值型变量,偏态分布
- 实现:创建新列,填充中位数
3. **众数填补**Mode Imputation
- 适用于:分类变量、离散型数值
- 实现:创建新列,填充众数
4. **固定值填补**Constant Imputation
- 适用于:任何类型,用户指定值
- 实现:创建新列,填充指定值
**注意**:所有填补方法都会创建新列(如`体重_填补`),新列紧邻原列,便于对比验证。
#### Tab 3高级填补 ⭐ 新增
1. **MICE多重插补**Multivariate Imputation by Chained Equations
- 适用于缺失率5%-30%,需要考虑变量间关系
- 实现:使用 `sklearn.impute.IterativeImputer`
### Phase 2未来扩展本次不开发
- 前向/后向填充Forward/Backward Fill
- 分组填补Grouped Imputation
- 线性插值Linear Interpolation
- KNN填补KNN Imputation
---
## 🎨 UI设计
### 1. 按钮重命名
**原**`[删除缺失值]`
**新**`[缺失值处理]`
### 2. 对话框结构
```
┌───────────────────────────────────────────────────────┐
│ 缺失值处理 [X] │
├───────────────────────────────────────────────────────┤
│ ┌────────┬────────┬──────────┐ │
│ │ 删除 │ 填补 │ 高级填补 │ ← Ant Design Tabs │
│ └────────┴────────┴──────────┘ │
├───────────────────────────────────────────────────────┤
│ │
│ 【Tab内容区域】 │
│ │
│ │
│ [取消] [执行处理] │
└───────────────────────────────────────────────────────┘
```
### 3. Tab 2填补详细设计
```
┌───────────────────────────────────────────────────────┐
│ 【Tab 2: 填补缺失值】 │
│ │
│ 原始列:[体重kg▼] │
│ ⚠️ 仅支持单列填补 │
│ │
│ 新列名:[体重_填补 ] ← 用户可修改 │
│ 💡 新列将创建在原列旁边,便于对比 │
│ │
│ 📊 缺失值统计: │
│ ┌──────────────────────────────────────────────┐ │
│ │ • 当前缺失125个15.6% │ │
│ │ • 有效值675个84.4% │ │
│ │ • 数据类型:数值型 │ │
│ │ • 有效值范围45.2 - 98.5 kg │ │
│ │ • 有效值均值70.3 kg │ │
│ │ • 有效值中位数68.5 kg │ │
│ │ • 推荐方法:中位数填补(数据偏态)⭐ │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ 填补方法: │
│ ⚪ 均值填补(适合正态分布的数值变量) │
│ ⚪ 中位数填补(适合偏态分布的数值变量)⭐ │
│ ⚪ 众数填补(适合分类变量或离散数值) │
│ ⚪ 固定值填补:[_______] ← 用户输入 │
│ │
│ 📈 填补预览: │
│ ┌──────────────────────────────────────────────┐ │
│ │ • 填补值68.5 kg │ │
│ │ • 填补后均值70.2 kg原75.3 kg │ │
│ │ • 填补后标准差12.5 kg原10.8 kg │ │
│ │ • 将创建新列:"体重_填补" │ │
│ │ • 原列"体重kg"保持不变 ✅ │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ✅ 优势:原始数据保留,可随时对比验证 │
│ │
│ [取消] [执行填补] │
└───────────────────────────────────────────────────────┘
```
### 4. Tab 3高级填补详细设计
```
┌───────────────────────────────────────────────────────┐
│ 【Tab 3: 高级填补 - MICE多重插补】⭐ 必须实现 │
│ │
│ ⭐ MICE多重插补 │
Multivariate Imputation by Chained Equations
│ │
│ 选择要填补的列(可多选): │
│ ┌──────────────────────────────────────────────┐ │
│ │ ☑ 体重kg 缺失12515.6% │ │
│ │ ☑ 收缩压mmHg 缺失8210.3% │ │
│ │ ☐ BMI 缺失30.4% │ │
│ │ ☐ 舒张压mmHg 缺失00% │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ 新列命名规则: │
│ ☑ 自动命名:原列名 + "_MICE" │
│ 示例体重kg → 体重kg_MICE │
│ 收缩压mmHg → 收缩压mmHg_MICE │
│ │
│ 参数设置: │
│ 迭代次数:[10▼] 默认10次范围5-50
│ 随机种子:[42 ] (确保结果可重复) │
│ │
│ 📊 MICE说明
│ ┌──────────────────────────────────────────────┐ │
│ │ MICE会根据其他变量的值来预测缺失值。 │ │
│ │ │ │
│ │ ✅ 适用场景: │ │
│ │ • 缺失率5%-30% │ │
│ │ • 需要考虑变量间的相关性 │ │
│ │ • 多个变量同时有缺失 │ │
│ │ • 医学研究高质量填补的首选方法 ⭐ │ │
│ │ │ │
│ │ ⚠️ 注意: │ │
│ │ • 计算时间较长10万行约1分钟 │ │
│ │ • 需要足够的有效样本(建议>50%有效) │ │
│ │ • 新列将创建在各原列旁边,便于对比 │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ 💡 新列位置:每个新列紧邻其原列,便于逐列验证 │
│ │
│ [取消] [执行MICE填补] │
└───────────────────────────────────────────────────────┘
```
---
## 🛠️ 技术实现方案
### 1. Python端extraction_service
#### 新增文件:`operations/fillna.py`
```python
"""
缺失值填补 - 预写函数
支持均值、中位数、众数、固定值、MICE填补
"""
import pandas as pd
import numpy as np
from typing import Literal, Optional, List, Union, Any
from sklearn.impute import IterativeImputer
def fillna_simple(
df: pd.DataFrame,
column: str,
new_column_name: str,
method: Literal['mean', 'median', 'mode', 'constant'],
fill_value: Any = None
) -> dict:
"""
简单填补缺失值(创建新列)
Args:
df: 输入数据框
column: 原始列名
new_column_name: 新列名(如"体重_填补"
method: 填补方法
- 'mean': 均值填补
- 'median': 中位数填补
- 'mode': 众数填补
- 'constant': 固定值填补
fill_value: 固定值method='constant'时必填)
Returns:
{
'df': 包含新列的数据框(新列紧邻原列),
'stats': {
'original_column': 原列名,
'new_column': 新列名,
'missing_before': 缺失数量,
'fill_value': 填补的值,
'mean_after': 填补后均值,
'std_after': 填补后标准差
}
}
实现细节:
1. 复制原列数据
2. 执行填补
3. 使用 df.insert() 将新列插入到原列旁边
4. 返回包含新列的完整数据框
"""
def get_column_missing_stats(
df: pd.DataFrame,
column: str
) -> dict:
"""
获取列的缺失值统计信息
Returns:
{
'missing_count': 缺失数量,
'missing_rate': 缺失率,
'valid_count': 有效值数量,
'data_type': 数据类型,
'value_range': [min, max], # 仅数值型
'mean': 均值, # 仅数值型
'median': 中位数, # 仅数值型
'mode': 众数,
'recommended_method': 推荐的填补方法
}
"""
def fillna_mice(
df: pd.DataFrame,
columns: List[str],
n_iterations: int = 10,
random_state: int = 42
) -> dict:
"""
MICE多重插补创建新列⭐ 必须实现
Args:
df: 输入数据框
columns: 要填补的列名列表(如["体重kg", "收缩压mmHg"]
n_iterations: 迭代次数默认10范围5-50
random_state: 随机种子默认42确保结果可重复
Returns:
{
'df': 包含所有新列的数据框(每个新列紧邻其原列),
'stats': {
column: {
'original_column': 原列名,
'new_column': 新列名原名_MICE,
'missing_before': 缺失数量,
'filled_count': 填补数量,
'mean_before': 填补前均值,
'mean_after': 填补后均值
}
}
}
实现细节:
1. 对所选列执行MICE填补
2. 为每列创建新列命名原列名_MICE
3. 使用 df.insert() 将每个新列插入到其原列旁边
4. 返回包含所有新列的完整数据框
示例:
原列体重kg、收缩压mmHg
新列体重kg_MICE、收缩压mmHg_MICE
结果顺序体重kg、体重kg_MICE、收缩压mmHg、收缩压mmHg_MICE、...
"""
```
#### 修改文件:`main.py`
```python
# 新增API端点
@app.post("/fillna-simple")
async def operation_fillna_simple(request: FillnaSimpleRequest):
"""简单填补缺失值"""
@app.post("/fillna-stats")
async def get_fillna_stats(request: FillnaStatsRequest):
"""获取列的缺失值统计"""
@app.post("/fillna-mice")
async def operation_fillna_mice(request: FillnaMiceRequest):
"""MICE多重插补"""
```
### 2. Node.js后端backend
#### 修改文件:`services/QuickActionService.ts`
```typescript
// 新增方法
async executeFillnaSimple(params: {
sessionId: string;
column: string;
method: 'mean' | 'median' | 'mode' | 'constant';
fillValue?: any;
}): Promise<any>
async getFillnaStats(params: {
sessionId: string;
column: string;
}): Promise<any>
async executeFillnaMice(params: {
sessionId: string;
columns: string[];
nIterations: number;
}): Promise<any>
```
#### 修改文件:`controllers/QuickActionController.ts`
```typescript
// 新增处理方法
async handleFillnaSimple(request, reply)
async getFillnaStats(request, reply)
async handleFillnaMice(request, reply)
```
### 3. 前端frontend-v2
#### 重命名文件
- `DropnaDialog.tsx``MissingValueDialog.tsx`
#### 修改文件:`MissingValueDialog.tsx`
```typescript
interface MissingValueDialogProps {
open: boolean;
onClose: () => void;
sessionId: string;
columns: Array<{ id: string; name: string; type?: string }>;
onSuccess: () => void;
}
// 新增状态
const [activeTab, setActiveTab] = useState<'delete' | 'fill' | 'mice'>('fill');
const [selectedColumn, setSelectedColumn] = useState<string>('');
const [fillMethod, setFillMethod] = useState<'mean' | 'median' | 'mode' | 'constant'>('median');
const [fillValue, setFillValue] = useState<any>(null);
const [columnStats, setColumnStats] = useState<any>(null);
// Tab 1: 删除(保留原有逻辑)
// Tab 2: 填补(新增)
// Tab 3: MICE新增
```
#### 修改文件:`index.tsx`
```typescript
// 更新按钮组
const actionButtons = [
// ...
{
key: 'missing',
icon: <DeleteOutlined />,
label: '缺失值处理', // ← 重命名
onClick: () => setMissingValueDialogOpen(true),
},
// ...
];
```
---
## 📂 文件修改清单
### 新增文件
1. `extraction_service/operations/fillna.py` - 填补功能实现
2. `docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理功能开发计划.md` - 本文档
### 修改文件
#### Python端
1. `extraction_service/main.py`
- 新增 `/fillna-simple` 端点
- 新增 `/fillna-stats` 端点
- 新增 `/fillna-mice` 端点
#### Node.js后端
2. `backend/src/modules/dc/tool-c/services/QuickActionService.ts`
- 新增 `executeFillnaSimple` 方法
- 新增 `getFillnaStats` 方法
- 新增 `executeFillnaMice` 方法
3. `backend/src/modules/dc/tool-c/controllers/QuickActionController.ts`
- 新增 `handleFillnaSimple` 处理方法
- 新增 `getFillnaStats` 处理方法
- 新增 `handleFillnaMice` 处理方法
#### 前端
4. `frontend-v2/src/modules/dc/pages/tool-c/components/DropnaDialog.tsx`
- **重命名为** `MissingValueDialog.tsx`
- 新增 Tabs 组件(删除/填补/高级填补)
- Tab 1: 保留原有删除功能
- Tab 2: 新增简单填补功能(均值/中位数/众数/固定值)
- Tab 3: 新增MICE填补功能
5. `frontend-v2/src/modules/dc/pages/tool-c/index.tsx`
- 更新按钮标签:`删除缺失值``缺失值处理`
- 更新 Dialog 组件引用
6. `frontend-v2/src/modules/dc/api/index.ts`
- 新增 `fillnaSimple` API
- 新增 `getFillnaStats` API
- 新增 `fillnaMice` API
---
## 🔄 开发步骤
### Step 1: Python端基础功能30min
1. 创建 `fillna.py`
2. 实现 `fillna_simple` 函数
3. 实现 `get_column_missing_stats` 函数
4.`main.py` 添加对应端点
5. 测试使用Postman或curl测试API
### Step 2: Python端高级功能30min
1. 实现 `fillna_mice` 函数
2.`main.py` 添加对应端点
3. 测试使用Postman测试MICE功能
### Step 3: Node.js后端20min
1. 修改 `QuickActionService.ts`
2. 修改 `QuickActionController.ts`
3. 测试确保API转发正常
### Step 4: 前端UI重构40min
1. 重命名 `DropnaDialog.tsx``MissingValueDialog.tsx`
2. 实现Tabs结构
3. Tab 1: 迁移原有删除功能
4. Tab 2: 实现简单填补UI
5. Tab 3: 实现MICE填补UI
6. 更新 `index.tsx` 中的引用和按钮标签
### Step 5: 前端API集成20min
1.`api/index.ts` 添加新API
2. 集成到 `MissingValueDialog.tsx`
3. 实现实时统计获取
4. 实现填补预览
### Step 6: 端到端测试30min
1. 测试均值填补
2. 测试中位数填补
3. 测试众数填补
4. 测试固定值填补
5. 测试MICE填补
6. 测试删除功能(确保未破坏原有功能)
### Step 7: 优化和文档20min
1. 添加错误处理
2. 优化加载状态
3. 更新用户提示
4. 记录开发总结
**总计约3小时**
---
## 🧪 测试计划
### 功能测试用例
#### 测试数据准备
```
- 数值列正态分布年龄缺失15%
- 数值列偏态分布体重缺失20%
- 分类列婚姻状况缺失10%
- 多列缺失收缩压15%+ 舒张压12%
```
#### 测试用例
| 编号 | 功能 | 测试场景 | 预期结果 |
|------|------|----------|----------|
| TC-1 | 均值填补 | 对"年龄"列使用均值填补,新列名"年龄_填补" | 创建新列,缺失值被均值填充,原列不变 ✅ |
| TC-2 | 中位数填补 | 对"体重"列使用中位数填补 | 创建新列,缺失值被中位数填充 ✅ |
| TC-3 | 众数填补 | 对"婚姻状况"列使用众数填补 | 创建新列,缺失值被众数填充 ✅ |
| TC-4 | 固定值填补(数值) | 对"年龄"列填充固定值"0" | 创建新列所有缺失值变为0 ✅ |
| TC-5 | 固定值填补(文本) | 对"婚姻状况"列填充"未知" | 创建新列,所有缺失值变为"未知" ✅ |
| TC-6 | MICE填补 | 选择"收缩压"+"舒张压"执行MICE | 创建2个新列_MICE后缀缺失值被预测 ✅ |
| TC-7 | 新列位置验证 ⭐ | 对"列A"填补,查看新列位置 | 新列"列A_填补"紧邻原列"列A"右侧 ✅ |
| TC-8 | MICE新列位置 ⭐ | 对"列A"+"列C"执行MICE | 列A_MICE在列A旁列C_MICE在列C旁 ✅ |
| TC-9 | 统计信息准确性 | 选择任意列,查看统计信息 | 显示正确的缺失数、均值、中位数等 |
| TC-10 | 删除功能保留 | Tab 1删除缺失行 | 功能正常,与原功能一致 |
| TC-11 | 空列处理 | 对完全无缺失的列执行填补 | 提示"该列无缺失值"或复制原列 |
| TC-12 | 全缺失列处理 | 对全部缺失的列执行填补 | 提示警告,仍创建新列(全部为填补值) |
| TC-13 | 重复新列名处理 | 新列名已存在 | 自动添加后缀(如"体重_填补_1")或提示 |
| TC-14 | 原始数据保留 ⭐ | 填补后,检查原列 | 原列数据完全不变 ✅ |
### 边界测试
| 测试项 | 场景 | 预期 |
|--------|------|------|
| 超大数据集 | 10万行数据执行MICE | 显示进度,不崩溃 |
| 特殊字符列名 | 列名带括号、等号 | 正常处理使用columnMapping |
| 数据类型混合 | 对文本列执行均值填补 | 提示错误或自动跳过 |
| 并发处理 | 同时打开多个Dialog | 状态隔离,不互相影响 |
---
## 📊 性能要求
| 操作 | 数据量 | 目标响应时间 |
|------|--------|--------------|
| 简单填补(均值/中位数/众数) | 1万行 | < 1秒 |
| 简单填补 | 10万行 | < 5秒 |
| MICE填补 | 1万行 | < 10秒 |
| MICE填补 | 10万行 | < 60秒 |
| 统计信息获取 | 任意 | < 0.5秒 |
---
## 🚨 风险和注意事项
### 1. 数据安全 ✅ 已解决
- ✅ 填补操作创建新列,原始数据完全保留
- ✅ 新列紧邻原列,便于对比验证
- ✅ 无需撤销功能(原始数据未被修改)
- ✅ 用户可随时删除填补后的列,或重新填补
### 2. MICE性能 ⭐ 重点关注
- ⚠️ MICE在大数据集上可能很慢10万行约1分钟
-**必须显示进度条或加载动画**
- ✅ 添加"预计耗时"提示(基于数据量估算)
- ✅ 提供"取消执行"按钮(长时间任务)
- 💡 优化建议考虑使用Web Worker或后台任务队列
### 3. 数据类型兼容性
- ⚠️ 均值/中位数只适用于数值列
- ✅ 需要前端验证列的数据类型
- ✅ 后端也需要校验并返回友好错误
### 4. 列名特殊字符
- ⚠️ 列名可能包含特殊字符
- ✅ 使用现有的 `columnMapping` 机制
- ✅ 确保与compute列功能一致
### 5. 全部缺失的列
- ⚠️ 如果列全部为空,均值/中位数为NaN
- ✅ 需要特殊处理并提示用户
---
## 📝 依赖项
### Python依赖需要确认
```
pandas >= 1.5.0
numpy >= 1.23.0
scikit-learn >= 1.2.0 # ← MICE需要
```
### 前端依赖
- 无新增依赖使用现有的Ant Design组件
---
## 🎯 验收标准
### 必须满足 ⭐
1.**MICE功能完全实现**(非常重要!)
2.**新列位置正确**(紧邻原列右侧)
3.**原始数据完全保留**(填补不修改原列)
4. ✅ 所有测试用例通过特别是TC-7, TC-8, TC-14
5. ✅ 无Breaking Changes原有删除功能不受影响
6. ✅ UI符合设计稿3个Tab切换流畅
7. ✅ 代码通过Linter检查
8. ✅ 添加适当的日志和错误处理
9. ✅ MICE显示进度条或加载动画
### 加分项
1. ⭐ 性能优于预期
2. ⭐ UI动画流畅
3. ⭐ 错误提示友好且具体
4. ⭐ 添加单元测试
---
## 📅 时间估算(已更新)
| 阶段 | 预计时间 | 备注 |
|------|----------|------|
| Python后端 - 简单填补 | 40分钟 | fillna.py基础功能 |
| Python后端 - MICE填补 ⭐ | 50分钟 | **必须实现**包括sklearn集成 |
| Python后端 - 新列插入逻辑 | 30分钟 | df.insert()实现,确保新列紧邻原列 |
| Python - main.py端点 | 20分钟 | 新增3个API端点 |
| Node.js后端 | 20分钟 | 简单转发 |
| 前端UI - Tab结构 | 30分钟 | 3个Tab切换 |
| 前端UI - Tab 2简单填补 | 40分钟 | 表单 + 统计 + 新列名输入 |
| 前端UI - Tab 3MICE ⭐ | 40分钟 | 多选列 + 参数 + 进度条 |
| API集成 | 30分钟 | 前端调用后端,处理新列名 |
| 测试 | 40分钟 | 14个测试用例重点测试新列位置 |
| 优化和文档 | 20分钟 | 错误处理 + 文档 |
| **总计** | **约5-6小时** | **包含完整MICE实现** ⭐ |
**说明**
- MICE是医学研究的核心需求必须完整实现
- 新列插入逻辑需要仔细处理,确保位置正确
- 前端需要额外时间处理新列名输入和预览
---
## 📚 参考资料
### 缺失值填补理论
- [sklearn.impute.IterativeImputer文档](https://scikit-learn.org/stable/modules/generated/sklearn.impute.IterativeImputer.html)
- [MICE算法原理](https://www.jstatsoft.org/article/view/v045i03)
### 医学研究中的缺失值处理
- 均值/中位数填补:最常用,简单快速
- MICE高质量研究首选考虑变量间关系
- 分组填补:不同人群特征差异大时使用
---
## ✅ 开发前确认清单
已确认事项 ✅:
- [x] **MICE功能必须开发**(医学研究核心需求)✅
- [x] **填补方式:创建新列**(保留原始数据)✅
- [x] **新列位置:紧邻原列**(便于对比验证)✅
- [x] **无需撤销功能**(原始数据未被修改)✅
- [x] UI设计符合预期3个Tab切换
- [x] 功能范围合理Phase 1不包括分组填补、插值等
- [x] 性能要求合理MICE 10万行<60秒
- [x] 测试用例完整14个测试用例
- [x] 时间估算可接受约3-4小时
## 🚀 准备开始开发
所有确认清单已完成,随时可以开始实施!
---
## 📝 更新记录
### 2025-12-09 更新(根据用户确认)
**核心变更**
1.**MICE功能必须实现**(医学研究核心需求)
2.**填补方式改为创建新列**(保留原始数据)
3.**新列位置:紧邻原列右侧**(便于对比验证)
4.**取消撤销功能**(原始数据未被修改,无需撤销)
**影响**
- Python函数签名新增 `new_column_name` 参数
- UI新增"新列名"输入框
- 实现逻辑使用 `df.insert()` 确保位置正确
- 测试用例新增新列位置验证TC-7, TC-8
- 开发时间从3小时增加到5-6小时MICE+新列逻辑)
**优势**
- ✅ 原始数据完全保留,数据安全性更高
- ✅ 新旧数据并列显示,便于验证填补效果
- ✅ 用户可多次尝试不同填补方法对比
- ✅ 符合医学研究的严谨性要求
---
**已确认,准备开始开发!** 🚀