3. 数据清洗与预处理:处理缺失值、异常值检测与处理、数据标准化与归一化

在历史项目成本估算中,原始数据往往来自多个异构系统(如 ERP、项目管理软件、手工台账),存在大量缺失、异常和量纲不一致的问题。本章聚焦于数据清洗与预处理的三大核心环节,确保后续特征工程和参数调优建立在干净、可靠的数据基础上。

3.1 缺失值处理策略

缺失值在历史成本数据中极为常见,原因包括:字段未强制录入、系统迁移丢失、特定项目阶段未采集等。处理缺失值需根据缺失机制和数据特性选择合适方法。

缺失机制 定义 成本数据示例 推荐处理方式
完全随机缺失 (MCAR) 缺失与任何变量无关 某项目人工工时因系统故障随机丢失 删除或均值/中位数填充
随机缺失 (MAR) 缺失与其他观测变量相关 大型项目更易缺失材料成本明细 回归填充、KNN 填充、多重插补
非随机缺失 (MNAR) 缺失与未观测到的因素相关 亏损项目故意不填报实际成本 需领域知识建模,或标记为特殊类别
实战建议:在成本估算场景中,若某特征缺失率超过 60%,优先考虑删除该特征;若缺失率在 20%-60% 之间,使用多重插补(MICE)或迭代决策树填充;低于 20% 可采用中位数或 KNN 填充。切勿对 MNAR 数据直接填充,否则会引入系统性偏差。

缺失值处理流程

  1. 缺失诊断:使用 isnull().sum() / len(df) 计算每列缺失率,绘制缺失热力图。
  2. 删除策略:按行删除(listwise deletion)适用于 MCAR 且样本充足;按列删除适用于高缺失率特征。
  3. 统计填充:数值型用中位数(对异常值鲁棒),分类型用众数。
  4. 模型填充:使用 KNN、随机森林或线性回归预测缺失值。
  5. 标记法:新增布尔列标记是否缺失,保留缺失信息。
# 缺失值处理示例(Python)
import pandas as pd
import numpy as np
from sklearn.impute import KNNImputer

# 加载历史成本数据
df = pd.read_csv('historical_cost_data.csv')

# 步骤1:缺失诊断
missing_ratio = df.isnull().mean().sort_values(ascending=False)
print("缺失率前5特征:\n", missing_ratio.head())

# 步骤2:删除缺失率 > 60% 的特征
high_missing_cols = missing_ratio[missing_ratio > 0.6].index
df.drop(columns=high_missing_cols, inplace=True)

# 步骤3:数值型特征用中位数填充
num_cols = df.select_dtypes(include=[np.number]).columns
df[num_cols] = df[num_cols].fillna(df[num_cols].median())

# 步骤4:分类型特征用众数填充
cat_cols = df.select_dtypes(include=['object']).columns
for col in cat_cols:
    df[col].fillna(df[col].mode()[0], inplace=True)

# 步骤5:KNN 填充(适用于低缺失率关键特征)
imputer = KNNImputer(n_neighbors=5)
df[['labor_hours', 'material_cost']] = imputer.fit_transform(df[['labor_hours', 'material_cost']])

3.2 异常值检测与处理

成本数据中的异常值可能源于录入错误、特殊项目事件(如自然灾害导致成本飙升)或数据合并错误。异常值会严重扭曲统计分布和模型参数,必须系统检测。

异常值检测方法对比 Z-Score 方法 假设正态分布,|Z| > 3 为异常 适用:对称分布,样本量 > 30 IQR 四分位距法 Q1 - 1.5*IQR 或 Q3 + 1.5*IQR 适用:偏态分布,含离群点 孤立森林 基于树模型,异常易被孤立 适用:高维数据,非线性 异常值处理策略 删除 确认录入错误时使用 截尾/封顶 替换为边界值(Winsorize) 转换 对数/Box-Cox 变换 单独建模 异常值作为独立类别 ⚠ 异常值处理原则 先区分异常类型(自然异常 vs 错误异常),再选择处理策略,避免误删真实成本波动

异常值检测实战步骤

  1. 可视化初筛:绘制箱线图、直方图、散点图,快速定位离群点。
  2. 统计检测:对近似正态分布的特征使用 Z-Score;对偏态分布使用 IQR 法。
  3. 模型检测:使用孤立森林(Isolation Forest)或 LOF(Local Outlier Factor)检测高维异常。
  4. 领域验证:将检测出的异常值交由成本工程师确认是否为真实异常。
  5. 处理执行:根据异常类型选择删除、截尾或转换。
# 异常值检测与处理示例
from scipy import stats
import numpy as np

# 使用 Z-Score 检测异常值
z_scores = np.abs(stats.zscore(df['total_cost']))
outliers_z = df[z_scores > 3]
print(f"Z-Score 检测到 {len(outliers_z)} 个异常值")

# 使用 IQR 法检测
Q1 = df['labor_hours'].quantile(0.25)
Q3 = df['labor_hours'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers_iqr = df[(df['labor_hours'] < lower_bound) | (df['labor_hours'] > upper_bound)]
print(f"IQR 法检测到 {len(outliers_iqr)} 个异常值")

# 截尾处理(Winsorize)
from scipy.stats.mstats import winsorize
df['material_cost_winsorized'] = winsorize(df['material_cost'], limits=[0.05, 0.05])

# 对数变换处理右偏异常
df['total_cost_log'] = np.log1p(df['total_cost'])

3.3 数据标准化与归一化

成本估算特征通常具有不同量纲和单位(如人工工时以小时计,材料成本以万元计,项目工期以月计)。标准化和归一化能消除量纲影响,使梯度下降收敛更快,提升模型性能。

方法 公式 输出范围 适用场景 对异常值敏感性
Z-Score 标准化 z = (x - μ) / σ 均值=0,标准差=1 数据近似正态分布;SVM、线性回归、PCA 高
Min-Max 归一化 x' = (x - min) / (max - min) [0, 1] 数据有明确边界;神经网络、KNN 极高
Robust 缩放 x' = (x - median) / IQR 无固定范围 数据含较多异常值;成本数据首选 低
MaxAbs 缩放 x' = x / |max| [-1, 1] 稀疏数据;文本特征 高
关键注意:标准化/归一化的参数(均值、标准差、最小值、最大值)必须仅从训练集计算,然后应用到验证集和测试集。切勿在整个数据集上计算后再拆分,否则会造成数据泄露,导致模型性能评估过于乐观。

标准化与归一化选择指南

  • 成本数据含异常值 → RobustScaler:使用中位数和 IQR,对异常值不敏感,是成本估算场景的首选。
  • 特征分布近似正态 → StandardScaler:适用于线性模型和距离度量方法。
  • 需要固定范围输入 → MinMaxScaler:适用于神经网络和图像处理,但对异常值敏感。
  • 稀疏数据 → MaxAbsScaler:保持稀疏性,不压缩到 [0,1] 区间。
# 标准化与归一化实战代码
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
from sklearn.model_selection import train_test_split

# 拆分数据集(防止数据泄露)
X = df[['labor_hours', 'material_cost', 'project_duration', 'team_size']]
y = df['total_cost']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 方案1:RobustScaler(推荐用于成本数据)
robust_scaler = RobustScaler()
X_train_robust = robust_scaler.fit_transform(X_train)
X_test_robust = robust_scaler.transform(X_test)

# 方案2:StandardScaler(正态分布特征)
std_scaler = StandardScaler()
X_train_std = std_scaler.fit_transform(X_train)
X_test_std = std_scaler.transform(X_test)

# 方案3:MinMaxScaler(神经网络输入)
minmax_scaler = MinMaxScaler()
X_train_mm = minmax_scaler.fit_transform(X_train)
X_test_mm = minmax_scaler.transform(X_test)

print("训练集 Robust 缩放后均值:", X_train_robust.mean(axis=0).round(2))
print("训练集 Robust 缩放后标准差:", X_train_robust.std(axis=0).round(2))
数据预处理完整流水线 原始成本数据 缺失值处理 删除/填充/插补 异常值检测 Z-Score/IQR/IF 标准化/归一化 Robust/Standard/MinMax 训练集 (80%) 拟合 scaler 参数 测试集 (20%) 仅 transform,不 fit 🔑 数据预处理流水线关键原则 1. 先拆分训练/测试集,再分别进行缺失填充和缩放 2. 所有预处理参数(均值、中位数、边界值)仅从训练集学习

3.4 综合案例:成本数据预处理流水线

以下代码整合了缺失值处理、异常值检测与 Robust 缩放,形成完整的预处理流水线,可直接应用于历史成本数据集。

# 完整数据预处理流水线
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import RobustScaler
from sklearn.impute import KNNImputer

def preprocess_cost_pipeline(df, target_col='total_cost', test_size=0.2):
    """
    历史成本数据预处理流水线
    步骤:缺失填充 → 异常截尾 → Robust 缩放
    """
    # 1. 分离特征和目标
    X = df.drop(columns=[target_col])
    y = df[target_col]

    # 2. 拆分训练/测试集
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=test_size, random_state=42
    )

    # 3. 缺失值填充(KNN)
    imputer = KNNImputer(n_neighbors=5)
    X_train_imp = pd.DataFrame(
        imputer.fit_transform(X_train),
        columns=X_train.columns
    )
    X_test_imp = pd.DataFrame(
        imputer.transform(X_test),
        columns=X_test.columns
    )

    # 4. 异常值截尾(Winsorize 1% 和 99% 分位数)
    for col in X_train_imp.select_dtypes(include=[np.number]).columns:
        lower = X_train_imp[col].quantile(0.01)
        upper = X_train_imp[col].quantile(0.99)
        X_train_imp[col] = X_train_imp[col].clip(lower, upper)
        X_test_imp[col] = X_test_imp[col].clip(lower, upper)

    # 5. Robust 缩放
    scaler = RobustScaler()
    X_train_scaled = pd.DataFrame(
        scaler.fit_transform(X_train_imp),
        columns=X_train_imp.columns
    )
    X_test_scaled = pd.DataFrame(
        scaler.transform(X_test_imp),
        columns=X_test_imp.columns
    )

    print("预处理完成!")
    print(f"训练集形状: {X_train_scaled.shape}")
    print(f"测试集形状: {X_test_scaled.shape}")

    return X_train_scaled, X_test_scaled, y_train, y_test, scaler

# 执行流水线
X_train, X_test, y_train, y_test, scaler = preprocess_cost_pipeline(df)
本章要点回顾:
  • 缺失值处理需先诊断缺失机制,MCAR 可删除,MAR 推荐多重插补,MNAR 需领域知识介入。
  • 异常值检测应结合统计方法(Z-Score/IQR)和模型方法(孤立森林),并经过领域验证后再处理。
  • 成本数据因含异常值,推荐使用 RobustScaler 进行标准化,避免异常值对缩放参数的干扰。
  • 所有预处理步骤的参数必须从训练集学习,再应用到测试集,防止数据泄露。