3. 数据加载与预处理

特征回放对比的第一步,是将原始特征数据从存储介质中正确加载到内存,并完成清洗与对齐。原始数据通常存在缺失值、类型不匹配、时间戳不同步等问题,直接影响后续对比分析的准确性。本节将系统讲解使用 Pandas 加载 CSV/Parquet 格式数据、处理缺失值、数据类型转换以及时间戳对齐的标准流程。

3.1 数据加载:CSV 与 Parquet

Pandas 提供 read_csv() 和 read_parquet() 两个核心接口。CSV 是通用文本格式,适合小规模数据交换;Parquet 是列式二进制格式,压缩率高、读取快,适合大规模特征存储。

对比维度 CSV Parquet
文件格式 纯文本,逗号分隔 二进制列式存储
压缩率 低(可配合 gzip) 高(内置 snappy/zstd)
读取速度 慢(全量解析) 快(列裁剪 + 谓词下推)
Schema 保留 需手动指定 dtype 自动保留数据类型
适用场景 小数据、调试、跨系统交换 生产级特征存储、大数据量
import pandas as pd

# 加载 CSV,指定日期列和数据类型
df_csv = pd.read_csv(
    'feature_data.csv',
    parse_dates=['timestamp'],
    dtype={'user_id': 'int32', 'score': 'float32'}
)

# 加载 Parquet,自动保留 schema
df_pqt = pd.read_parquet('feature_data.parquet')

print(f'CSV shape: {df_csv.shape}')
print(f'Parquet shape: {df_pqt.shape}')
实践建议:生产环境优先使用 Parquet 格式。若必须使用 CSV,务必通过 dtype 参数显式指定列类型,避免 Pandas 自动推断导致内存浪费或类型错误。

3.2 缺失值处理

特征数据中的缺失值通常以 NaN、None 或占位符(如 -999)形式存在。处理策略需根据特征含义和下游模型要求决定。

  • 删除法:当缺失比例极低(< 1%)且随机分布时,直接删除缺失行。
  • 填充法:数值特征常用均值/中位数填充;类别特征用众数或特殊标记填充。
  • 前向/后向填充:时间序列特征中,使用 ffill() 或 bfill() 按时间顺序填充。
  • 插值法:使用 interpolate() 进行线性或样条插值,适合连续变化特征。
# 检查缺失值
print(df.isnull().sum())

# 策略1:删除缺失比例 < 1% 的行
df_clean = df.dropna(thresh=len(df.columns) - 1)

# 策略2:数值列用中位数填充
num_cols = df.select_dtypes(include='number').columns
df[num_cols] = df[num_cols].fillna(df[num_cols].median())

# 策略3:时间序列前向填充
df['feature_a'] = df['feature_a'].ffill()

# 策略4:线性插值
df['feature_b'] = df['feature_b'].interpolate(method='linear')
注意:填充缺失值后,务必记录填充策略和参数,避免在回放对比时引入数据泄露。建议将填充逻辑封装为可复用的 Transformer。

3.3 数据类型转换

特征数据加载后,常见类型问题包括:数值列被读为 object、时间戳为字符串、类别列未转为 category 类型。类型转换可降低内存占用并提升计算效率。

原始类型 目标类型 转换方法 内存节省
int64 int32 / int16 astype('int32') 50% / 75%
float64 float32 astype('float32') 50%
object category astype('category') 视基数而定
object (时间) datetime64 pd.to_datetime() —
# 批量优化数值类型
for col in df.select_dtypes(include='int64').columns:
    df[col] = df[col].astype('int32')

for col in df.select_dtypes(include='float64').columns:
    df[col] = df[col].astype('float32')

# 类别列转换
cat_cols = ['city', 'channel', 'device_type']
for col in cat_cols:
    df[col] = df[col].astype('category')

# 时间戳转换
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')

3.4 时间戳对齐

特征回放对比要求不同来源的特征在相同时间基准上进行比较。时间戳对齐的核心是将多个 DataFrame 按照统一的时间频率(如 1 分钟、1 小时)进行重采样和合并。

时间戳对齐流程 原始特征 A 时间戳: 不规则 原始特征 B 时间戳: 不同频率 原始特征 C 时间戳: 含缺失 统一重采样 (resample) 频率: 1min / 5min / 1H 对齐合并 (merge / join) 以统一时间索引为基准 对齐后的特征 DataFrame
# 设置时间索引
df_a = df_a.set_index('timestamp')
df_b = df_b.set_index('timestamp')

# 统一重采样到 1 分钟频率,使用前向填充
df_a_resampled = df_a.resample('1T').ffill()
df_b_resampled = df_b.resample('1T').ffill()

# 按时间索引对齐合并
df_aligned = pd.merge(
    df_a_resampled,
    df_b_resampled,
    left_index=True,
    right_index=True,
    how='outer'
)

# 处理合并后仍存在的缺失时间点
df_aligned = df_aligned.interpolate(method='time')

print(f'对齐后数据范围: {df_aligned.index.min()} ~ {df_aligned.index.max()}')
print(f'对齐后记录数: {len(df_aligned)}')
对齐策略选择:
  • 左对齐:以特征 A 的时间戳为基准,适合 A 为主特征、B 为辅助特征。
  • 右对齐:以特征 B 的时间戳为基准,适合 B 为标签或目标特征。
  • 内对齐:只保留双方都有的时间点,适合严格对比场景。
  • 外对齐:保留所有时间点,缺失部分填充,适合全量回放。

3.5 完整预处理流水线

将上述步骤整合为可复用的预处理函数,便于在回放对比实验中重复调用。

def load_and_preprocess(csv_path: str, parquet_path: str = None) -> pd.DataFrame:
    """
    加载并预处理特征数据,返回对齐后的 DataFrame。
    """
    # 1. 加载数据
    df = pd.read_csv(csv_path, parse_dates=['timestamp'])
    if parquet_path:
        df_pqt = pd.read_parquet(parquet_path)
        df = pd.concat([df, df_pqt], ignore_index=True)

    # 2. 处理缺失值
    num_cols = df.select_dtypes(include='number').columns
    df[num_cols] = df[num_cols].fillna(df[num_cols].median())

    # 3. 类型转换
    for col in df.select_dtypes(include='int64').columns:
        df[col] = df[col].astype('int32')
    for col in df.select_dtypes(include='float64').columns:
        df[col] = df[col].astype('float32')

    # 4. 时间戳对齐
    df = df.set_index('timestamp')
    df = df.resample('1T').ffill().interpolate(method='time')

    return df.reset_index()

# 使用示例
feature_df = load_and_preprocess('train_features.csv', 'test_features.parquet')
print(feature_df.info())
工程化建议:将预处理流水线保存为 preprocessing_pipeline.pkl,使用 joblib 序列化,确保回放对比时使用完全相同的预处理逻辑。