4. 数据预处理:缺失值处理、异常值检测、数据标准化、时间序列对齐

数据预处理,说白了就是给模型「喂饭」之前,先把食材洗干净、切好、摆盘。我见过太多团队,模型选得再好,算法调得再炫,结果数据一塌糊涂,最后跑出来的预警全是噪音。

反洗钱数据尤其脏。为什么?因为数据来源多——银行核心系统、支付平台、第三方渠道,每个系统的字段定义、录入规范都不一样。你想想看,一个客户的身份证号,在A系统是18位,在B系统可能漏了最后一位,在C系统干脆填了个「无」。这种数据直接扔进模型,不出问题才怪。

我个人习惯,拿到数据后先做四件事:缺失值处理、异常值检测、数据标准化、时间序列对齐。下面一个一个说。

4.1 缺失值处理

缺失值在反洗钱数据里太常见了。比如交易对手信息、IP地址、设备指纹,这些字段经常是空的。但你不能直接删掉,因为缺失本身可能就有信息量——一个故意隐藏IP地址的交易,本身就值得警惕。

核心原则:不要盲目填充,先分析缺失原因。

我一般把缺失值分成三类:

  • 随机缺失:比如某个柜员偶尔漏填了交易金额。这种可以用均值、中位数填充。
  • 非随机缺失:比如高风险客户故意不填职业信息。这种填充反而会掩盖问题,我建议单独标记。
  • 结构性缺失:比如新接入的数据源本来就没有某个字段。这种只能做特征工程,或者干脆不用这个字段。

举个例子,处理交易金额的缺失值:

import pandas as pd
import numpy as np

# 加载交易数据
df = pd.read_csv('transactions.csv')

# 查看缺失率
missing_rate = df['amount'].isnull().mean()
print(f'金额缺失率: {missing_rate:.2%}')

# 如果缺失率低于5%,用中位数填充
if missing_rate < 0.05:
    df['amount'].fillna(df['amount'].median(), inplace=True)
else:
    # 缺失率高,说明可能有问题,先标记再处理
    df['amount_missing'] = df['amount'].isnull().astype(int)
    df['amount'].fillna(-1, inplace=True)  # 用-1占位,后续模型自己学习

我的经验:我曾经处理过一个跨境支付数据集,交易对手字段缺失率高达40%。一开始我想填充,后来发现这些缺失的交易几乎全是高风险地区的。于是我干脆把「是否缺失」作为一个特征喂给模型,效果反而更好。

4.2 异常值检测

异常值在反洗钱里就是「信号」。一笔100万的交易,如果客户平时都是几千块的流水,那这笔交易就是异常。但要注意,异常不一定是洗钱,也可能是大额消费、工资入账等正常行为。

我常用的方法有三种:

方法 适用场景 优点 缺点
Z-Score 正态分布的数据 简单快速 对非正态分布效果差
IQR(四分位距) 偏态分布的数据 不受极端值影响 需要手动设定阈值
孤立森林 高维数据 能处理复杂模式 计算量大

我个人最常用的是IQR方法,因为反洗钱数据大多是偏态的——大部分交易金额很小,少数交易金额巨大。

# IQR异常值检测
Q1 = df['amount'].quantile(0.25)
Q3 = df['amount'].quantile(0.75)
IQR = Q3 - Q1

# 定义异常阈值
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

# 标记异常
df['amount_outlier'] = ((df['amount'] < lower_bound) | 
                        (df['amount'] > upper_bound)).astype(int)

print(f'检测到异常交易: {df["amount_outlier"].sum()} 笔')

注意:不要直接删除异常值!在反洗钱场景下,异常值往往是模型最需要学习的样本。我建议先标记,然后让模型自己判断哪些是真正的洗钱行为。

4.3 数据标准化

标准化,就是把不同量纲的数据拉到同一个尺度。比如交易金额可能是几百万,而交易次数只有个位数。如果不标准化,模型会天然认为金额更重要,这显然不对。

常用的标准化方法:

  • Min-Max标准化:把数据缩放到[0,1]区间。适合有明确边界的数据,比如年龄、评分。
  • Z-Score标准化:把数据变成均值为0、标准差为1的分布。适合没有明显边界的数据,比如交易金额。
  • Robust标准化:使用中位数和四分位距,对异常值不敏感。反洗钱数据我强烈推荐这个。
from sklearn.preprocessing import RobustScaler

# 初始化标准化器
scaler = RobustScaler()

# 选择需要标准化的特征
features = ['amount', 'frequency', 'account_age']
df[features] = scaler.fit_transform(df[features])

print('标准化完成,数据分布已调整')

避坑指南:我曾经犯过一个错误——先标准化再划分训练集和测试集。结果测试集的信息泄露到了训练集里,模型评估虚高。正确的做法是:先划分数据集,再用训练集的参数去标准化测试集。

4.4 时间序列对齐

反洗钱数据天然带有时间属性。一笔交易发生在什么时候,前后有什么关联,这些信息至关重要。但不同数据源的时间粒度可能不一样——银行流水是逐笔的,而风控系统可能是按天汇总的。

时间序列对齐要解决三个问题:

  1. 时间粒度统一:把所有数据对齐到同一个时间粒度,比如按小时、按天、按周。
  2. 时区处理:跨境交易涉及不同时区,必须统一转换成UTC时间。
  3. 缺失时间点:比如某天没有交易记录,是应该填充0还是直接跳过?
# 时间序列对齐示例
df['timestamp'] = pd.to_datetime(df['timestamp'], utc=True)

# 按天重采样,填充缺失值
df_daily = df.set_index('timestamp').resample('D').agg({
    'amount': 'sum',
    'count': 'sum',
    'customer_id': 'nunique'
}).fillna(0)

print(f'对齐后数据维度: {df_daily.shape}')

核心要点:时间序列对齐不是简单的「填0」。如果某天没有交易,可能意味着客户在休息,也可能意味着客户在刻意规避监控。我建议保留缺失标记,让模型自己去学习。

嗯,到这里数据预处理的四个核心步骤就讲完了。你可能觉得这些操作很简单,但实际项目中,80%的时间都花在这些「脏活累活」上。数据预处理做得好,模型训练就是水到渠成的事。

数据预处理核心流程 原始数据 缺失值处理 填充/标记/删除 异常值检测 IQR/孤立森林 数据标准化 RobustScaler 时间序列对齐 重采样/时区统一 清洗后的数据

数据预处理没有银弹,每个数据集都有自己的脾气。我的建议是:多动手、多尝试、多复盘。你做得多了,自然就能一眼看出数据的问题在哪里。

专注资料整理