4. 数据预处理:数据清洗、缺失值处理、异常值检测与平滑

在构建行情坏账率哨兵告警系统时,原始数据通常来自交易所行情推送、风控日志、清算对账等多个异构数据源。这些数据存在格式不一致、字段缺失、极端跳变等问题。如果直接用于阈值计算,会导致告警误报率升高或漏报。本章将系统讲解数据预处理的四个核心环节:数据清洗、缺失值处理、异常值检测与平滑。

4.1 数据清洗:去重、格式统一与业务规则过滤

数据清洗是预处理的第一步,主要解决数据质量的基础问题。针对行情坏账率数据,清洗工作包含以下三个子任务:

清洗任务 常见问题 处理方法
去重 同一时间戳、同一标的出现多条记录 按时间戳+标的ID去重,保留最后一条或取均值
格式统一 时间戳格式不一致(Unix秒/毫秒/字符串) 统一转换为UTC毫秒时间戳,并校验时间连续性
业务规则过滤 坏账率超过100%或为负值 剔除明显违反业务逻辑的记录,并记录异常日志
实践建议:在清洗阶段保留原始数据副本,避免不可逆操作。建议使用dataframe.duplicated()和dataframe.describe()快速定位异常范围。
数据清洗流程 原始数据 去重 格式统一 业务过滤 清洗后数据 清洗阶段不修改数据值,只剔除或转换格式,保证数据可追溯

4.2 缺失值处理:策略选择与填充方法

行情数据在传输过程中可能因网络抖动、交易所断连等原因产生缺失。缺失值处理策略直接影响后续异常值检测的准确性。根据缺失比例和业务场景,推荐以下处理方案:

  • 直接删除:当缺失比例小于1%且缺失随机分布时,直接删除缺失记录。
  • 前向填充(Forward Fill):适用于高频行情数据,用上一时刻值填充当前缺失,保持序列连续性。
  • 线性插值:适用于坏账率等连续变化指标,利用缺失点前后有效值进行线性拟合。
  • 业务默认值填充:当坏账率缺失时,可填充为0或最近N个周期的均值,需在告警日志中标记。
注意:缺失值填充后必须记录填充标记字段,避免在阈值计算时将填充值与真实值混淆。对于连续缺失超过5个时间窗口的情况,建议直接标记为“数据不可用”状态。

4.3 异常值检测:统计方法与业务规则结合

行情坏账率数据中,异常值通常表现为瞬时跳变(如从0.5%突然跳至15%)或持续偏离正常范围。单一检测方法容易误判,建议采用“统计+规则”双层检测策略。

检测方法 适用场景 参数说明
3σ原则 数据近似正态分布 均值±3倍标准差之外视为异常
IQR四分位法 数据分布偏态明显 Q1-1.5×IQR 或 Q3+1.5×IQR 之外为异常
Z-Score 标准化后的离群点检测 |Z| > 3 视为异常
业务阈值规则 已知风控上限 坏账率 > 10% 直接标记为异常
异常值双层检测架构 清洗后数据 第一层:统计检测 3σ / IQR / Z-Score 第二层:业务规则 风控上限 / 变化率阈值 异常标记合并 异常值标记数据集

4.4 数据平滑:降低噪声保留趋势

行情坏账率数据存在高频噪声,直接使用原始值进行阈值计算会导致告警频繁抖动。平滑处理的目标是滤除短期波动,保留中长期趋势。常用的平滑方法包括:

  • 移动平均(Moving Average):取最近N个时间窗口的均值,N通常取5~20。
  • 指数加权移动平均(EWMA):对近期数据赋予更高权重,衰减因子α通常取0.1~0.3。
  • 中值滤波:取窗口内中位数,对脉冲型噪声抑制效果较好。
平滑参数调优建议:在哨兵告警系统中,建议对坏账率数据使用EWMA(α=0.15)进行平滑。该参数在抑制噪声与保持趋势灵敏度之间取得平衡。平滑后的数据用于阈值计算,原始数据保留用于异常复核。

4.5 预处理完整流程代码示例

以下代码展示了从原始数据到预处理完成的核心步骤,包含清洗、缺失填充、异常检测与平滑:

import pandas as pd
import numpy as np

# 1. 数据清洗:去重 + 格式统一
df = df.drop_duplicates(subset=['timestamp', 'symbol'])
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
df = df.sort_values('timestamp')

# 2. 缺失值处理:线性插值
df['bad_rate'] = df['bad_rate'].interpolate(method='linear', limit=3)

# 3. 异常值检测:IQR + 业务规则
Q1 = df['bad_rate'].quantile(0.25)
Q3 = df['bad_rate'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
df['is_anomaly'] = ((df['bad_rate'] < lower_bound) | 
                    (df['bad_rate'] > upper_bound) | 
                    (df['bad_rate'] > 10.0)).astype(int)

# 4. 数据平滑:EWMA
df['bad_rate_smooth'] = df['bad_rate'].ewm(alpha=0.15, adjust=False).mean()

print(f"预处理完成,总记录数:{len(df)},异常标记数:{df['is_anomaly'].sum()}")
关键点:预处理后的数据应包含原始值、平滑值、异常标记三个字段。在后续阈值调优中,平滑值用于计算基线,异常标记用于评估告警准确率。