4. 数据采集与清洗
供应商报价趋势分析的可靠性,直接取决于原始数据的质量。在采购管理平台中,报价数据通常以结构化表格形式存储,但导出后常面临字段缺失、格式不一致、异常跳变等问题。本章将系统讲解从数据导出到清洗完成的完整流程,确保后续分析建立在干净、可信的数据基础之上。
4.1 数据导出规范与字段清单
从采购管理平台导出报价数据时,建议选择 CSV 或 XLSX 格式,并确保包含以下核心字段。字段缺失或不完整将直接影响趋势分析的准确性。
| 字段名称 | 数据类型 | 说明 | 是否必选 |
|---|---|---|---|
| 报价单号 | 字符串 | 唯一标识一次报价 | 是 |
| 供应商编码 | 字符串 | 供应商唯一ID | 是 |
| 物料编码 | 字符串 | 被报价物料的唯一ID | 是 |
| 报价日期 | 日期 | 报价提交时间 | 是 |
| 含税单价 | 浮点数 | 单位价格(含税) | 是 |
| 报价数量 | 整数 | 该次报价对应的数量 | 否 |
| 币种 | 字符串 | 如 CNY、USD | 是 |
| 有效期至 | 日期 | 报价失效日期 | 否 |
导出建议:在平台中设置导出模板时,勾选“包含历史版本”选项,避免只导出最新报价而丢失趋势分析所需的时间序列数据。
4.2 缺失值处理策略
缺失值是数据清洗中最常见的问题。针对不同字段的缺失原因,应采取差异化的处理策略,而非简单删除。
- 删除记录:当缺失字段为必选字段(如报价单号、供应商编码)时,直接删除该条记录。
- 均值/中位数填充:对于含税单价等数值字段,若缺失比例低于5%,可使用同一物料近三次报价的中位数填充。
- 向前填充:对于报价日期字段,若同一供应商连续多条记录仅日期缺失,可使用前一条有效日期+1天填充。
- 标记保留:对于非关键字段(如报价数量),可填充为0或-1,并在分析时做排除标记。
# Python 示例:缺失值处理
import pandas as pd
import numpy as np
# 加载数据
df = pd.read_csv('quote_data.csv')
# 删除必选字段缺失的记录
df.dropna(subset=['报价单号', '供应商编码', '物料编码'], inplace=True)
# 按物料分组,用中位数填充含税单价
df['含税单价'] = df.groupby('物料编码')['含税单价'].transform(
lambda x: x.fillna(x.median())
)
# 向前填充报价日期
df['报价日期'] = df['报价日期'].fillna(method='ffill')
# 非关键字段填充标记值
df['报价数量'] = df['报价数量'].fillna(-1)
注意:均值填充会降低数据方差,在趋势分析中可能导致价格波动被平滑。建议在填充后对比填充前后的标准差变化,若偏差超过10%,改用更保守的填充策略。
4.3 异常值检测与修正
异常值通常由录入错误、单位换算错误或系统故障引起。在供应商报价趋势分析中,异常值会扭曲趋势线,导致错误的采购决策。以下是三种常用的检测方法。
4.3.1 基于统计分布的检测(Z-Score)
假设报价数据近似正态分布,计算每个报价的 Z-Score,值大于3的视为异常值。
from scipy import stats
# 计算 Z-Score
df['z_score'] = np.abs(stats.zscore(df['含税单价']))
# 标记异常值
df['is_outlier'] = df['z_score'] > 3
# 查看异常值
outliers = df[df['is_outlier'] == True]
print(f"检测到 {len(outliers)} 个异常值")
4.3.2 基于四分位距的检测(IQR)
对于非正态分布的数据,IQR 方法更为稳健。将低于 Q1-1.5×IQR 或高于 Q3+1.5×IQR 的值视为异常。
# IQR 方法
Q1 = df['含税单价'].quantile(0.25)
Q3 = df['含税单价'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
df['is_outlier_iqr'] = (df['含税单价'] < lower_bound) | (df['含税单价'] > upper_bound)
4.3.3 基于时间序列的上下文检测
在趋势分析中,同一供应商对同一物料的报价应具有时间连续性。若某次报价较前一次变化超过50%,且无合理备注,则标记为上下文异常。
# 按供应商+物料分组,计算环比变化率
df['price_change_pct'] = df.groupby(['供应商编码', '物料编码'])['含税单价'].pct_change()
# 标记环比变化超过50%的记录
df['is_context_outlier'] = df['price_change_pct'].abs() > 0.5
修正原则:对于检测到的异常值,不要直接删除。首先回溯原始单据确认;若确为录入错误,使用该供应商最近三次有效报价的中位数替换;若无法确认,保留并标记为“可疑”,在趋势图中以不同颜色显示。
4.4 数据标准化与格式统一
不同供应商或不同时期导出的数据,可能存在单位、币种、日期格式不一致的问题。标准化是趋势分析前的最后一道工序。
- 日期统一:将所有日期字段转换为 YYYY-MM-DD 格式。
- 币种转换:按导出当日的汇率,将所有报价统一转换为分析基准币种(通常为人民币)。
- 单位归一:检查同一物料的报价单位(如“个”“件”“千克”),若不一致则按换算系数统一。
- 去重:基于报价单号+物料编码去重,保留最新一条记录。
# 数据标准化示例
# 日期统一
df['报价日期'] = pd.to_datetime(df['报价日期'], format='%Y-%m-%d')
# 币种转换(假设汇率表已加载)
exchange_rate = {'USD': 7.2, 'EUR': 7.8, 'CNY': 1.0}
df['含税单价_CNY'] = df.apply(
lambda row: row['含税单价'] * exchange_rate.get(row['币种'], 1), axis=1
)
# 去重
df.drop_duplicates(subset=['报价单号', '物料编码'], keep='last', inplace=True)
效率提示:建议将清洗流程封装为可复用的 Python 函数或 SQL 存储过程。每次从平台导出新数据后,只需运行一次清洗脚本,即可获得可直接用于趋势分析的标准数据集。
4.5 数据质量检查清单
完成清洗后,建议使用以下清单进行最终验证,确保数据已准备好进入趋势分析阶段。
| 检查项 | 检查方法 | 合格标准 |
|---|---|---|
| 缺失率 | 计算每个字段的缺失比例 | 所有字段缺失率 < 1% |
| 异常值比例 | 统计被标记为异常的记录数 | 异常值比例 < 3% |
| 日期连续性 | 按供应商+物料分组,检查日期是否递增 | 无日期倒挂或跳跃超过90天 |
| 币种统一性 | 检查基准币种字段是否唯一 | 100% 统一为目标币种 |
| 去重率 | 对比去重前后的记录数 | 去重后记录数减少 < 5% |
最佳实践:将清洗后的数据保存为独立副本(如 clean_quote_data.csv),与原始数据分开存储。每次分析时都从清洗后的数据开始,避免重复清洗。同时保留清洗日志,记录每一步的操作和参数,便于审计和回溯。