4. 数据采集与预处理

在构建机器学习驱动的成本估算模型之前,必须确保输入数据的质量。成本历史数据通常来源于多个异构系统,存在格式不统一、字段缺失、记录异常等问题。本章节系统阐述从数据采集到预处理的全流程,为后续特征工程与模型训练奠定可靠基础。

4.1 成本历史数据的主要来源

成本数据的采集渠道决定了数据的初始质量与覆盖范围。下表总结了典型的数据源及其特点:

数据源类型 典型系统/文件 数据特点 采集频率
企业资源计划(ERP) SAP、Oracle EBS 结构化、字段完整、含物料与工时成本 实时/每日
项目管理工具 Jira、MS Project 含任务级预算与实际工时,但成本字段可能缺失 每日/每周
财务核算系统 用友、金蝶 精确到科目级,但缺乏项目维度关联 每月
供应商报价单 Excel、PDF 非结构化、格式多变、需人工解析 按需
历史项目档案 数据库备份、遗留系统 字段定义陈旧、编码不统一、存在大量空值 一次性/定期
实践建议:优先从 ERP 和项目管理工具中抽取结构化数据作为主数据集,将财务核算数据作为校准基准,供应商报价单作为补充特征。避免一开始就混合所有来源,否则数据清洗成本会急剧上升。

4.2 数据清洗:标准化与格式统一

原始数据进入预处理管道后,首先需要执行清洗操作,解决以下典型问题:

  • 字段命名统一:将不同系统中的“项目编号”、“Proj_ID”、“ProjectCode”统一为 project_id。
  • 数据类型转换:确保成本金额字段为数值型(float),日期字段为标准 datetime 格式。
  • 单位归一化:工时统一为“人天”,货币统一为“人民币元”或“美元”,避免因单位差异导致模型偏差。
  • 编码映射:将不同系统中的部门编码、物料编码通过映射表转换为统一编码体系。
# 数据清洗示例:字段标准化与类型转换
import pandas as pd

def clean_cost_data(df):
    # 统一列名
    df.rename(columns={
        'Proj_ID': 'project_id',
        'Actual_Cost': 'actual_cost',
        'Plan_Hours': 'planned_hours'
    }, inplace=True)

    # 转换数据类型
    df['actual_cost'] = pd.to_numeric(df['actual_cost'], errors='coerce')
    df['planned_hours'] = pd.to_numeric(df['planned_hours'], errors='coerce')
    df['start_date'] = pd.to_datetime(df['start_date'], errors='coerce')

    # 单位归一化:假设工时原始为小时,转换为天(8小时/天)
    df['planned_days'] = df['planned_hours'] / 8.0

    return df

4.3 缺失值处理策略

成本历史数据中的缺失值不可简单删除,因为缺失本身可能隐含信息(如未记录的成本项)。处理策略需根据缺失机制与业务含义选择:

缺失类型 业务场景示例 推荐处理方法
完全随机缺失(MCAR) 某项目因系统故障未记录工时 删除该记录(若缺失比例 < 5%)或均值填充
随机缺失(MAR) 外包项目缺少内部工时数据 使用同类项目的均值/中位数填充,或建立回归模型预测
非随机缺失(MNAR) 成本超支项目故意未填写预算 单独标记缺失状态作为特征,或使用多重插补法
避坑提示:不要对所有缺失字段统一使用“均值填充”。例如,对于“外包成本”字段,若缺失往往意味着项目未使用外包,则应将缺失值填充为 0 而非均值。务必结合业务逻辑判断。
# 缺失值处理示例:结合业务逻辑的填充策略
def handle_missing_values(df):
    # 对于'outsourcing_cost',缺失表示未外包,填充0
    df['outsourcing_cost'] = df['outsourcing_cost'].fillna(0)

    # 对于'actual_labor_hours',使用同类项目(按项目类型分组)的中位数填充
    df['actual_labor_hours'] = df.groupby('project_type')['actual_labor_hours']\
                                  .transform(lambda x: x.fillna(x.median()))

    # 对于'budget_amount',若缺失比例较高,新增'budget_missing'标志列
    df['budget_missing'] = df['budget_amount'].isna().astype(int)
    df['budget_amount'] = df['budget_amount'].fillna(df['budget_amount'].median())

    return df

4.4 异常值检测与处理

成本数据中的异常值可能由录入错误、系统故障或真实的极端项目(如灾难恢复)引起。检测方法需兼顾统计稳健性与业务可解释性。

4.4.1 常用检测方法对比

方法 适用场景 优点 缺点
Z-Score 法 近似正态分布的数据 计算简单、阈值明确(通常 |Z| > 3) 对非正态分布敏感,受极端值影响大
IQR 四分位距法 偏态分布、含离群点的数据 稳健、不受极端值影响 阈值(1.5×IQR)较固定,可能漏检
孤立森林(Isolation Forest) 高维数据、非线性关系 无分布假设、可处理多特征 结果可解释性较差
DBSCAN 聚类 密度分布不均匀的数据 可发现局部异常点 参数(eps, min_samples)调优困难

4.4.2 异常值处理流程

检测到异常值后,不应直接删除,而应遵循以下决策流程:

  1. 业务复核:将疑似异常值提交给成本分析师或项目经理确认是否为真实极端值。
  2. 修正:若为录入错误(如多写一个零),直接修正为正确值。
  3. 截尾处理:若为真实但罕见的极端值(如紧急项目成本为正常值的 5 倍),可进行 Winsorize 截尾(如将超出 99% 分位数的值替换为 99% 分位数)。
  4. 保留并标记:若异常值具有业务意义(如“灾备项目”),则保留原值并新增 is_extreme 标志列供模型学习。
关键原则:异常值处理的核心不是“消除异常”,而是“区分噪声与信号”。在成本估算场景中,极端项目往往是最需要模型准确预测的,盲目删除会严重削弱模型的泛化能力。
# 异常值检测与处理示例:IQR 法与业务标记
def detect_and_handle_outliers(df, column='actual_cost'):
    Q1 = df[column].quantile(0.25)
    Q3 = df[column].quantile(0.75)
    IQR = Q3 - Q1
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR

    # 标记异常值
    df['is_cost_outlier'] = ((df[column] < lower_bound) | (df[column] > upper_bound)).astype(int)

    # 对异常值进行截尾处理(Winsorize)
    df[column] = df[column].clip(lower=lower_bound, upper=upper_bound)

    return df

4.5 数据预处理整体流程

下图展示了从原始数据采集到可供模型使用的干净数据集的完整预处理管道:

成本历史数据预处理管道 ERP 系统 项目管理工具 财务核算系统 供应商报价单 数据清洗:字段统一 · 类型转换 · 单位归一化 缺失值处理:均值/中位数填充 · 标记缺失 · 多重插补 异常值检测:IQR · Z-Score · 孤立森林 · 业务复核
流程要点:预处理管道应设计为可重复执行的脚本,每次新增数据源或调整清洗规则时,只需修改配置参数,无需重写整个流程。建议使用 Apache Airflow 或 Prefect 编排每日/每周的自动预处理任务。

4.6 数据质量评估指标

在完成预处理后,应使用量化指标评估数据质量,确保达到模型训练的最低要求:

指标 计算公式 合格标准
完整率 非空记录数 / 总记录数 ≥ 95%
一致率 符合编码映射规则的记录数 / 总记录数 ≥ 98%
异常率 标记为异常值的记录数 / 总记录数 ≤ 5%(超过需复核数据源)
时效性 最近一次数据更新的时间差 ≤ 7 天(在线模型)或 ≤ 30 天(离线模型)

通过上述系统化的数据采集与预处理流程,可以构建一个高质量、高一致性的成本历史数据集,为后续的特征工程与模型训练提供坚实的数据底座。在实际项目中,建议将预处理逻辑封装为独立的 Python 包,并配合单元测试与数据质量监控仪表盘,实现数据管道的持续交付与运维。