4. 单变量切片分析:数值型特征切片、类别型特征切片、时间序列切片、文本特征切片

单变量切片,说白了就是一次只看一个特征。你可能会问:「这有什么难的?不就是画个直方图吗?」嗯,没那么简单。我做了这么多年模型诊断,发现很多模型翻车,恰恰就翻在单变量切片这一步没做透。

这一章,咱们把四种最常见的特征类型——数值型、类别型、时间序列、文本——的切片方法挨个过一遍。每种我都会给出具体的操作步骤、代码示例,以及我踩过的坑。

核心原则:单变量切片不是为了看分布,而是为了看「模型在这个特征的不同取值上,表现是否一致」。如果某个区间误差突然变大,那可能就是数据泄露、样本偏差或者特征工程出了问题。

4.1 数值型特征切片

数值型特征是最常见的。年龄、收入、温度、点击次数……这些连续值怎么切?我见过有人直接按等距分桶,结果样本分布不均,模型评估完全失真。

我个人习惯用三种方法:

  • 等宽分桶:把值域分成 N 个等距区间。适合分布均匀的特征,比如温度 0-100°C 每 10°C 切一段。
  • 等频分桶:每个桶里样本数差不多。适合长尾分布,比如收入——少数人收入极高,等宽分桶会导致前几个桶挤满人,后面桶几乎空着。
  • 业务阈值分桶:按业务含义切。比如年龄按 0-18、18-35、35-60、60+ 来分。这是我最推荐的方式,因为可解释性最强。

举个例子,假设我们有个房价预测模型,特征 房屋面积 从 20 到 500 平米。用等频分桶看看模型误差:

import pandas as pd
import numpy as np

# 假设 df 包含 'area' 和 'error'(预测误差)
df['area_bucket'] = pd.qcut(df['area'], q=5, labels=['Q1','Q2','Q3','Q4','Q5'])

# 计算每个桶的平均误差
error_by_bucket = df.groupby('area_bucket')['error'].agg(['mean','std','count'])
print(error_by_bucket)

输出结果可能长这样:

面积分桶 平均误差 误差标准差 样本数
Q1 (20-60) 0.15 0.08 2000
Q2 (60-90) 0.12 0.06 2000
Q3 (90-130) 0.11 0.05 2000
Q4 (130-180) 0.18 0.12 2000
Q5 (180-500) 0.35 0.25 2000
注意:Q5 的误差明显偏高,而且标准差也大。我在一个电商推荐项目里遇到过类似情况——大户型样本太少,模型根本没学到规律。后来我们专门对大面积房源做了数据增强,误差才降下来。

4.2 类别型特征切片

类别型特征,比如城市、职业、产品类别。这类特征切片最容易犯的错误是:只看整体准确率,忽略低频类别。

我曾经在一个信贷风控模型里发现,整体 AUC 高达 0.92,但「自由职业」这个类别的坏账率预测完全不准。为什么?因为样本太少,模型直接把它当成了「平均人」。

类别型切片我一般分三步走:

  1. 计算每个类别的样本量——少于 100 条的类别要标记为「低频」。
  2. 计算每个类别的评估指标——准确率、召回率、F1 都算上。
  3. 对比低频类别与整体表现——如果差距超过 20%,就要警惕了。

代码示例:

# 假设 df 包含 'city' 和 'pred'、'label'
city_stats = df.groupby('city').agg(
    count=('label', 'count'),
    accuracy=('label', lambda x: (x == df.loc[x.index, 'pred']).mean())
).reset_index()

# 标记低频类别
city_stats['flag'] = city_stats['count'].apply(
    lambda x: '低频' if x < 100 else '正常'
)

print(city_stats[city_stats['flag'] == '低频'])
我的建议:对于低频类别,不要直接删除。可以考虑做类别合并(比如「自由职业」和「兼职」合并成「非固定职业」),或者用迁移学习的方式给低频类别单独训练一个子模型。

4.3 时间序列切片

时间序列切片,很多人以为就是按年、月、日分组看看。其实没那么简单。时间序列里有个很隐蔽的问题——时间漂移。

什么意思?模型在训练集上表现很好,但上线后效果越来越差。因为数据分布随时间变了。比如用户行为习惯变了、政策变了、季节变了。

我建议按以下维度做时间切片:

  • 按时间粒度:年、季度、月、周、日。根据业务周期选。
  • 按时间窗口:比如「训练期 vs 验证期 vs 上线后第1周 vs 第2周」。
  • 按事件节点:比如「促销前 vs 促销中 vs 促销后」。

代码示例——按月查看模型误差变化:

# 假设 df 包含 'date' 和 'error'
df['month'] = pd.to_datetime(df['date']).dt.to_period('M')

monthly_error = df.groupby('month')['error'].agg(['mean','std']).reset_index()
monthly_error['month'] = monthly_error['month'].astype(str)

# 看看有没有明显上升趋势
print(monthly_error.head(12))

如果发现某个月误差突然飙升,别急着调模型。先看看那个月有没有特殊事件——比如双十一、春节、或者数据采集系统出了故障。

我曾经踩过的坑:有一次模型在 12 月误差突然变大,我折腾了两周调参,后来才发现是数据管道在 12 月改了日志格式,导致特征计算错误。所以时间切片分析一定要结合数据质量检查。

4.4 文本特征切片

文本特征切片,听起来有点抽象。文本怎么切?按长度?按情感?按主题?

其实思路是一样的——找到文本的某个可量化维度,然后看模型在这个维度上的表现差异。我常用的维度有:

  • 文本长度:短文本 vs 长文本。模型对短文本(比如标题)和长文本(比如评论)的表现可能完全不同。
  • 情感极性:正面 vs 负面 vs 中性。情感分类模型在负面样本上往往更差。
  • 主题类别:按 LDA 或 BERT 聚类后的主题分组。
  • 语言/方言:如果数据是多语言的,一定要按语言切片。

举个例子,假设我们有个垃圾邮件分类模型,按文本长度切片:

# 假设 df 包含 'text' 和 'pred'、'label'
df['text_len'] = df['text'].apply(len)
df['len_bucket'] = pd.cut(df['text_len'], 
                          bins=[0, 50, 200, 500, 10000],
                          labels=['极短','短','中','长'])

len_stats = df.groupby('len_bucket').agg(
    count=('label', 'count'),
    accuracy=('label', lambda x: (x == df.loc[x.index, 'pred']).mean())
)
print(len_stats)

结果可能显示「极短」文本的准确率只有 60%,而整体是 95%。为什么?因为极短文本信息量太少,模型很难判断。这时候就需要对短文本做特征增强,比如加入字符级 n-gram。

一个小技巧:文本切片时,别忘了看 OOV(词表外词)的比例。如果某个切片里 OOV 词特别多,模型表现差是必然的。我曾经在一个医疗文本项目里发现,专业术语多的切片误差是普通切片的 3 倍——因为词表里根本没这些词。

4.5 四种切片方法的对比与选择

说了这么多,你可能想问:「那我到底该用哪种?」

我的经验是:先看特征类型,再看业务场景。下面这张表可以帮你快速决策:

特征类型 推荐切片方法 常见陷阱 我的建议
数值型 等频分桶 + 业务阈值 等宽分桶导致样本不均 优先用业务阈值,其次等频
类别型 按类别分组 + 低频标记 忽略低频类别 低频类别合并或单独建模
时间序列 按时间粒度 + 事件节点 时间漂移被忽略 结合数据质量检查
文本 按长度/情感/主题 OOV 词比例过高 先做文本预处理再切片

4.6 单变量切片分析知识体系

下面这张图帮你理清本章的核心逻辑。四种特征类型各自对应不同的切片策略,最终都指向同一个目标——发现模型表现不一致的区域。

单变量切片分析知识体系 单变量切片分析 数值型特征 类别型特征 时间序列特征 文本特征 等宽分桶 等频分桶 业务阈值分桶 按类别分组 低频类别标记 类别合并/单独建模 按时间粒度 按时间窗口 按事件节点 按文本长度 按情感极性 按主题/语言 目标:发现模型表现不一致的区域 💡 说明:四种特征类型的切片方法各有侧重,但核心逻辑一致——将特征离散化后,对比模型在各切片上的表现。 数值型侧重分桶策略,类别型关注低频问题,时间序列警惕漂移,文本特征注意 OOV 和长度影响。