4. 数据采集与清洗:采购订单数据源与清洗规则

高频采购商品识别的准确性,完全取决于底层数据的质量。ERP/SCM 系统中的原始采购订单数据往往存在重复、缺失、命名混乱等问题,必须经过严格的采集与清洗,才能用于后续的支出分析与商品聚类。

4.1 数据源:ERP/SCM 核心表与字段

采购订单数据通常分布在 ERP 或 SCM 系统的多个关联表中。以下为必须采集的核心字段及其业务含义:

数据表/视图 关键字段 业务含义 采集优先级
采购订单头表 (PO_HEADER) PO_NUMBER, PO_DATE, BUYER, VENDOR_CODE 订单唯一编号、下单日期、采购员、供应商编码 强制
采购订单行项表 (PO_LINE) PO_NUMBER, LINE_NUM, ITEM_CODE, QUANTITY, UNIT_PRICE, TAX_AMOUNT 商品编码、采购数量、单价、税额 强制
物料主数据表 (MATERIAL_MASTER) ITEM_CODE, ITEM_NAME, MATERIAL_GROUP, UNIT_OF_MEASURE 商品名称、物料组、计量单位 强制
供应商主数据表 (VENDOR_MASTER) VENDOR_CODE, VENDOR_NAME, COUNTRY, PAYMENT_TERMS 供应商名称、国家、付款条件 推荐
收货/发票匹配表 (GR_IR) PO_NUMBER, LINE_NUM, RECEIVED_QTY, INVOICE_QTY 已收货数量、已开票数量(用于校验) 可选
数据采集建议:优先通过 ERP 系统的标准 API 或数据库只读视图抽取近 24 个月的历史订单数据。避免直接连接生产库,防止对业务系统造成性能影响。

4.2 数据清洗三大规则

原始数据必须经过以下三个核心步骤的清洗,才能进入分析管道:

4.2.1 去重(Deduplication)

由于系统接口重传、人工多次保存等原因,同一笔订单可能在数据集中出现多次。去重规则如下:

  • 精确去重:以 PO_NUMBER + LINE_NUM 为唯一键,保留最新一条记录(按最后修改时间排序)。
  • 模糊去重:对于同一供应商、同一商品、同一日期、金额完全相同的订单,视为重复,仅保留一条。

4.2.2 异常值处理(Outlier Handling)

异常值会严重扭曲高频商品的统计结果,必须识别并标记:

  • 单价异常:单价为 0 或负数(可能是赠品或录入错误);单价超过该商品历史均价 ±3 倍标准差。
  • 数量异常:采购数量为 0 或负数;单笔数量超过该商品月均采购量的 10 倍。
  • 日期异常:订单日期早于系统上线日期或晚于当前日期。

处理策略:对于异常值,先标记为“待人工审核”,不直接删除;在分析阶段可设置过滤条件排除异常行。

4.2.3 标准化商品名称(Standardization)

同一商品在不同采购员、不同时期可能使用不同名称,必须统一:

  • 去除冗余信息:删除商品名称中的括号备注、批次号、特殊符号(如 ★、※)。
  • 统一单位与规格:将“个”、“只”、“PCS”统一为“个”;将“KG”、“千克”统一为“kg”。
  • 同义词映射:建立同义词库,如“笔记本电脑”与“笔记本”、“Notebook”映射为同一标准名称。
  • 正则替换:使用正则表达式将“A4纸 70g 500张/包”标准化为“A4复印纸 70g”。
清洗效果验证:清洗后,同一商品名称的唯一值数量应下降 40%~60%。例如,清洗前“A4纸”相关名称有 120 个变体,清洗后应收敛至 5~8 个标准名称。

4.3 数据清洗流程架构

以下 SVG 图展示了从数据源到清洗后数据集的完整流程,包含三个清洗节点的输入与输出:

采购订单数据清洗流程架构 采购订单数据清洗流程架构 ERP 订单头表 ERP 订单行项表 物料主数据表 原始订单数据集 规则1:去重 PO_NUMBER+LINE_NUM 规则2:异常值 单价/数量/日期 规则3:标准化 商品名称/单位 清洗后数据集 用于高频商品分析 清洗规则执行顺序说明: 1. 先执行去重,避免重复记录影响统计基数。 2. 再执行异常值检测与标记,防止极端值扭曲高频排名。 3. 最后执行标准化,确保同一商品名称唯一,便于后续聚类与汇总。

4.4 清洗规则执行脚本示例(Python/Pandas)

以下代码片段展示了使用 Python 的 Pandas 库执行上述清洗规则的核心逻辑:

import pandas as pd
import numpy as np

# 加载原始数据
po_header = pd.read_sql("SELECT * FROM po_header", conn)
po_line = pd.read_sql("SELECT * FROM po_line", conn)
material = pd.read_sql("SELECT * FROM material_master", conn)

# 合并为宽表
df = po_header.merge(po_line, on='PO_NUMBER', how='inner')
df = df.merge(material, on='ITEM_CODE', how='left')

# 规则1:去重(保留最后修改时间最新的记录)
df = df.sort_values('LAST_MODIFIED_DATE', ascending=False)
df = df.drop_duplicates(subset=['PO_NUMBER', 'LINE_NUM'], keep='first')

# 规则2:异常值标记(不删除,新增标记列)
df['IS_ANOMALY'] = 0
df.loc[df['UNIT_PRICE'] <= 0, 'IS_ANOMALY'] = 1
df.loc[df['QUANTITY'] <= 0, 'IS_ANOMALY'] = 1
# 单价超过历史均值±3倍标准差
mean_price = df['UNIT_PRICE'].mean()
std_price = df['UNIT_PRICE'].std()
df.loc[(df['UNIT_PRICE'] > mean_price + 3*std_price) | 
       (df['UNIT_PRICE'] < mean_price - 3*std_price), 'IS_ANOMALY'] = 1

# 规则3:标准化商品名称
def standardize_name(name):
    name = str(name).strip()
    # 去除括号及括号内内容
    name = re.sub(r'[((][^))]*[))]', '', name)
    # 统一单位
    name = name.replace('PCS', '个').replace('pcs', '个')
    name = name.replace('KG', 'kg').replace('千克', 'kg')
    # 去除特殊符号
    name = re.sub(r'[★※◆◇]', '', name)
    return name.strip()

df['STANDARD_NAME'] = df['ITEM_NAME'].apply(standardize_name)

# 输出清洗后数据集
df_clean = df[df['IS_ANOMALY'] == 0]  # 分析时过滤异常行
print(f"清洗前记录数: {len(df)}, 清洗后记录数: {len(df_clean)}")
常见避坑:不要在清洗阶段直接删除异常值,而是标记后保留。因为某些“异常”可能是真实的高价值采购(如设备备件),直接删除会导致支出分析失真。建议在分析阶段提供“包含异常”与“排除异常”两种视图。

4.5 清洗质量检查清单

完成清洗后,建议按以下清单逐项验证数据质量:

  • 完整性:关键字段(PO_NUMBER、ITEM_CODE、QUANTITY、UNIT_PRICE)缺失率是否低于 1%?
  • 唯一性:去重后,同一 PO_NUMBER + LINE_NUM 是否仅出现一次?
  • 一致性:同一 STANDARD_NAME 对应的物料组(MATERIAL_GROUP)是否一致?
  • 时效性:订单日期是否全部在合理时间范围内(如近 24 个月)?
  • 准确性:随机抽取 50 条记录,人工核对原始系统数据,准确率是否达到 100%?