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 + 测试