4. 数据集准备:使用公开数据集模拟生产数据流

在特征漂移监控实战中,数据集准备是构建可靠监控管线的第一步。本章选用 UCI Adult Income(又称“人口普查收入”)数据集,该数据集包含 48,842 条记录,混合了数值型与类别型特征,天然适合模拟生产环境中的结构化数据流。我们将完成以下三项核心任务:

  • 数据获取与清洗:下载原始 CSV,处理缺失值与异常编码。
  • 训练集与实时批次划分:按时间或随机顺序切分,模拟“历史训练 + 在线推理”场景。
  • 数据流模拟器构建:将测试批次包装为逐批到达的生成器,用于后续漂移检测。
💡 为什么选择 Adult Income? 该数据集特征类型丰富(年龄、教育、职业、资本收益等),且存在天然的概念漂移可能性(如收入标准随年份变化),非常适合作为漂移监控的入门案例。

4.1 数据集概览与字段说明

下表列出了 Adult Income 数据集中用于漂移监控的关键字段及其类型。注意,我们将 income 作为目标变量,其余字段作为特征。

字段名 类型 示例值 监控关注点
age 数值(连续) 39 均值、标准差漂移
workclass 类别(8 类) Private 类别分布漂移
education 类别(16 类) Bachelors 类别分布漂移
marital-status 类别(7 类) Never-married 类别分布漂移
occupation 类别(14 类) Exec-managerial 类别分布漂移
capital-gain 数值(连续) 2174 分位数、异常值漂移
hours-per-week 数值(连续) 40 均值、分布形状漂移
income 二分类(目标) >50K / ≤50K 标签分布漂移

4.2 数据流模拟架构

为了贴近生产环境,我们不能一次性加载全部测试数据,而应将其包装成“实时批次”。下图展示了从原始数据集到模拟数据流的完整架构:

数据流模拟架构图 UCI Adult Income 48,842 条原始记录 数据清洗与编码 缺失值处理 / 标签编码 训练集 70% ~ 32,000 条 测试批次池 30% ~ 14,000 条 流模拟器 逐批 yield 漂移检测器 统计检验 / PSI ⚠️ 模拟要点 批次大小固定,顺序可随机或按时间

4.3 代码实现:从 CSV 到数据流

以下 Python 代码实现了完整的准备流程。我们使用 pandas 进行数据处理,并构建一个生成器函数 data_stream() 来模拟实时到达的批次。

import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder

# 1. 加载原始数据(假设已下载 adult.data)
columns = ['age','workclass','fnlwgt','education','education-num',
           'marital-status','occupation','relationship','race','sex',
           'capital-gain','capital-loss','hours-per-week','native-country','income']
df = pd.read_csv('adult.data', names=columns, sep=',\s*', engine='python')

# 2. 数据清洗:去除缺失值(用 '?' 表示)
df.replace('?', np.nan, inplace=True)
df.dropna(inplace=True)

# 3. 选择用于漂移监控的特征子集
feature_cols = ['age','workclass','education','marital-status',
                'occupation','capital-gain','hours-per-week']
target_col = 'income'

# 4. 对类别特征进行标签编码
le_dict = {}
for col in feature_cols:
    if df[col].dtype == 'object':
        le = LabelEncoder()
        df[col] = le.fit_transform(df[col].astype(str))
        le_dict[col] = le

# 5. 划分训练集与测试集(70% / 30%)
X = df[feature_cols]
y = (df[target_col] == '>50K').astype(int)

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)

print(f"训练集大小: {X_train.shape[0]}")
print(f"测试集大小: {X_test.shape[0]}")

# 6. 构建数据流模拟器
def data_stream(X, y, batch_size=500, shuffle=True):
    """
    生成器函数:每次 yield 一个批次的特征和标签。
    """
    data = pd.concat([X, y], axis=1).values
    if shuffle:
        np.random.shuffle(data)
    for i in range(0, len(data), batch_size):
        batch = data[i:i+batch_size]
        yield batch[:, :-1], batch[:, -1]

# 7. 使用示例:模拟前 3 个批次
stream = data_stream(X_test, y_test, batch_size=500)
for batch_idx, (X_batch, y_batch) in enumerate(stream):
    if batch_idx >= 3:
        break
    print(f"批次 {batch_idx+1}: X_shape={X_batch.shape}, y_mean={y_batch.mean():.3f}")

4.4 批次划分策略对比

在实际生产中,数据到达的顺序可能影响漂移检测的敏感性。下表对比了两种常见的批次划分策略:

策略 划分方式 适用场景 对漂移检测的影响
随机打乱 对整个测试集进行 shuffle 后分批 模拟独立同分布(i.i.d.)的在线请求 漂移信号较弱,适合基线测试
时间顺序 按原始数据中的隐含顺序(如索引)分批 模拟随时间变化的数据流(如月度数据) 更容易暴露概念漂移或数据漂移
🔧 实战建议: 在课程后续章节中,我们将默认使用随机打乱策略来模拟生产流量;当你需要测试漂移检测算法的敏感性时,可以切换到时间顺序策略,并在测试集中人为注入漂移。

4.5 验证数据流完整性

在进入漂移检测之前,务必验证数据流是否按预期工作。以下检查清单可以帮助你快速排查问题:

  • 批次大小一致性:除最后一个批次外,所有批次应具有相同的行数。
  • 特征顺序不变:每个批次的特征列顺序必须与训练集完全一致。
  • 类别编码对齐:测试批次中的类别值必须在训练集的编码范围内,避免出现未见过的标签。
  • 目标变量分布:打印每个批次中正例(>50K)的比例,确保没有极端失衡。
# 快速验证:检查批次大小与类别编码
stream = data_stream(X_test, y_test, batch_size=500)
batch_sizes = []
for X_batch, y_batch in stream:
    batch_sizes.append(len(X_batch))
    # 检查类别特征是否在训练集编码范围内
    for col_idx in [1, 2, 3, 4]:  # workclass, education, marital-status, occupation
        unique_vals = set(X_batch[:, col_idx])
        train_vals = set(X_train.iloc[:, col_idx].unique())
        if not unique_vals.issubset(train_vals):
            print(f"警告:批次中发现训练集未出现的类别值!")

print(f"批次大小列表(前10个): {batch_sizes[:10]}")
print(f"所有批次大小一致: {len(set(batch_sizes)) == 1 or batch_sizes[-1] <= 500}")
⚠️ 常见陷阱: 如果使用 pd.get_dummies() 进行独热编码,测试集中可能缺少某些 dummy 列,导致特征维度不匹配。建议统一使用 LabelEncoder 并保存编码器对象,确保生产环境与训练环境一致。