3. 数据清洗与预处理:处理缺失值、异常值检测与处理、数据标准化与归一化
在历史项目成本估算中,原始数据往往来自多个异构系统(如 ERP、项目管理软件、手工台账),存在大量缺失、异常和量纲不一致的问题。本章聚焦于数据清洗与预处理的三大核心环节,确保后续特征工程和参数调优建立在干净、可靠的数据基础上。
3.1 缺失值处理策略
缺失值在历史成本数据中极为常见,原因包括:字段未强制录入、系统迁移丢失、特定项目阶段未采集等。处理缺失值需根据缺失机制和数据特性选择合适方法。
| 缺失机制 | 定义 | 成本数据示例 | 推荐处理方式 |
|---|---|---|---|
| 完全随机缺失 (MCAR) | 缺失与任何变量无关 | 某项目人工工时因系统故障随机丢失 | 删除或均值/中位数填充 |
| 随机缺失 (MAR) | 缺失与其他观测变量相关 | 大型项目更易缺失材料成本明细 | 回归填充、KNN 填充、多重插补 |
| 非随机缺失 (MNAR) | 缺失与未观测到的因素相关 | 亏损项目故意不填报实际成本 | 需领域知识建模,或标记为特殊类别 |
实战建议:在成本估算场景中,若某特征缺失率超过 60%,优先考虑删除该特征;若缺失率在 20%-60% 之间,使用多重插补(MICE)或迭代决策树填充;低于 20% 可采用中位数或 KNN 填充。切勿对 MNAR 数据直接填充,否则会引入系统性偏差。
缺失值处理流程
- 缺失诊断:使用
isnull().sum() / len(df)计算每列缺失率,绘制缺失热力图。 - 删除策略:按行删除(listwise deletion)适用于 MCAR 且样本充足;按列删除适用于高缺失率特征。
- 统计填充:数值型用中位数(对异常值鲁棒),分类型用众数。
- 模型填充:使用 KNN、随机森林或线性回归预测缺失值。
- 标记法:新增布尔列标记是否缺失,保留缺失信息。
# 缺失值处理示例(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;对偏态分布使用 IQR 法。
- 模型检测:使用孤立森林(Isolation Forest)或 LOF(Local Outlier Factor)检测高维异常。
- 领域验证:将检测出的异常值交由成本工程师确认是否为真实异常。
- 处理执行:根据异常类型选择删除、截尾或转换。
# 异常值检测与处理示例
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))
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 进行标准化,避免异常值对缩放参数的干扰。
- 所有预处理步骤的参数必须从训练集学习,再应用到测试集,防止数据泄露。