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 图展示了从数据源到清洗后数据集的完整流程,包含三个清洗节点的输入与输出:
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%?