4. 数据采集与清洗
数据采集与清洗是越权采购排查的基石。原始数据通常分散在ERP、OA、SRM等多个异构系统中,存在缺失、重复、格式不统一等问题。本节将系统讲解如何通过Python实现自动化数据采集与清洗,确保后续分析的准确性和可追溯性。
4.1 数据采集:多源异构系统连接
越权采购分析需要整合采购订单、审批记录、用户权限、组织架构四类核心数据。Python通过sqlalchemy和pyodbc驱动实现统一连接。
核心连接策略: 使用SQLAlchemy ORM封装不同数据库方言,通过连接字符串参数隔离环境差异;生产环境建议使用连接池(
pool_size=5)避免频繁握手。
from sqlalchemy import create_engine, text
import pandas as pd
# ERP系统(SQL Server)
erp_engine = create_engine(
'mssql+pyodbc://user:pass@erp_host:1433/erp_db?driver=ODBC+Driver+17+for+SQL+Server',
pool_size=5, pool_recycle=3600
)
# OA系统(MySQL)
oa_engine = create_engine(
'mysql+pymysql://oa_user:oa_pass@oa_host:3306/oa_db',
pool_size=3
)
# 执行查询
po_query = """
SELECT PO_NUMBER, CREATED_DATE, APPROVER_ID, AMOUNT, STATUS
FROM PURCHASE_ORDERS
WHERE CREATED_DATE >= '2024-01-01'
"""
df_po = pd.read_sql_query(text(po_query), erp_engine)
| 数据源 | 系统类型 | 关键字段 | 连接驱动 |
|---|---|---|---|
| 采购订单 | ERP (SQL Server) | PO编号、创建日期、审批人、金额 | pyodbc + SQLAlchemy |
| 审批记录 | OA (MySQL) | 流程ID、操作人、操作时间、审批意见 | pymysql |
| 用户权限 | AD/LDAP | 用户ID、角色、部门、审批额度 | python-ldap |
| 组织架构 | HR系统 (PostgreSQL) | 员工ID、上级主管、职级、成本中心 | psycopg2 |
4.2 缺失值处理:分级策略与业务规则
缺失值不能简单删除或填充,必须结合业务含义分级处理。越权采购场景中,审批人ID缺失和金额缺失的处置逻辑完全不同。
业务规则优先原则: 审批人ID缺失 → 标记为“疑似越权”并进入人工复核;金额缺失 → 从关联的采购明细表回填;部门缺失 → 通过组织架构表左连接补全。
# 分级缺失值处理
def handle_missing_values(df):
# 1级:关键字段缺失 → 标记异常
df['approver_missing'] = df['APPROVER_ID'].isna()
# 2级:金额缺失 → 从明细表回填
amount_missing = df['AMOUNT'].isna()
if amount_missing.any():
detail_amounts = df_detail.groupby('PO_NUMBER')['LINE_AMOUNT'].sum()
df.loc[amount_missing, 'AMOUNT'] = df.loc[amount_missing, 'PO_NUMBER'].map(detail_amounts)
# 3级:部门缺失 → 左连接补全
df = df.merge(df_org[['EMP_ID', 'DEPT']], on='EMP_ID', how='left', suffixes=('', '_org'))
df['DEPT'] = df['DEPT'].fillna(df['DEPT_org'])
df.drop(columns=['DEPT_org'], inplace=True)
# 记录缺失统计
missing_stats = {
'approver_missing': df['approver_missing'].sum(),
'amount_filled': amount_missing.sum(),
'dept_filled': df['DEPT'].isna().sum() # 仍缺失的部门
}
return df, missing_stats
| 缺失字段 | 缺失等级 | 处理策略 | 对越权分析的影响 |
|---|---|---|---|
| 审批人ID | 严重 | 标记异常,人工复核 | 直接触发越权告警 |
| 采购金额 | 中等 | 从明细行汇总回填 | 影响金额阈值判断 |
| 部门/成本中心 | 轻微 | 左连接组织架构表 | 影响权限归属判定 |
| 创建日期 | 严重 | 从审批日志中提取 | 影响时间序列分析 |
4.3 统一日期格式:时区归一化与业务周期对齐
不同系统输出的日期格式差异巨大:ERP使用2024-01-15 14:30:00,OA使用2024/01/15 14:30,部分旧系统甚至使用15-JAN-2024。统一日期格式是时间序列分析的前提。
import pandas as pd
def unify_date_format(df, date_columns):
"""
统一日期格式为 YYYY-MM-DD HH:MM:SS,时区统一为 UTC+8
"""
for col in date_columns:
# 尝试多种常见格式
df[col] = pd.to_datetime(df[col], errors='coerce',
format='mixed', dayfirst=False)
# 时区处理:统一转为 UTC+8
if df[col].dt.tz is None:
df[col] = df[col].dt.tz_localize('Asia/Shanghai', ambiguous='infer')
else:
df[col] = df[col].dt.tz_convert('Asia/Shanghai')
# 提取业务周期字段
df[f'{col}_year'] = df[col].dt.year
df[f'{col}_month'] = df[col].dt.month
df[f'{col}_week'] = df[col].dt.isocalendar().week.astype(int)
df[f'{col}_hour'] = df[col].dt.hour
return df
# 应用
date_cols = ['CREATED_DATE', 'APPROVED_DATE', 'DELIVERY_DATE']
df = unify_date_format(df, date_cols)
常见陷阱: ① 混合格式时使用
format='mixed' 参数,避免解析失败;② 时区处理必须在去重之前完成,否则同一订单因时区差异可能被误判为不同记录;③ 业务周期字段(年/月/周)单独提取,便于后续按周期聚合分析。
4.4 去重逻辑:基于业务语义的精确去重
越权采购场景中,去重不能仅依赖主键。同一笔采购可能因系统重试、接口超时、人工补录产生多条记录。需要设计业务主键 + 时间窗口的复合去重策略。
def dedup_purchase_orders(df, time_window_minutes=5):
"""
复合去重:业务主键 + 时间窗口
"""
# 步骤1:按业务主键去重,保留最早记录
df_sorted = df.sort_values('CREATED_DATE')
df_dedup = df_sorted.drop_duplicates(
subset=['PO_NUMBER', 'APPROVER_ID', 'AMOUNT'],
keep='first'
)
# 步骤2:时间窗口去重(同一PO在5分钟内多次提交)
df_dedup['time_diff'] = df_dedup.groupby('PO_NUMBER')['CREATED_DATE'].diff()
df_dedup['is_duplicate_window'] = (
df_dedup['time_diff'].notna() &
(df_dedup['time_diff'].dt.total_seconds() < time_window_minutes * 60)
)
# 保留时间窗口内的第一条
df_final = df_dedup[~df_dedup['is_duplicate_window']].copy()
# 步骤3:标记疑似重复(供人工复核)
df_final['duplicate_flag'] = '正常'
df_final.loc[df_dedup['is_duplicate_window'], 'duplicate_flag'] = '疑似重复'
# 统计
stats = {
'原始记录数': len(df),
'业务主键去重后': len(df_dedup),
'时间窗口去重后': len(df_final),
'疑似重复数': df_final['duplicate_flag'].eq('疑似重复').sum()
}
return df_final, stats
4.5 数据清洗流水线:端到端执行
将上述步骤封装为可复用的清洗流水线,确保每次数据刷新时执行一致的清洗逻辑。
class DataCleaningPipeline:
"""采购数据清洗流水线"""
def __init__(self, config):
self.config = config
self.stats = {}
def run(self, raw_data_dict):
# 1. 数据采集
df_po = self._extract_erp(raw_data_dict['erp'])
df_oa = self._extract_oa(raw_data_dict['oa'])
df_ad = self._extract_ad(raw_data_dict['ad'])
df_hr = self._extract_hr(raw_data_dict['hr'])
# 2. 合并数据集
df = self._merge_datasets(df_po, df_oa, df_ad, df_hr)
# 3. 缺失值处理
df, self.stats['missing'] = handle_missing_values(df)
# 4. 统一日期格式
df = unify_date_format(df, self.config['date_columns'])
# 5. 去重
df, self.stats['dedup'] = dedup_purchase_orders(df)
# 6. 数据质量报告
self._generate_quality_report(df)
return df
def _generate_quality_report(self, df):
"""生成数据质量报告"""
report = {
'总记录数': len(df),
'缺失审批人': df['approver_missing'].sum(),
'重复标记数': df['duplicate_flag'].eq('疑似重复').sum(),
'日期范围': f"{df['CREATED_DATE'].min()} ~ {df['CREATED_DATE'].max()}",
'涉及PO数': df['PO_NUMBER'].nunique()
}
self.stats['quality'] = report
return report
流水线执行效果(某企业2024年Q1数据): 原始记录 128,473 条 → 业务主键去重后 126,891 条 → 时间窗口去重后 126,544 条 → 最终有效记录 126,544 条,去重率 1.5%;其中标记疑似重复 347 条,经人工复核确认 89 条为真实重复(系统重试导致),其余为业务正常补录。
4.6 数据质量监控看板
清洗完成后,需要建立持续的数据质量监控机制,确保每次数据刷新后质量可控。
| 监控指标 | 阈值 | 告警级别 | 触发动作 |
|---|---|---|---|
| 缺失审批人占比 | > 0.5% | 严重 | 暂停分析,通知IT排查 |
| 日期解析失败率 | > 1% | 警告 | 记录异常日志,人工复核 |
| 去重率异常波动 | 超过历史均值±3σ | 提示 | 检查数据源是否变更 |
| 金额回填成功率 | < 95% | 警告 | 检查明细表关联完整性 |
通过上述数据采集与清洗流程,我们构建了高质量、可追溯的采购数据集,为后续的越权规则计算和异常检测奠定了坚实的数据基础。清洗后的数据将进入下一阶段的特征工程与规则引擎处理。