OpsMind 数据预处理模块升级:从简单清洗到智能结构化

一、背景与问题

1.1 原有预处理能力

OpsMind 的数据预处理模块已具备以下能力:

功能 方法 说明
数据诊断 diagnose() 缺失率、重复行、类型异常检测
基础清洗 clean() 去重、类型修正、缺失值填充
衍生属性 generate_derived() 年龄段分箱、日期维度展开
LLM 建议 suggest_derived() 根据查询意图建议衍生规则

1.2 遇到的问题

在实际使用中,我们发现了一个常见的数据格式问题:宽表结构

┌─────────────────────────────────────────────────────────────────────────┐
│ 典型宽表问题 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 原始数据(宽表): │
│ ┌──────────┬──────┬──────┬──────┬──────┬──────┐ │
│ │ Unnamed │ 2020 │ 2021 │ 2022 │ 2023 │ 2024 │ │
│ ├──────────┼──────┼──────┼──────┼──────┼──────┤ │
│ │ 北京 │ 100 │ 120 │ 150 │ 180 │ 200 │ │
│ │ 上海 │ 200 │ 220 │ 250 │ 280 │ 300 │ │
│ │ 广东 │ 150 │ 170 │ 190 │ 210 │ 230 │ │
│ └──────────┴──────┴──────┴──────┴──────┴──────┘ │
│ │
│ 问题: │
│ 1. 年份作为列名,无法按时间维度分析 │
│ 2. Unnamed 列名无语义,难以理解 │
│ 3. 无法直接绘制趋势图(需要年份作为行) │
│ │
│ 期望格式(长表): │
│ ┌──────────┬──────┬──────┐ │
│ │ 省份 │ 年份 │ 值 │ │
│ ├──────────┼──────┼──────┤ │
│ │ 北京 │ 2020 │ 100 │ │
│ │ 北京 │ 2021 │ 120 │ │
│ │ ... │ ... │ ... │ │
│ └──────────┴──────┴──────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘

1.3 其他痛点

  1. 异常值干扰:极端值影响统计分析和图表展示
  2. 表头行错位:Excel 导出时表头前有说明行,导致列名解析错误
  3. Unnamed 列:导出数据丢失列名,难以理解数据语义

二、新增功能概览

2.1 功能矩阵

功能 方法 说明 新增
数据诊断 diagnose() 缺失率、重复行、类型异常 -
基础清洗 clean() 去重、类型修正、缺失值填充 -
宽表检测 detect_structure() 检测年份列、日期列、Unnamed列
结构归一化 normalize() 宽表→长表、表头校正
异常值处理 _handle_outliers() IQR 方法截断异常值
列名推断 _infer_column_name() 根据内容推断语义名称
衍生属性 generate_derived() 年龄段、日期维度 -
LLM 建议 suggest_derived() 智能衍生建议 -
一站式预处理 prepare() 全流程整合 升级

2.2 预处理流程升级

升级前:
┌─────────┐ ┌─────────┐ ┌─────────────┐
│ clean │ → │ suggest │ → │ generate │
│ │ │ derived │ │ derived │
└─────────┘ └─────────┘ └─────────────┘

升级后:
┌───────────┐ ┌─────────┐ ┌───────────┐ ┌─────────┐ ┌─────────────┐
│ normalize │ → │ clean │ → │ outliers │ → │ suggest │ → │ generate │
│ (新增) │ │ │ │ (新增) │ │ derived │ │ derived │
└───────────┘ └─────────┘ └───────────┘ └─────────┘ └─────────────┘

├── detect_structure() 检测宽表
├── melt_wide_table() 宽表转长表
└── 表头行校正

三、宽表结构检测

3.1 检测逻辑

def detect_structure(self, df: pd.DataFrame) -> Dict[str, Any]:
"""
轻量级宽表结构检测(无 LLM 调用)

Returns:
{
needs_attention: bool, # 是否需要用户关注
confidence: float, # 检测置信度 (0-1)
format: str, # "wide_year" | "wide_date" | "normal"
year_cols: List[str], # 年份列名列表
date_str_cols: List[str], # 日期字符串列名列表
text_cols: List[str], # 非时间文本列名
nan_cols: List[str], # 全 NaN 列名
unnamed_cols: List[str], # Unnamed 列名
inferred_names: Dict, # 推断的列名映射
suggested_config: Dict, # 建议的转换配置
}
"""

3.2 检测维度

┌─────────────────────────────────────────────────────────────────────────┐
│ 宽表检测维度 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 1. 年份列检测 │ │
│ │ 列名可直接转 int,且落在 [1900, 2100] │ │
│ │ 例:2020, 2021, 2022, 2023, 2024 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 2. 日期字符串列检测 │ │
│ │ 匹配正则:2023Q1, Jan-2023, 2024-01 等 │ │
│ │ 例:2020Q1, 2020Q2, 2021Q1, 2021Q2 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 3. Unnamed 列检测 │ │
│ │ 列名以 "Unnamed:" 开头,通常是导出时丢失的索引列 │ │
│ │ 例:Unnamed: 0, Unnamed: 1 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 4. 全 NaN 列检测 │ │
│ │ 整列都是空值,通常是导出时的空白列 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 5. 表头行错位检测 │ │
│ │ 大多数列名是 Unnamed,但首行数据看起来更像列名 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘

3.3 置信度计算

# 置信度计算逻辑
confidence = 0.0

# 时间列占比
if n_time >= 3:
time_ratio = n_time / n_cols
confidence += 0.5 if time_ratio > 0.7 else (0.3 if time_ratio > 0.4 else 0.1)

# 文本列数量
if n_text_obj == 1:
confidence += 0.3 # 单一维度列,典型宽表
elif n_text_obj > 1:
confidence += 0.1 # 多维度列

# 行数较少(宽表特征)
if len(df) < 200:
confidence += 0.2

confidence = min(confidence, 1.0)

四、结构归一化

4.1 normalize() 方法

def normalize(self, df: pd.DataFrame,
config: Optional[Dict] = None) -> Tuple[pd.DataFrame, Dict]:
"""
结构归一化

Args:
df: 原始 DataFrame
config: 用户确认的转换配置(来自 UI),None 表示自动无损清理

Returns:
(df_out, report) 元组
"""

4.2 自动无损清理

config=None 时,自动执行无损操作:

if config is None:
detection = self.detect_structure(df)

# 1. 删除全 NaN 列
nan_cols = detection.get("nan_cols", [])
if nan_cols:
df = df.drop(columns=nan_cols)

# 2. 重命名 Unnamed 列
inferred = detection.get("inferred_names", {})
if inferred:
df = df.rename(columns=inferred)

return df, {"action": "auto_clean", "details": [...]}

4.3 宽表转长表(melt)

当检测到宽表且用户确认后:

if action == "melt":
df = df.melt(
id_vars=id_vars_final, # 维度列(如省份)
value_vars=value_vars, # 时间列(如 2020, 2021...)
var_name="年份", # 新列名:时间维度
value_name="值", # 新列名:数值
)

转换示例

宽表:
┌──────────┬──────┬──────┬──────┐
│ 省份 │ 2022 │ 2023 │ 2024 │
├──────────┼──────┼──────┼──────┤
│ 北京 │ 100 │ 120 │ 150 │
│ 上海 │ 200 │ 220 │ 250 │
└──────────┴──────┴──────┴──────┘

↓ melt 转换 ↓

长表:
┌──────────┬──────┬──────┐
│ 省份 │ 年份 │ 值 │
├──────────┼──────┼──────┤
│ 北京 │ 2022 │ 100 │
│ 北京 │ 2023 │ 120 │
│ 北京 │ 2024 │ 150 │
│ 上海 │ 2022 │ 200 │
│ 上海 │ 2023 │ 220 │
│ 上海 │ 2024 │ 250 │
└──────────┴──────┴──────┘

4.4 表头行校正

处理 Excel 导出时表头前有说明行的情况:

# 检测:大多数列名是 Unnamed,但首行数据看起来更像列名
unnamed_ratio = sum(1 for c in df.columns if str(c).startswith("Unnamed:")) / n_cols
if unnamed_ratio > 0.4:
row0_str_count = sum(1 for v in df.iloc[0] if isinstance(v, str) and v.strip())
if row0_str_count / n_cols > 0.25:
suggested_header_row = 0 # 首行应作为列名

# 执行校正
if header_row is not None:
new_cols = [str(v).strip() for v in df.iloc[header_row]]
df = df.iloc[header_row + 1:].reset_index(drop=True)
df.columns = new_cols

五、异常值处理

5.1 IQR 方法

使用四分位距(Interquartile Range)检测异常值:

def _handle_outliers(self, df: pd.DataFrame, strategy: str = "cap") -> pd.DataFrame:
"""
IQR 法检测并处理数值列中的异常值

Args:
df: DataFrame
strategy: "cap" 截断 | "drop" 删除
"""
for col in numeric_cols:
q1, q3 = series.quantile(0.25), series.quantile(0.75)
iqr = q3 - q1
lower, upper = q1 - 1.5 * iqr, q3 + 1.5 * iqr

if strategy == "cap":
df[col] = df[col].clip(lower=lower, upper=upper)
elif strategy == "drop":
df = df[(df[col] >= lower) & (df[col] <= upper)]

5.2 异常值判定标准

┌─────────────────────────────────────────────────────────────────────────┐
│ IQR 异常值判定 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 数据分布: │
│ │
│ ──────────┬──────────┬──────────┬──────────┬─────────── │
│ │ │ │ │ │
│ lower Q1 median Q3 upper │
│ │ │ │ │ │
│ Q1-1.5×IQR │ │ │ Q3+1.5×IQR │
│ │ │ │ │ │
│ ──────────┴──────────┴──────────┴──────────┴─────────── │
│ │
│ 正常范围:[Q1 - 1.5×IQR, Q3 + 1.5×IQR] │
│ 异常值:< lower 或 > upper │
│ │
│ 处理策略: │
│ - cap: 将异常值截断到边界值(保留数据行) │
│ - drop: 删除含异常值的行(减少数据量) │
│ │
└─────────────────────────────────────────────────────────────────────────┘

六、一站式预处理升级

6.1 prepare() 方法升级

def prepare(
self,
df: pd.DataFrame,
query: Optional[str] = None,
use_llm_suggestions: bool = True,
table_config: Optional[Dict] = None, # 新增:用户确认的宽表配置
) -> tuple:
"""
一站式预处理

流程:
normalize → clean → outliers → suggest_derived → generate_derived

Returns:
(df_cleaned, cleaning_report) 元组
"""

6.2 完整流程

def prepare(self, df, query, use_llm_suggestions, table_config):
self._cleaning_report = {}

# Step 0: 结构归一化(宽表→长表等)
df, norm_report = self.normalize(df, table_config)
self._cleaning_report["normalize"] = norm_report

# Step 1: 基础清洗(去重、类型修正、缺失值处理)
df = self.clean(df)

# Step 2: 异常值处理(IQR cap)
df = self._handle_outliers(df, strategy="cap")

# Step 3: LLM 衍生属性建议
suggestions = []
if use_llm_suggestions and self.client and query:
suggestions = self.suggest_derived(df, query)

# Step 4: 生成衍生属性
df = self.generate_derived(df, suggestions=suggestions)

return df, self._cleaning_report

6.3 清洗报告示例

{
"normalize": {
"action": "melt",
"details": [
"重设表头: 使用第 0 行作为列名",
"删除指定列: ['Unnamed: 5']",
"宽表转长表完成: 150行×3列"
]
},
"clean": {
"duplicates_dropped": 5,
"type_conversions": ["'销售额' 转为数值型"],
"missing_filled": ["'地区'(缺失率3.2%, 填充方式:众数('华东'))"],
"columns_dropped": [],
"shape_before": "50行×6列",
"shape_after": "45行×5列"
},
"outliers": [
"'销售额': 3个异常值已截断至 [1000, 50000]"
],
"llm_suggestions_applied": 2
}

七、实际应用场景

7.1 场景一:年度数据宽表

原始数据

┌──────────┬──────┬──────┬──────┬──────┬──────┐
│ Unnamed │ 2020 │ 2021 │ 2022 │ 2023 │ 2024 │
├──────────┼──────┼──────┼──────┼──────┼──────┤
│ 北京 │ 100 │ 120 │ 150 │ 180 │ 200 │
│ 上海 │ 200 │ 220 │ 250 │ 280 │ 300 │
│ 广东 │ 150 │ 170 │ 190 │ 210 │ 230 │
└──────────┴──────┴──────┴──────┴──────┴──────┘

检测结果

{
"needs_attention": True,
"confidence": 0.9,
"format": "wide_year",
"year_cols": ["2020", "2021", "2022", "2023", "2024"],
"unnamed_cols": ["Unnamed: 0"],
"inferred_names": {"Unnamed: 0": "省份"},
"suggested_config": {
"action": "melt",
"id_vars": ["Unnamed: 0"],
"value_vars": ["2020", "2021", "2022", "2023", "2024"],
"var_name": "年份",
"value_name": "值",
"rename_id_var": "省份"
}
}

转换后

┌──────────┬──────┬──────┐
│ 省份 │ 年份 │ 值 │
├──────────┼──────┼──────┤
│ 北京 │ 2020 │ 100 │
│ 北京 │ 2021 │ 120 │
│ ... │ ... │ ... │
└──────────┴──────┴──────┘

7.2 场景二:季度数据宽表

原始数据

┌──────────┬─────────┬─────────┬─────────┬─────────┐
│ 产品 │ 2023Q1 │ 2023Q2 │ 2023Q3 │ 2023Q4 │
├──────────┼─────────┼─────────┼─────────┼─────────┤
│ A类 │ 1000 │ 1200 │ 1500 │ 1800 │
│ B类 │ 2000 │ 2200 │ 2500 │ 2800 │
└──────────┴─────────┴─────────┴─────────┴─────────┘

检测结果

{
"format": "wide_date",
"date_str_cols": ["2023Q1", "2023Q2", "2023Q3", "2023Q4"],
"suggested_config": {
"action": "melt",
"var_name": "时间",
"value_name": "值"
}
}

7.3 场景三:表头行错位

原始数据

┌──────────┬──────────┬──────────┬──────────┐
│ Unnamed │ Unnamed │ Unnamed │ Unnamed │
├──────────┼──────────┼──────────┼──────────┤
│ 地区 │ 产品 │ 销售额 │ 利润 │ ← 真正的列名
├──────────┼──────────┼──────────┼──────────┤
│ 华东 │ A类 │ 1000 │ 200 │
│ 华南 │ B类 │ 2000 │ 400 │
└──────────┴──────────┴──────────┴──────────┘

检测结果

{
"suggested_header_row": 0, # 首行应作为列名
}

校正后

┌──────────┬──────────┬──────────┬──────────┐
│ 地区 │ 产品 │ 销售额 │ 利润 │
├──────────┼──────────┼──────────┼──────────┤
│ 华东 │ A类 │ 1000 │ 200 │
│ 华南 │ B类 │ 2000 │ 400 │
└──────────┴──────────┴──────────┴──────────┘

八、总结与展望

8.1 本次升级总结

方面 升级前 升级后
宽表处理 ❌ 不支持 ✅ 自动检测 + 智能转换
异常值 ❌ 不处理 ✅ IQR 检测 + 截断
列名推断 ❌ 无 ✅ 根据内容推断语义名
表头校正 ❌ 无 ✅ 自动检测错位表头
流程完整性 3 步 5 步

8.2 技术亮点

  1. 零 LLM 调用检测detect_structure() 纯规则检测,无延迟
  2. 置信度评估:量化检测结果可信度,辅助用户决策
  3. 无损优先:自动清理只执行无损操作,有损操作需用户确认
  4. 完整报告:每步操作都有详细日志,便于追溯

8.3 后续规划

功能 说明 优先级
多级宽表支持 支持多个维度列的宽表转换 🟡 中
自定义分箱规则 用户可配置分箱边界 🟡 中
数据质量评分 综合评估数据质量 🟢 低
增量预处理 支持数据追加时的增量处理 🟢 低

本次升级让 OpsMind 的数据预处理能力从”简单清洗”进化为”智能结构化”,能够自动识别和处理复杂的数据格式问题,大大提升了数据分析的效率和准确性。


本文由 OpsMind 技术团队撰写,转载请注明出处。