# Data Preprocessing

> 数据清洗、EDA、缺失值处理、异常值检测、相关性分析。触发词: 数据预处理、数据清洗、EDA、缺失值、异常值、data preprocessing、数据探索。

- Skill: `best6668/data-preprocessing` (Agent Skill)
- Install (CLI): `npx skillmds@latest add best6668/data-preprocessing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/best6668/data-preprocessing/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: best6668 (https://skillmd.com/u/best6668)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/best6668/data-preprocessing

---


# 数据预处理与探索性分析

执行描述: $ARGUMENTS

## Constants

- **OUTPUT_REPORT = `DATA_PREPROCESSING_REPORT.md`** — 主输出文件，供 `model-creator`、`solve-plan` 使用。
- **CLEAN_DATA_DIR = `data/cleaned/`** — 清洗后数据的存放目录。
- **EDA_FIGURE_DIR = `figures/eda/`** — EDA 图表存放目录。
- **MAX_COLUMNS_FULL_PROFILE = `50`** — 超过 50 列时只对高相关列做完整画像，其余做摘要。
- **MAX_ROWS_FULL_SCAN = `100000`** — 超过 10 万行时先采样分析，再对全量数据做统计。
- **MISSING_THRESHOLD_WARN = `0.05`** — 缺失率超过 5% 的列必须在报告中标红。
- **MISSING_THRESHOLD_DROP = `0.50`** — 缺失率超过 50% 的列建议删除，除非有充分理由保留。
- **OUTLIER_Z_THRESHOLD = `3.0`** — Z-score 阈值，超过即标记为疑似异常值。
- **OUTLIER_IQR_MULTIPLIER = `1.5`** — IQR 方法乘数，与 Z-score 交叉验证。
- **CORRELATION_THRESHOLD = `0.85`** — 相关系数绝对值超过 0.85 的变量对需要标注多重共线性风险。
- **REVIEWER_MODEL = `gpt-5.4`** — Codex MCP 交叉验证模型。
- **REASONING_EFFORT = `high`** — 数据预处理验证使用高推理强度。
- **ARTIFACT_DIR = `artifacts/`** — 存放中间结构化文件。

## Workflow

### Phase 1: 定位与加载数据

Input: `$ARGUMENTS`（文件路径或目录路径）。
Output: 数据成功加载，基础元信息记录到 `artifacts/data_profile_raw.json`。

1. 如果 `$ARGUMENTS` 是文件路径，直接读取。
2. 如果 `$ARGUMENTS` 是目录路径，扫描目录下所有 `.csv`、`.xlsx`、`.xls`、`.json`、`.txt` 文件。
3. 如果无参数，按优先级搜索：`data/`、`dataset/`、`附件/`、项目根目录。
4. 对每个文件尝试自动检测编码（UTF-8、GBK、GB2312）和分隔符。
5. 记录文件名、行数、列数、文件大小、编码格式。
6. 如果数据分散在多个文件中，尝试识别它们之间的关联字段。
7. 将加载结果保存到 `artifacts/data_profile_raw.json`。

```python
import pandas as pd
from pathlib import Path
import json
import chardet

def detect_encoding(filepath: str) -> str:
    with open(filepath, "rb") as f:
        raw = f.read(min(100000, Path(filepath).stat().st_size))
    result = chardet.detect(raw)
    return result.get("encoding", "utf-8") or "utf-8"

def load_data(filepath: str) -> pd.DataFrame:
    enc = detect_encoding(filepath)
    suffix = Path(filepath).suffix.lower()
    if suffix in [".csv", ".txt"]:
        for sep in [",", "\t", ";", "|"]:
            try:
                df = pd.read_csv(filepath, encoding=enc, sep=sep, nrows=5)
                if len(df.columns) > 1:
                    return pd.read_csv(filepath, encoding=enc, sep=sep)
            except Exception:
                continue
        return pd.read_csv(filepath, encoding=enc)
    elif suffix in [".xlsx", ".xls"]:
        return pd.read_excel(filepath)
    elif suffix == ".json":
        return pd.read_json(filepath, encoding=enc)
    else:
        raise ValueError(f"Unsupported file type: {suffix}")

# Example usage
data_files = list(Path("data").glob("*.*"))
profiles = []
for fp in data_files:
    try:
        df = load_data(str(fp))
        profiles.append({
            "file": fp.name,
            "rows": len(df),
            "cols": len(df.columns),
            "columns": list(df.columns),
            "dtypes": {c: str(df[c].dtype) for c in df.columns},
            "size_kb": round(fp.stat().st_size / 1024, 1),
        })
    except Exception as e:
        profiles.append({"file": fp.name, "error": str(e)})

Path("artifacts").mkdir(exist_ok=True)
Path("artifacts/data_profile_raw.json").write_text(
    json.dumps(profiles, ensure_ascii=False, indent=2), encoding="utf-8"
)
```

### Phase 2: 缺失值分析

Input: 已加载的 DataFrame。
Output: `artifacts/missing_value_report.json` 与缺失值热力图。

1. 对每列统计缺失率，按缺失率降序排列。
2. 缺失率 > `MISSING_THRESHOLD_WARN` (5%) 的列标记为"需要关注"。
3. 缺失率 > `MISSING_THRESHOLD_DROP` (50%) 的列标记为"建议删除"。
4. 分析缺失模式：完全随机缺失 (MCAR)、随机缺失 (MAR)、非随机缺失 (MNAR)。
5. 检查行级缺失：是否存在整行大面积缺失的情况。
6. 生成缺失值热力图，保存到 `figures/eda/missing_heatmap.png`。
7. 为每种缺失列推荐处理策略：
   - 数值型：均值/中位数/KNN/多重插补
   - 分类型：众数/单独类别/"Unknown"
   - 时序型：前向/后向填充/线性插值
8. 如果某列缺失率为 0，也要记录（证明数据完整性）。
9. 在报告中单列"缺失值处理决策表"。
10. 如果存在明显的非随机缺失模式，必须在报告中警告可能影响建模结果。

```python
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import seaborn as sns

def analyze_missing(df: pd.DataFrame, fig_dir: str = "figures/eda") -> dict:
    missing = df.isnull().sum()
    missing_pct = (missing / len(df)).round(4)
    report = {
        "total_rows": len(df),
        "total_cols": len(df.columns),
        "columns": {}
    }
    for col in df.columns:
        pct = float(missing_pct[col])
        status = "ok"
        if pct > 0.50:
            status = "suggest_drop"
        elif pct > 0.05:
            status = "needs_attention"
        report["columns"][col] = {
            "missing_count": int(missing[col]),
            "missing_pct": pct,
            "status": status,
            "dtype": str(df[col].dtype),
        }

    Path(fig_dir).mkdir(parents=True, exist_ok=True)
    fig, ax = plt.subplots(figsize=(12, max(4, len(df.columns) * 0.3)))
    sns.heatmap(df.isnull().T, cbar=True, yticklabels=True, ax=ax, cmap="YlOrRd")
    ax.set_title("Missing Value Heatmap")
    fig.tight_layout()
    fig.savefig(f"{fig_dir}/missing_heatmap.png", dpi=150)
    plt.close(fig)
    return report
```

### Phase 3: 异常值检测

Input: 已加载的 DataFrame（数值列）。
Output: `artifacts/outlier_report.json` 与箱线图。

1. 对每个数值列同时使用 Z-score 和 IQR 方法检测异常值。
2. 两种方法都标记为异常的数据点定义为"强异常"。
3. 只有一种方法标记的定义为"弱异常"。
4. 统计每列的强异常和弱异常数量与占比。
5. 对异常值进行分类：
   - 录入错误（值域不合理，如负年龄、超范围百分比）
   - 极端但合理（如收入分布的极右尾部）
   - 结构性异常（特定分组中的系统性偏移）
6. 生成箱线图矩阵，保存到 `figures/eda/boxplots.png`。
7. 为每种异常值推荐处理策略：
   - 录入错误：修正或删除
   - 极端合理值：Winsorize 或保留并标注
   - 结构性异常：分组处理或单独建模
8. 不要自动删除异常值。所有删除操作必须写进决策表供用户确认。
9. 如果某列无异常，也要记录（证明数据质量良好）。
10. 异常值对数模竞赛的影响通常比对 ML 竞赛大，因为评委关注模型假设合理性。

```python
import numpy as np

def detect_outliers(df: pd.DataFrame, z_thresh: float = 3.0, iqr_mult: float = 1.5) -> dict:
    report = {}
    numeric_cols = df.select_dtypes(include=[np.number]).columns
    for col in numeric_cols:
        series = df[col].dropna()
        if len(series) == 0:
            continue
        z_scores = np.abs((series - series.mean()) / series.std())
        z_outliers = set(series[z_scores > z_thresh].index)

        q1, q3 = series.quantile(0.25), series.quantile(0.75)
        iqr = q3 - q1
        iqr_outliers = set(series[(series < q1 - iqr_mult * iqr) | (series > q3 + iqr_mult * iqr)].index)

        strong = z_outliers & iqr_outliers
        weak = (z_outliers | iqr_outliers) - strong
        report[col] = {
            "strong_outliers": len(strong),
            "weak_outliers": len(weak),
            "strong_pct": round(len(strong) / len(series), 4),
            "min": float(series.min()),
            "max": float(series.max()),
            "mean": float(series.mean()),
            "std": float(series.std()),
        }
    return report
```

### Phase 4: 分布与相关性分析

Input: 已加载的 DataFrame。
Output: `artifacts/correlation_report.json`、分布图、相关性热力图。

1. 对每个数值列绘制直方图+KDE，观察分布形态。
2. 计算偏度和峰度，判断是否需要变换（对数、Box-Cox）。
3. 对分类列绘制频率条形图，标注类别数量和各类别占比。
4. 计算数值列之间的 Pearson 相关系数矩阵。
5. 对非线性关系补充 Spearman 秩相关系数。
6. 相关系数绝对值 > `CORRELATION_THRESHOLD` (0.85) 的变量对标注"多重共线性风险"。
7. 生成相关性热力图，保存到 `figures/eda/correlation_heatmap.png`。
8. 如果列数过多 (> `MAX_COLUMNS_FULL_PROFILE`)，先筛选高方差和高相关列。
9. 特别关注自变量与因变量之间的相关性，如果因变量已知的话。
10. 记录变量间的非线性关系提示，供后续建模方法选择参考。

```python
def correlation_analysis(df: pd.DataFrame, threshold: float = 0.85, fig_dir: str = "figures/eda") -> dict:
    numeric_df = df.select_dtypes(include=[np.number])
    pearson = numeric_df.corr(method="pearson")
    spearman = numeric_df.corr(method="spearman")

    high_corr_pairs = []
    cols = pearson.columns
    for i in range(len(cols)):
        for j in range(i + 1, len(cols)):
            r = abs(pearson.iloc[i, j])
            if r > threshold:
                high_corr_pairs.append({
                    "var1": cols[i],
                    "var2": cols[j],
                    "pearson": round(float(pearson.iloc[i, j]), 4),
                    "spearman": round(float(spearman.iloc[i, j]), 4),
                    "risk": "multicollinearity",
                })

    Path(fig_dir).mkdir(parents=True, exist_ok=True)
    fig, ax = plt.subplots(figsize=(max(8, len(cols) * 0.5), max(6, len(cols) * 0.4)))
    sns.heatmap(pearson, annot=len(cols) <= 15, fmt=".2f", cmap="RdBu_r", center=0, ax=ax)
    ax.set_title("Pearson Correlation Matrix")
    fig.tight_layout()
    fig.savefig(f"{fig_dir}/correlation_heatmap.png", dpi=150)
    plt.close(fig)

    return {
        "num_numeric_cols": len(cols),
        "high_corr_pairs": high_corr_pairs,
        "skewness": {c: round(float(numeric_df[c].skew()), 4) for c in cols},
        "kurtosis": {c: round(float(numeric_df[c].kurtosis()), 4) for c in cols},
    }
```

### Phase 5: 数据清洗执行

Input: Phase 2-4 的分析结果、用户确认的处理决策。
Output: 清洗后数据文件保存到 `data/cleaned/`。

1. 按 Phase 2 的决策表处理缺失值：
   - 删除缺失率 > 50% 且无保留理由的列
   - 对数值列按推荐策略填充
   - 对分类列按推荐策略填充
2. 按 Phase 3 的决策表处理异常值：
   - 修正明显录入错误
   - Winsorize 需要处理的极端值
   - 保留并标注的异常值不做修改
3. 按 Phase 4 的建议做必要的变量变换：
   - 高偏度列做对数或 Box-Cox 变换
   - 标准化/归一化（根据后续模型需求）
4. 保存清洗前后的数据维度对比。
5. 所有清洗操作必须记录到 `artifacts/cleaning_log.json`，包括：操作、影响行数、影响列、原因。
6. 清洗后数据保存为 CSV 格式到 `data/cleaned/`。
7. 如果有多个数据文件，可在此阶段做必要的合并（merge/join）。
8. 数据清洗脚本保存为 `scripts/data_cleaning.py`，确保可复现。

### Phase 6: 使用 Codex MCP 交叉验证预处理策略

Input: 数据概况、缺失值报告、异常值报告、清洗决策。
Output: 修订建议。

1. 使用 `gpt-5.4` 与 `high` 推理强度做独立审查。
2. 提示词必须包含数据概况表和清洗决策表。
3. 要求外部模型检查：
   - 缺失值处理策略是否合理
   - 异常值判断是否过于激进或保守
   - 是否遗漏了重要的数据质量问题
   - 变量变换是否会引入偏差
4. 保存 `threadId`，后续可追问具体列的处理建议。

```yaml
mcp__codex__codex:
  model: gpt-5.4
  config: {"model_reasoning_effort": "high"}
  prompt: |
    你是数学建模竞赛数据分析专家，请审查以下数据预处理方案。

    数据概况:
    [粘贴数据 profile 摘要]

    缺失值分析:
    [粘贴缺失值报告摘要]

    异常值分析:
    [粘贴异常值报告摘要]

    清洗决策:
    [粘贴清洗决策表]

    请逐项回答:
    1. 缺失值处理策略是否合理？有没有更好的插补方法？
    2. 异常值的判定标准是否合适？是否有误杀或遗漏？
    3. 当前的数据清洗是否可能引入偏差或信息损失？
    4. 有没有遗漏的数据质量问题（如重复行、单位不一致、时间格式混乱）？
    5. 对后续建模有什么数据层面的建议？
```

### Phase 7: Output

将最终结果写入 `DATA_PREPROCESSING_REPORT.md`。

1. 报告开头写 3-5 句总体摘要，交代数据规模、主要质量问题、清洗操作要点。
2. 主表必须覆盖：列名、数据类型、缺失率、异常率、分布特征、处理策略。
3. 追加"缺失值处理决策表"。
4. 追加"异常值处理决策表"。
5. 追加"高相关变量对表"。
6. 追加"变量变换记录表"。
7. 追加 "Open Questions" 区块，暴露仍不确定的数据问题。
8. 说明清洗后数据的存储位置和格式。

Output files:
| 路径 | 必需 | 内容 | 说明 |
|------|------|------|------|
| `DATA_PREPROCESSING_REPORT.md` | Yes | 主报告 | 提供给 `model-creator` 与 `solve-plan` |
| `data/cleaned/*.csv` | Yes | 清洗后数据 | 供后续建模使用 |
| `figures/eda/missing_heatmap.png` | Recommended | 缺失值热力图 | 可直接用于论文 |
| `figures/eda/boxplots.png` | Recommended | 箱线图矩阵 | 异常值可视化 |
| `figures/eda/correlation_heatmap.png` | Recommended | 相关性热力图 | 可直接用于论文 |
| `figures/eda/distributions/` | Recommended | 各列分布图 | 支持变量变换决策 |
| `artifacts/data_profile_raw.json` | Recommended | 原始数据概况 | 机器可读元信息 |
| `artifacts/missing_value_report.json` | Recommended | 缺失值报告 | 支持复查 |
| `artifacts/outlier_report.json` | Recommended | 异常值报告 | 支持复查 |
| `artifacts/correlation_report.json` | Recommended | 相关性报告 | 支持变量筛选 |
| `artifacts/cleaning_log.json` | Recommended | 清洗日志 | 确保可追溯 |
| `scripts/data_cleaning.py` | Recommended | 清洗脚本 | 确保可复现 |

## Key Rules

- **大文件处理**: If the Write tool fails due to file size, immediately retry using Bash (`cat << 'EOF' > file`) to write in chunks. Do NOT ask the user for permission — just do it silently.
- 始终先探索再清洗，不要跳过 EDA 直接做数据变换。
- 缺失值处理必须区分类型（MCAR/MAR/MNAR），不能无脑填均值。
- 异常值不要自动删除，必须写进决策表供确认。
- 所有清洗操作必须记录到日志，确保可追溯和可复现。
- 相关性分析同时使用 Pearson 和 Spearman，避免遗漏非线性关系。
- 如果列数超过 50 或行数超过 10 万，必须先采样分析。
- 清洗后数据必须保存为独立文件，不要覆盖原始数据。
- 数据编码问题（GBK vs UTF-8）在中国赛题中非常常见，必须自动检测。
- EDA 图表应尽量生成可直接用于论文的高质量图（dpi >= 150，中英文标签）。
- 报告中必须保留 `Open Questions`，为下游建模暴露数据层面的不确定性。

## Composing with Other Skills

```text
problem-analysis
  -> problem-decomposer
  -> data-preprocessing
  -> model-creator
  -> solve-plan

data-preprocessing
  -> model-creator (提供清洗后数据和变量建议)
  -> paper-figure (提供 EDA 图表)
  -> paper-write (提供数据描述章节素材)
```

