3. 数据采集与预处理

好,咱们直接进入正题。数据采集与预处理,说白了就是「把脏活累活干漂亮」。很多同学觉得这步没啥技术含量,上来就调模型、跑仿真。我刚开始也这么想,结果被真实数据狠狠教育了一回——数据源没接对,后面全白干。

今天我就把这块的实战经验掰开揉碎讲清楚。你想想看,仿真数据回放的核心是什么?是「还原真实场景」。如果源头数据就歪了,那回放出来的结果就是自欺欺人。

3.1 数据源接入:三种主流方式

数据源接入,我习惯把它分成三类:日志、数据库、消息队列。每种都有各自的脾气,得顺着来。

3.1.1 日志接入

日志是最常见的数据源。线上系统每时每刻都在吐日志,格式五花八门。我个人习惯用 Filebeat + Logstash 这套组合拳。

核心思路:轻量采集 + 集中处理。Filebeat 只负责读文件、发数据,不干重活。Logstash 在服务端做解析、过滤、转换。

# Filebeat 配置示例(简化版)
filebeat.inputs:
- type: log
  enabled: true
  paths:
    - /var/log/app/*.log
  multiline.pattern: '^\d{4}-\d{2}-\d{2}'
  multiline.negate: true
  multiline.match: after

output.logstash:
  hosts: ["logstash-server:5044"]

这里有个坑。日志里经常有多行消息(比如 Java 异常堆栈),如果不做 multiline 处理,一条异常会被拆成几十条记录。我曾经因为这个排查了整整一个下午,最后发现是配置漏了。

小技巧:日志接入后,先做一次「抽样验证」。随便挑几条原始日志,看看解析后的字段对不对得上。别等全量跑完了才发现字段丢了。

3.1.2 数据库接入

数据库接入,我建议用 CDC(Change Data Capture) 方案。为什么?因为全量同步太慢了,而且对业务库有压力。

常用的工具有 Debezium、Canal、Maxwell。我个人偏爱 Debezium,它基于 Kafka Connect,生态好,支持 MySQL、PostgreSQL、MongoDB 等多种数据库。

# Debezium MySQL 连接器配置(关键部分)
{
  "name": "mysql-connector",
  "config": {
    "connector.class": "io.debezium.connector.mysql.MySqlConnector",
    "database.hostname": "192.168.1.100",
    "database.port": "3306",
    "database.user": "debezium",
    "database.password": "debezium_pwd",
    "database.server.id": "184054",
    "database.server.name": "myapp",
    "database.include.list": "order_db",
    "table.include.list": "order_db.t_order, order_db.t_order_item",
    "snapshot.mode": "initial"
  }
}

嗯,这里要注意 snapshot.mode 参数。第一次启动时,Debezium 会做一次全量快照,之后才增量监听 binlog。如果你的表数据量很大(比如上亿条),快照阶段可能会很慢。我建议先做一次手动全量导出,再启动 CDC。

避坑指南:我曾经在生产环境直接启动 Debezium 全量快照,结果把业务库的 IO 打满了。从那以后,我都是先在从库上测试,或者用 snapshot.mode: "when_needed" 配合手动控制。

3.1.3 消息队列接入

消息队列接入,最常见的是 Kafka。仿真数据回放场景下,Kafka 几乎是标配。为什么?因为它能保存历史数据,支持回溯消费。

我一般这样配置消费者:

# Kafka 消费者配置(Python 示例)
from kafka import KafkaConsumer

consumer = KafkaConsumer(
    'order_topic',
    bootstrap_servers=['kafka1:9092', 'kafka2:9092'],
    auto_offset_reset='earliest',  # 从最早的消息开始消费
    enable_auto_commit=False,      # 手动提交偏移量
    group_id='data_replay_group',
    value_deserializer=lambda m: json.loads(m.decode('utf-8'))
)

# 逐条消费,用于回放
for message in consumer:
    process_message(message.value)
    # 模拟真实时间间隔
    time.sleep(original_interval)

这里 auto_offset_reset='earliest' 很关键。仿真回放需要从头开始,而不是从最新消息开始。我见过有人忘了配这个,结果回放的数据全是最近几分钟的,完全失去了仿真意义。

3.2 数据清洗:把脏数据筛出去

数据清洗,说白了就是「去伪存真」。真实数据里什么妖魔鬼怪都有:字段错位、编码乱码、重复记录、格式不统一……

我总结了一套清洗流程,按优先级排序:

  1. 格式清洗:统一日期格式、数字格式、编码格式
  2. 去重清洗:基于业务主键去重,保留最新或最完整的一条
  3. 逻辑清洗:检查字段间的逻辑关系(比如下单时间不能晚于支付时间)
  4. 范围清洗:剔除超出合理范围的值(比如年龄 200 岁)
# 数据清洗示例(Python)
def clean_order_data(df):
    # 1. 格式清洗:统一日期格式
    df['order_time'] = pd.to_datetime(df['order_time'], errors='coerce')
    
    # 2. 去重清洗:按订单号去重,保留最后一条
    df = df.drop_duplicates(subset=['order_id'], keep='last')
    
    # 3. 逻辑清洗:支付时间不能早于下单时间
    invalid_mask = df['pay_time'] < df['order_time']
    df = df[~invalid_mask]
    
    # 4. 范围清洗:金额不能为负数
    df = df[df['amount'] >= 0]
    
    return df

个人经验:清洗规则不要一次加太多。我习惯先跑一轮基础清洗,看看数据分布,再逐步加规则。否则你都不知道是哪条规则把数据搞没了。

3.3 缺失值处理:别乱填,先问为什么

缺失值处理,很多人上来就填均值、填中位数。我不建议这么干。为什么?因为缺失本身可能是一种信号。

我一般分三步走:

缺失类型 常见原因 处理方式
完全随机缺失 系统故障、网络抖动 删除该行,或用均值/中位数填充
随机缺失 用户未填写、字段可选 用模型预测填充,或用「未知」标记
非随机缺失 系统设计缺陷、特定条件下不采集 必须查明原因,不能随意填充

举个例子。用户画像数据里,「收入」字段经常缺失。如果你直接填均值,那低收入和高收入用户都会被拉到平均水平,仿真出来的用户行为就失真了。

我建议的做法是:

# 缺失值处理策略
def handle_missing(df, column, strategy='mark'):
    if strategy == 'mark':
        # 用特殊值标记,保留缺失信息
        df[column] = df[column].fillna(-999)
    elif strategy == 'model':
        # 用其他字段预测缺失值
        from sklearn.ensemble import RandomForestRegressor
        # ... 训练模型填充
    elif strategy == 'drop':
        # 缺失比例低时直接删除
        df = df.dropna(subset=[column])
    return df

避坑指南:我曾经在一个风控仿真项目里,把缺失的「逾期天数」全填了 0。结果回放出来的模型表现特别好,上线后直接崩了。后来才发现,缺失的逾期天数其实是「未催收」的情况,填 0 完全扭曲了业务逻辑。

3.4 异常值检测:别一刀切,要分场景

异常值检测,我把它分成两类:统计异常和业务异常。统计异常用算法找,业务异常靠规则筛。

3.4.1 统计异常检测

常用的方法有:

  • 3σ 原则:适用于正态分布的数据
  • 箱线图(IQR):适用于偏态分布,更稳健
  • Z-Score:标准化后看偏离程度
  • 孤立森林:适用于高维数据
# 箱线图法检测异常值
def detect_outliers_iqr(df, column):
    Q1 = df[column].quantile(0.25)
    Q3 = df[column].quantile(0.75)
    IQR = Q3 - Q1
    
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR
    
    outliers = df[(df[column] < lower_bound) | (df[column] > upper_bound)]
    return outliers

3.4.2 业务异常检测

统计方法只能告诉你「这个值不太常见」,但到底是不是异常,还得看业务。比如:

  • 凌晨 3 点的下单量突然暴增——可能是促销活动,也可能是爬虫攻击
  • 用户单笔支付 100 万——可能是大客户,也可能是洗钱
  • 接口响应时间 10 秒——可能是网络抖动,也可能是真的慢

我个人的处理原则是:先标记,再判断,最后处理。不要一上来就删除异常值,先看看它们是不是「有价值的异常」。

核心观点:仿真数据回放的目标是「还原真实」,不是「制造完美」。异常值如果真实存在,就应该保留。只有在异常值明显是数据采集错误时,才需要剔除或修正。

3.5 本章知识体系总览

说了这么多,我画了一张图帮你梳理整体流程。从数据源接入,到清洗、缺失值处理、异常值检测,每一步都有对应的策略和工具。

数据采集与预处理全流程 数据源接入 日志接入 (Filebeat+Logstash) 数据库接入 (Debezium CDC) 消息队列接入 (Kafka) 数据清洗 格式清洗 去重清洗 逻辑清洗 范围清洗 缺失值处理 完全随机缺失 → 删除/填充 随机缺失 → 模型预测/标记 非随机缺失 → 查明原因 异常值检测 统计异常 (3σ/IQR/Z-Score) 业务异常 (规则/经验判断)

这张图把整个流程串起来了。你从左边选数据源,中间做清洗和处理,最后得到干净可用的数据。每一步都有对应的工具和方法,照着走基本不会出大问题。

最后说一句:数据预处理没有银弹。每个项目的数据都不一样,关键是多看、多试、多验证。我做了这么多年,每次遇到新数据源,还是会老老实实先做一轮探索性分析。别嫌麻烦,这一步省下来的时间,后面会加倍还给你。

可可精品资料库