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
修正原则:对于检测到的异常值,不要直接删除。首先回溯原始单据确认;若确为录入错误,使用该供应商最近三次有效报价的中位数替换;若无法确认,保留并标记为“可疑”,在趋势图中以不同颜色显示。
数据清洗与异常检测流程 原始报价数据 缺失值处理 异常值检测 Z-Score 统计法 IQR 四分位法 时间序列上下文法 清洗后的干净数据

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 存储过程。每次从平台导出新数据后,只需运行一次清洗脚本,即可获得可直接用于趋势分析的标准数据集。
数据标准化前后对比 清洗前(混乱数据) 报价日期: 2024/1/5, 2024-01-10, 01/15/2024 币种: USD, CNY, EUR 混合 单价: 100 USD, 720 CNY, 780 EUR 单位: 个, PCS, 件 混用 存在重复报价单号 缺失值: 部分无报价数量 清洗后(标准数据) 报价日期: 2024-01-05, 2024-01-10, 2024-01-15 币种: 全部统一为 CNY 单价: 720, 720, 780 (已换算) 单位: 统一为“个” 去重完成,无重复记录 缺失值已填充或标记

4.5 数据质量检查清单

完成清洗后,建议使用以下清单进行最终验证,确保数据已准备好进入趋势分析阶段。

检查项 检查方法 合格标准
缺失率 计算每个字段的缺失比例 所有字段缺失率 < 1%
异常值比例 统计被标记为异常的记录数 异常值比例 < 3%
日期连续性 按供应商+物料分组,检查日期是否递增 无日期倒挂或跳跃超过90天
币种统一性 检查基准币种字段是否唯一 100% 统一为目标币种
去重率 对比去重前后的记录数 去重后记录数减少 < 5%
最佳实践:将清洗后的数据保存为独立副本(如 clean_quote_data.csv),与原始数据分开存储。每次分析时都从清洗后的数据开始,避免重复清洗。同时保留清洗日志,记录每一步的操作和参数,便于审计和回溯。