4. 数据采集与预处理:实时数据流接入、数据清洗、特征工程基础
好,咱们进入实战环节的第一个硬骨头——数据。做风控的人都知道一句话:「数据决定上限,模型只是逼近这个上限」。我做了这么多年风控架构,见过太多团队在模型上死磕,结果回头一看,数据流断了、字段是脏的、特征算错了……全白搭。
这一章,咱们就聊聊实时风控场景下,数据是怎么进来的、怎么洗干净、怎么变成特征的。说白了,就是给后面的阈值模型喂「好饭」。
4.1 实时数据流接入:别让数据迟到
实时风控,核心就一个字——快。用户点一下「支付」,你必须在几百毫秒内给出决策。数据晚到一秒,可能钱已经没了。
我个人习惯用 Kafka 作为消息中间件。为什么?吞吐量高、延迟低、还能扛住突发流量。双十一那种峰值,Kafka 基本是标配。
这里有个坑,我踩过。数据接入时,一定要做「流量整形」。你想想看,如果一瞬间涌进来 10 万条消息,下游的 Flink 任务直接被打满,GC 频繁,延迟飙升。怎么办?
- 设置合理的分区数:Kafka 分区数 = 下游消费者并行度 × 2~3 倍,留点余量。
- 消息体不要太大:我见过有人把整个 HTTP 请求体塞进 Kafka,一个消息 1MB,直接拉爆网络。建议控制在 10KB 以内,只传关键字段。
- 做好序列化:用 Avro 或 Protobuf,别用 JSON 明文。JSON 解析慢,而且 schema 变更容易出问题。
4.2 数据清洗:脏数据比没数据更可怕
数据进来了,但你能直接用吗?不能。实时数据流里,脏数据比比皆是。我总结了几类最常见的「毒瘤」:
| 脏数据类型 | 典型表现 | 处理方式 |
|---|---|---|
| 缺失值 | 用户ID为空、金额为0 | 丢弃或填充默认值(如 -1) |
| 异常值 | 交易金额 999999999 | 设定上下限,超过则截断或标记 |
| 重复数据 | 同一笔交易被发送两次 | 基于唯一ID做去重(幂等性) |
| 时间乱序 | 事件时间比当前时间晚 1 小时 | 设置 watermark,丢弃超时数据 |
| 格式错误 | 手机号多了字母、IP 地址格式不对 | 正则校验,不合规的丢弃 |
嗯,这里要注意。清洗不是「一刀切」。比如缺失值,如果用户ID为空,这笔交易基本没法做风控,直接丢弃。但如果只是「用户年龄」为空,你可以用 -1 填充,或者用均值/中位数替代。具体怎么选?看业务容忍度。
我一般会在 Flink 里写一个 通用的清洗算子,把清洗逻辑做成配置化。这样业务方改规则,不用重启任务,热加载就行。
// 伪代码:Flink 清洗算子
DataStream<RawEvent> cleanedStream = rawStream
.filter(event -> event.getUserId() != null) // 丢弃无效用户
.filter(event -> event.getAmount() > 0) // 丢弃零金额
.map(new CleanFunction()); // 自定义清洗逻辑
// CleanFunction 内部
public RawEvent map(RawEvent event) {
// 金额截断:超过 100 万视为异常
if (event.getAmount() > 1_000_000) {
event.setAmount(1_000_000);
event.setFlag("amount_truncated");
}
// 时间校正:如果事件时间比当前时间早 5 分钟以上,丢弃
if (event.getEventTime() < System.currentTimeMillis() - 300_000) {
return null; // 会被 filter 掉
}
return event;
}
4.3 特征工程基础:从原始数据到「风控语言」
数据洗干净了,接下来就是特征工程。说白了,就是把原始数据翻译成模型能听懂的语言。
实时风控的特征,跟离线不一样。离线你可以算 1000 个特征,跑个 XGBoost 慢慢调。实时不行,你只有几十毫秒。所以特征要少而精,计算要快。
我一般把特征分成三类:
4.3.1 基础特征
直接从原始字段提取,几乎不需要计算。比如:
- 交易金额(数值型)
- 交易时间(转为小时、星期几)
- 用户注册天数(当前时间 - 注册时间)
- 设备类型(枚举型,做 one-hot 或 embedding)
4.3.2 统计特征
这是实时风控的重头戏。比如:
- 过去 1 小时内,该用户的交易次数
- 过去 24 小时内,该 IP 地址的失败次数
- 过去 5 分钟内,该设备的平均金额
这些特征怎么做?用 Flink 的 滑动窗口 或者 状态编程。我习惯用 KeyedProcessFunction,按用户ID分组,维护一个状态列表,记录最近 N 笔交易的时间戳和金额。窗口滑动时,实时更新统计值。
// 伪代码:实时统计过去1小时交易次数
DataStream<FeatureEvent> featureStream = cleanedStream
.keyBy(event -> event.getUserId())
.process(new KeyedProcessFunction<String, RawEvent, FeatureEvent>() {
private ValueState<List<Long>> timestampsState;
@Override
public void processElement(RawEvent event, Context ctx, Collector<FeatureEvent> out) {
List<Long> timestamps = timestampsState.value();
if (timestamps == null) {
timestamps = new ArrayList<>();
}
long now = event.getEventTime();
// 移除超过1小时的数据
timestamps.removeIf(t -> t < now - 3600_000);
// 加入当前时间戳
timestamps.add(now);
timestampsState.update(timestamps);
// 输出特征:过去1小时交易次数
out.collect(new FeatureEvent(event.getUserId(), "txn_count_1h", timestamps.size()));
}
});
4.3.3 衍生特征
这类特征需要一点「脑洞」。比如:
- 金额与历史均值的偏差:当前金额 / 过去 7 天平均金额。如果比值 > 5,说明异常。
- 设备聚集度:同一 IP 下关联了多少个不同设备。设备越多,越可能是团伙作案。
- 行为序列特征:用户最近 5 次操作是否都是「登录-查询-支付」这种模式?如果突然变成「登录-改密-支付」,风险就高了。
衍生特征的计算,我建议放在 Flink 的 ProcessFunction 里做,因为需要维护状态和上下文。但注意,状态不要太大。比如「行为序列特征」,我最多保留最近 20 条记录,超过就丢弃旧的。
4.4 知识体系总览
说了这么多,咱们用一张图把整个流程串起来。这张图是我自己画的,涵盖了从数据接入到特征产出的核心链路。
这张图里,我把整个流程分成了三层:接入层、清洗层、特征层。每一层都有明确的职责和边界。你想想看,如果这三层混在一起写,后期维护就是噩梦。我见过一个项目,所有逻辑写在一个 Flink 算子里面,3000 行代码,改一个 bug 要半天。所以,分层设计是架构师的底线。
公众号:蓝海资料掘金营,微信deep3321