跳转至

P4-7.1 特征选择(feature selection)

Section ID: P4-7.1 Version: v2026.07.31

先想象一个场景。如果客户流失预测表中同时有最近访问次数、会员等级、最近咨询数、负责人完成 复核 后产生的结果 备注、内部临时 ID,把它们全都交给模型并不是好的出发点。本节首先要做的,不是计算性能,而是过滤掉预测时点不可用的列、与问题关系较弱的列、运营中难以再次生成的列。

在 P4-6 里,我们看过 该用什么标准来评价。现在把问题再往前推一步。在更换评价指标之前,必须先检查:到底要给 model 什么输入。特征选择(feature selection)正是这个输入设计的起点。

这一节处理的是 该怎样挑出好特征。它的目的不是深讲复杂选择算法,而是先固定在实际工作里应该优先检查的判断标准。

这一节会说明 特征选择(feature selection) 和 特征空间(feature space) 的含义。下一节会沿着这个抓手继续当前语境,而 到底用什么去填输入格子 的基本标准,会通过这一节和 概念词汇表 再次接回。

本节范围

这一节回答下面这些问题。

  • 什么是特征(feature),为什么输入设计很重要?
  • 为什么不能因为可用数据多,就把它们全部塞进去?
  • 读者最先该检查的特征选择标准是什么?
  • 特征选择和预处理(preprocessing)有什么不同?

这一节先收束 该怎样选择好特征,以及 为什么特征选择是输入设计问题。按输入问题来分预处理类型的感觉,会在补充学习 P4-7.3 再次抓住;基于统计检验的选择和递归特征消除的比较视角,会在补充学习 P4-7.4 再整理一次。降维的大图景会在 P4-18.1、P4-18.2 继续展开。

用特征选择(feature selection)留下的判断标准

  • 能把特征(feature)解释成 现实信息被变成 model 输入后的形式。
  • 能说明特征选择不只和性能数字有关,也会连到 leakage、成本(cost)、稳定性和可解释性。
  • 能使用基本问题去区分哪些特征该先丢掉,哪些特征该先留下。
  • 能说明:如果 preprocessing 是 把选出来的特征再加工,那么 feature selection 就是 决定一开始到底要采用哪些特征。

学习背景

什么是特征

在 machine learning 里,特征(feature)就是把现实对象改造成 model 能读的输入格子。

例如,客户流失(churn)预测问题可以像下面这样来读。

现实信息 转成特征后的例子
最近一个月访问次数 visits_30d
最近一个月支付金额 spend_30d
最近一周咨询次数 support_tickets_7d
会员等级 membership_tier

特征并不是把现实世界里的事实原样复制进来,而是 为了更好地解决问题而切出来的输入表达。

所以,即使是同一份数据,只要把哪些格子当成特征来用,model 就会完全不同。

不过,这个判断要稳定下来,前提是先固定 一条样本到底是什么。因为如果一行到底代表一个客户、一次动作,还是多个时点汇总后的摘要行都不稳定,那么特征的含义也会一起摇晃。

如果稍微从理论一点的角度说,特征就是 输入变量(input variable) 的单位。model 通常不会一下子理解一个巨大的答案,而是会把多个输入变量放在一起看,并从它们的组合里找出重复出现的模式。每个输入变量就是一个特征。

例如,在房价预测问题里,面积、房间数、到地铁站的距离 可以成为特征。在 spam 分类问题里,某个词是否出现、是否有附件、发件域名模式 可以成为特征。也就是说,特征会随着问题改变,同样的现实也会因为提问不同而被表达成不同的特征集合。

为什么特征选择要先看

特征选择并不只是减少格子数量,它是在决定 什么信息会被允许参加 model 判断。

scikit-learn 文档说明,feature selection 模块可以用于减少不必要或噪音很大的特征,以此改善性能和计算效率。尤其是在维度很高的数据里,这个判断会变得更重要。

核心理由有下面四点。

  1. 如果无关输入太多,model 可能会学到噪音。
  2. 输入太多会提高训练和推理成本。
  3. 难以解释的输入越多,结果解释就可能越困难。
  4. 如果把未来预测时点拿不到的输入塞进去,就会出现数据泄漏(leakage)。

特征选择就是 留下好信号,减少危险信号和不必要信号。

如果再用理论语言说,特征选择会同时调整下面四件事。

视角 特征选择改变的东西
表达视角 model 所看到的输入空间形状
学习视角 model 必须学习的模式难度
统计视角 signal 与 noise 的比例
运营视角 在实际 service 里能不能把这个输入再复现出来

按这张表来读,特征选择更接近的不是单纯整理数据,而是 重新定义学习问题。

特征空间(feature space)与表达(representation)

逐个看特征固然重要,但 machine learning 往往不是只靠一个特征来判断。它通常会看多个特征一起形成的输入空间。这一节把这个东西叫作 特征空间(feature space)。

例如,假设只有两个特征。

  • visits_30d
  • support_tickets_30d

那么一个客户就可以被表示成一个点,(访问次数, 咨询次数)。如果有几千个客户,这些点就会聚成一个空间。model 会在这个空间里学习 流失点的倾向、相似点的聚集、边界的形状。

所以,特征选择不只是减少列数,它也是在 设计 model 将会看到的输入空间本身。

flowchart TB
  A["现实对象<br/>人 / 产品 / 事件"]
  B["特征设计<br/>选择可测量的方面"]
  C["特征空间<br/>把每一行表示成输入空间中的点"]
  D["模型学习<br/>寻找模式或边界"]

  A --> B
  B --> C
  C --> D

这张图说明:特征选择并不只是挑列,而是在决定到底把现实翻译成什么样的输入空间。即使面对同样的现实,只要抽出的特征不同,model 学到的模式和边界就可能完全改变。

这张图的核心是,比起特征多不多,更重要的是 你是用什么视角把现实切成输入空间。

同样的事实,只要换一种表达方式,model 就可能更容易学,也可能更难学。

例如,假设在表达客户活动。

同样的现实 表达 1 表达 2
最近活动 最近 30 天总访问数 最近 7 天、30 天、90 天访问数
购买规模 最近 1 次购买金额 最近 30 天平均购买金额
咨询行为 总咨询数 退款咨询数、配送咨询数

这张表的核心是:特征选择不仅和 挑原始列 有关,也和 到底用什么表达单位来构造输入 有关。

如果处理的是原始日志,这个差别会更明显。即使看到的是按时间顺序堆起来的值,model 输入也往往不会把整段原始行全部直接放进去,而是会先按动作单位重新生成一些摘要特征候选。

这里的顺序也一样是 原始时序 -> 每次动作一条摘要行 -> 特征选择。特征选择是在这个顺序完成后,再决定 摘要行里哪些输入格子要留下 的阶段。

原始日志里看到的值 转成特征候选后的例子 为什么需要这种表达
随时间变化的 signal_a signal_a_mean 为了把整体水平概括成一个格子
前半段和后半段的值差 signal_a_drop 为了暴露变化方向和大小
分段波动 signal_a_std 为了同时看稳定性和变动性
每段反复出现的形状 pattern_code 为了把类似模式简短地做比较

这里重要的不是记名字,而是有没有把 原始值本身 转成 更直接接到问题上的阅读单位。也就是说,特征选择不只是删列,它也是把表达重新设计成 原始时序 -> 每次动作一条摘要行 -> model 输入格子 的工作。

反过来读,这也意味着:如果一张表里 一行到底代表什么 还没说明白,那就不该太早开始特征选择讨论。

这个场景更直接地看,可以像下面这样读。

action_id duration_steps control_mean sensor_a_peak sensor_b_mean sensor_a_slope
A-101 5 0.44 28.4 1.18 0.63
A-102 5 0.46 28.0 1.22 0.51
A-103 5 0.43 29.1 1.34 0.72

这张表是把多行原始时序重新表达成 每次动作一行 的摘要表。这里像 control_mean、sensor_a_peak、sensor_a_slope 这样的列,不只是计算结果,它们已经是对 什么应该留下作为 model 输入格子 的选择结果。也就是说,特征选择在 要丢哪些列 这个问题之前,就已经连到 用什么表达保留什么 这个问题上了。

再往前一步,有些特征并不会停在描述单次动作,它们还会继续接到把最近状态和基准线做对比的输入上。

metric recent_5_avg baseline_20_avg delta interpretation
duration_steps 5.4 5.0 0.4 recent longer
control_mean 0.48 0.44 0.04 recent slightly higher
sensor_a_peak 29.6 28.2 1.4 recent peak increased
sensor_b_mean 1.42 1.16 0.26 recent average increased

在这张比较表里,重要的不是 delta 数字本身,而是前面做出来的特征现在又被拿去做 最近区间 和 基准线 的比较框架。所以,特征选择不只是学习输入设计,它也会同时变成给人阅读的比较报告设计。

不过,这张比较表首先展示的只是变化信号,它并不会自动把差异原因直接定死。

如果用一段短代码来看,这条线会更清楚。

问题场景:

  • 想把多行原始日志变成动作单位特征,然后再把这些特征拿去做最近区间和基准线比较输入

输入(input):

  • 包含 action_id、time_step、control_level、sensor_a、sensor_b 的小型原始日志表

期望输出(output):

  • 动作单位摘要表
  • 最近平均、基准线平均、差值对比表

确认概念:

  • 特征是把原始日志改造成单行输入的表达设计
  • 同一个特征可以同时被复用在 model 输入和运营比较表里
# 这个例子比较原始特征候选和已选择的特征组合,用来设计模型输入空间。
import pandas as pd

raw = pd.DataFrame(
    [
        ["A-101", 0, 0.20, 24.8, 0.3],
        ["A-101", 1, 0.35, 25.6, 0.8],
        ["A-101", 2, 0.55, 27.1, 1.5],
        ["A-102", 0, 0.18, 24.5, 0.2],
        ["A-102", 1, 0.32, 25.3, 0.7],
        ["A-102", 2, 0.60, 27.8, 1.6],
        ["A-103", 0, 0.22, 25.0, 0.4],
        ["A-103", 1, 0.36, 26.1, 0.9],
        ["A-103", 2, 0.54, 29.1, 1.8],
    ],
    columns=["action_id", "time_step", "control_level", "sensor_a", "sensor_b"],
)

summary = (
    raw.groupby("action_id")
    .agg(
        duration_steps=("time_step", "count"),
        control_mean=("control_level", "mean"),
        sensor_a_peak=("sensor_a", "max"),
        sensor_b_mean=("sensor_b", "mean"),
    )
    .reset_index()
)

recent = summary.tail(2).mean(numeric_only=True)
baseline = summary.head(len(summary) - 2).mean(numeric_only=True)

comparison = pd.DataFrame(
    {
        "recent": recent,
        "baseline": baseline,
        "delta": recent - baseline,
    }
)

print(summary)
print(comparison.round(2))

在这段代码里,首先该看的不是性能数字,而是表达方式的转换。

  • 多行原始日志被转换成动作单位特征表。
  • 这个特征表又被继续拿去做最近区间和基准线对比表。
  • 也就是说,特征选择甚至在预处理之前,就已经在决定 什么会变成可比较输入。
  • 摘要行让比较更容易,但它并不会完全替代原始时序里的全部语境。

所以,特征选择总会同时装着下面两个问题。

  1. 要留下什么信息?
  2. 要把这些信息做成什么形状的输入表达?

第二个问题会继续接到 preprocessing,但出发点仍然是特征设计。

好特征需要具备什么

人们常常误会 好特征 = 数字特征。但真正重要的不是数据类型,而是这个特征和问题到底形成了什么关系。

这一节把好特征看成 既是 model 容易学习的信号,又是实际运营中还能再做出来的输入。也就是说,它不能只是训练表里看起来很像样的列,而应该是 在预测时点也能正当使用、意义不会乱晃、和其他特征放在一起时角色仍然清楚的输入。

从理论上说,下面五个视角最重要。

1. 有没有 signal

这个特征应该多少装着一些和目标 supervised learning label 有关的模式。

例如,在客户流失问题里,最近访问次数就可能和流失有关。相反,一个完全任意的内部流水号,通常无法解释问题的原因或倾向。

好特征应该多少装着 有助于预测目标的 signal。

2. noise 会不会太大

就算看起来有值,也不一定就是好特征。

  • 测量本身是不是不准确?
  • 是不是人工随手输入,所以波动很大?
  • 它的意义会不会经常随场景改变?

这种特征里,noise 可能比 signal 还大。那 model 就更容易学到偶然波动,而不是稳定规则。

3. 在预测时点到底能不能用

好特征不能只是在训练数据集里 存在这个值,还必须是在真实预测时点也 拿得到这个值。

  • 它是不是只有结果出来后才会被记录?
  • 它是不是人事后补上的判断值?
  • 它是不是只有看完整个时间段之后才算得出来的摘要值?

例如,像 contract_cancelled_at、refund_confirmed_at、next_30d_spend 这样的值,在训练表里可能会看起来信号很强,但在真实预测时点通常还不知道。这样的特征即使信号很强,也不是 好特征,而是 有泄漏风险的特征。

所以,好特征不仅要 帮助预测,还要同时具备 在预测时点能被正当地使用。

4. redundancy 会不会太大

如果塞入过多意思相近的特征,信息不一定更丰富,解释反而可能只是重复。

例如,如果下面这些列同时出现,就值得先怀疑一定程度的重复。

  • monthly_spend
  • quarterly_spend
  • yearly_spend

当然,它们并不总是无用。但首先要检查的是:是不是在 用不同单位重复表达近似相同的意思。

5. 在运营里能不能再做出来

好特征不能只是离线实验表里存在的值,它还必须能在真实 service 里按类似规则再生成出来。

  • 收集延迟会不会太大?
  • 会不会像人工记录备注那样,质量波动很大?
  • 会不会因为隐私、成本、API 依赖,而在推理时点很难稳定拿到?

例如,客服后来留下的摘要备注,训练时可能看起来很有用,但在实时流失预测里可能来得太晚,或者格式不稳定。反过来,像 visits_7d、failed_payments_30d 这样的聚合特征,就更容易被稳定复现。

概括起来,好特征大体会朝下面这个条件靠拢。

它要装着和问题相关的 signal,noise 不能过大,预测时点必须能用,和其他特征的重复要可管理,并且在运营里还能再做出来。

这五个条件其实不是分家各玩的。比如一个特征看起来很强,只要在预测时点不能用,就直接出局;只要在运营里无法复现,它也不适合作为 service 输入。反过来,一个特征就算 signal 不是最强,只要能稳定重复收集、意义清楚,也可能成为很好的基础特征候选。

如果想更直观地抓住好特征,最好把它和 看起来很强但实际上危险的特征 放在一起对照。

表面上为什么看起来不错 实际上为什么可能很危险 更好的方向
和 label 几乎完美同步变化 如果是结果之后的值,就可能是 leakage 去找预测时点之前的行为信号
数字大、变化也大,很显眼 可能只是单位效果大,真实 signal 反而弱 先看和问题直接相连的意义
描述文字很多,看起来信息很丰富 人工输入、延迟、格式波动会让运营复现性很低 优先选可重复收集的结构化信号
按不同时间窗口做了很多类似聚合,看起来很丰富 重复可能很大,新增信息不多 缩成各自角色不同的特征组合

所以,好特征更接近的不是 看起来很强的特征,而是 能被正当且稳定地再次使用的特征。

还是用客户流失问题来读,可以像下面这样。

特征候选 signal noise 时点正当性 重复 运营复现性 一次判断
visits_30d 有 较低 是 一般 高 优先保留候选
customer_id 低 看起来低,但泛化弱 是 低 高 通常排除
contract_cancelled_at 看起来很强 低 否 一般 低 排除
agent_note_score 可能有 可能高 视情况而定 一般 可能低 保留观察 / 追加审查
同时使用 monthly_spend 与 quarterly_spend 可能有 较低 是 高 高 检查重复

这里重要的不是找出 信号最强的单个特征,而是建立 model 和运营都能一起撑住的特征组合。例如,contract_cancelled_at 虽然看起来信号非常强,却会被剔除;visits_30d 即使不完美,也可能被保留。原因在于后者更正当,也更可重复。

把这些条件再捆成一个图,可以像下面这样。

flowchart TB
  A["候选特征<br/>可能的输入信号"]
  B["信号<br/>与目标相关吗?"]
  C["噪声<br/>是否足够稳定?"]
  D["时机<br/>预测时能获得吗?"]
  E["冗余<br/>是否只是重复信息?"]
  F["运维<br/>在线服务中能稳定复现吗?"]
  G["好的特征候选<br/>值得保留"]

  A --> B
  B --> C
  C --> D
  D --> E
  E --> F
  F --> G

这张图的意思是:不要把 好特征 看成一个神秘属性,而要把它拆成相关性、稳定性、时点正当性、重复管理、运营复现性这五个问题来读。

选好特征时最后还会附着的问题

真正能长期留下来的好特征,通常还要过下面这些问题。

  1. 如果这个特征没有了,model 会不会真的漏掉重要 signal?
  2. 如果这个特征有,但到运营里根本复现不出来,那还有什么意义?
  3. 这个特征是在重复别的特征已经说过的话,还是带来了新的视角?

把这些问题附上之后,feature selection 就会更接近 可解释输入设计,而不是 拼命多收集列。

为什么不能因为能拿到就全放进去

在实务里,数据库列越多,反而越危险的情况非常常见。

例如,假设一个贷款审批 model 拿到了下面这些列。

列 直接塞进去可能出现的问题
customer_id 它只是区分人,不一定能解释泛化模式
loan_approved_at 这是审批结果已经出来后才生成的值,预测时点不能用
default_next_90d 它本身就是 label,放进输入就是 leakage
branch_note_text 在真实运营里,它可能来得晚,格式也很不稳定
monthly_income 它可能是和问题相关的 signal,所以可以作为候选保留

这张表的核心非常简单。

特征选择不是比谁放得多,而是在挑预测时点可以正当使用的信息。

读者最好先用的三个问题

开始做特征选择时,比起复杂算法,更重要的是问题顺序。

1. 这条信息在预测时点真的能用吗

首先要检查的是时点。

  • 它是不是结果出来之后才会存在?
  • 它是不是人事后补上的判断?
  • 它是不是看完整个测试数据后才生成的值?

scikit-learn 的 common pitfalls 文档说明:包括 feature selection 在内的 preprocessing 步骤,都应该只用训练数据;一旦把测试数据拉进来,性能就会被乐观地膨胀。

可以这样记。

如果把预测那一刻还不知道的信息放进输入里,那就不是会预测的 model,而是提前偷看答案的 model。

2. 这条信息是不是和问题相关的 signal

第二个问题是相关性。

并不是所有列都一样重要。

  • 它是不是直接接到问题上的行为记录?
  • 它是不是只是在区分人或物体的编号?
  • 它是不是几乎总是同一个值,所以信息量很小?
  • 它是不是几乎在重复另一个列的意思?

scikit-learn 文档提供了减少 low variance 特征、通过 univariate statistics 打分的特征、以及通过 model importance 减少特征的方法。但在这一节里,比起算法,重点仍然是先理解 为什么这些特征可以被减少。

3. 这条信息能不能在运营里稳定拿到

第三个问题是现场性。

有些特征在训练数据集里看起来确实存在,但到了真实 service 里却很难每次稳定获得。

  • 收集延迟是否经常发生?
  • 会不会因为需要人工输入而让质量大幅波动?
  • 会不会因为隐私或成本问题而很难在运营里使用?
  • 如果每次推理都要拉一次,会不会把 latency 拉高?

最终,feature selection 不只是数据科学问题,它也是 service 设计问题。

这个判断流程的核心不是算法,而是检查顺序。尤其重要的是:先问 预测时点能不能用。

feature selection 和 preprocessing 到底哪里不同

读者很容易把这两件事混在一起想。但它们问的问题不同。

区分 先问的问题 例子
feature selection 要采用哪些列? 去掉 ID,去掉事后信息,排除太弱的特征
preprocessing 被采用的列要改造成什么形式? 缺失值处理、scale 调整、类别编码

feature selection 是 先把入口定下来,而 preprocessing 是 把选中的输入修整成 model 更容易读的样子。

如果把这个差别简单画出来,就是下面这样。

flowchart TD
  A["原始列<br/>所有可用字段"]
  B["特征选择<br/>保留 / 删除 / 延后"]
  C["选中的特征<br/>可用的输入集合"]
  D["预处理<br/>缩放 / 填补 / 编码"]
  E["模型输入<br/>可直接训练"]

  A --> B
  B --> C
  C --> D
  D --> E

什么先留,什么先删

在复杂算法之前,feature selection 也意味着先把 为什么留下 和 为什么保留观察或删除 说清楚。

输入候选的状态 先做的判断 原因
在预测时点已经能拿到 优先检查 因为它是 service 输入的可复现候选
只有答案出来后才出现 删除 因为作为事后信息,leakage 风险高
像 ID 一样只区分个体 通常删除 因为它更接近个体识别,而不是泛化模式
装着和问题相关的行为信号 优先考虑保留 因为它可能装着和目标相连的模式
收集延迟或人工输入波动很大 保留观察或补强检查 因为在运营里难以稳定复现
几乎在重复别的特征意思 考虑压缩 因为重复可能比新增信息更大

这个表里的顺序很重要。比起先问 看起来和问题有关吗,更应该先问 在预测时点能不能被正当地使用。这样就会先检查输入的正当性,而不是先被性能数字带走。

细部学习内容

在学术语境里怎样整理它的含义

在入门书里,variable 和 feature 常常会被写得几乎像同一个词。但在学术语境里,人们有时会把它们稍微区分开。

Guyon 和 Elisseeff 的经典综述把 raw input variables 和 constructed features 分开。把这个差别翻成入门层次,可以像下面这样理解。

表达 面向读者的理解
变量(variable) 原本给定的输入列或测量值
特征(feature) 直接使用这个变量,或把它加工后拿来做 model 输入的表达

例如,可以这样读。

原始值 从学术上看 在 model 输入里的角色
year_of_birth, current_year raw variable 还没计算
age = current_year - year_of_birth constructed feature model 会直接读的特征

特征并不只是列名,它指向的是整个 被投进学习里的输入表达。所以,feature selection 有时会被用作一个更宽的词:它同时包括 原始变量里留哪些 和 加工特征里采用哪些。

从学术上,feature selection 的目的也会被整理得更清楚。Guyon 和 Elisseeff 把它的目的说明成下面这样。

  1. 提高 prediction performance
  2. 建立更快、成本更低的 predictor
  3. 帮助理解生成这些数据的过程

这三个目的在实务里也会直接延续下来。

学术目的 在实务里出现的样子
改善预测性能 减少噪音,让性能更稳定
降低计算成本 降低训练时间和推理时间
提高可理解性 更容易解释判断到底用了什么信息

还有一点非常重要,就是 relevance 和 usefulness 并不总是同一个意思。

表达 含义
relevance 这个特征和目标之间到底有什么连接
usefulness 这个特征在当前 model 和当前特征集合里,实际上有没有帮到忙

例如,如果两个列几乎装着同样的信息,那么它们都可能有 relevance。但在真正构造 predictor 时,也许只留一个就已经足够。剩下那个就会变成 虽然相关,但额外 usefulness 较低 的特征。

这个区分说明:feature selection 不是 把最相关的列全部留下,而是 找出重复低、放在一起也真的有帮助的子集(subset)。

列很多并不保证输入更好的场景

案例 1. 在流失预测表里,列越多反而越危险

订阅服务团队正在做客户流失预测 model。人最先使用的标准,是 最近登录次数、咨询频率、是否支付失败、套餐变更历史 这样的信号。

但把数据库打开之后,会发现可用列远远更多。像 customer_id、解约完成时间、客服事后留下的备注、下个月是否逾期 这些都放进去时,表面上会显得信息更丰富。但这些列里,既有只是区分人的编号,也有只有结果发生后才会出现的值,还有在真实预测时点根本还不知道的信息。

在这个场景里,feature selection 不再是 尽量多放,而是 只留下可以正当地使用的信号。先看它在预测时点是否可用,再看它是否和问题相关,最后再看它能不能在运营里稳定采集。走完这一套,虽然表面上信息变少了,但实际上会得到一个更容易泛化的输入空间。

可检查的结果也很清楚。只要比较包含泄漏列和排除泄漏列时的验证分数,就可能看出:最开始那个很高的分数为什么其实是幻觉。再回头检查保留下来的特征列表,也能说明哪些列是真正的行为信号,哪些列其实是事后信息。

flowchart TD
  A["很多流失相关列"]
  B["把 ID 和事后字段也放进去"]
  C["由于泄漏,分数可能看起来很高"]
  D["只保留预测时可用的信号"]
  E["移除 ID、标签、结果发生后的字段"]
  F["重新检查 验证 分数"]
  G["解释为何保留后的特征列表更安全"]

  A --> B --> C
  A --> D --> E --> F --> G

先筛掉特征候选的标准

先用实务 heuristic 来找优先丢弃候选

在做复杂计算之前,下面这些特征就很值得先检查。

先检查的特征 为什么要小心
ID、订单号、客户编号 它能区分个体,但可能离泛化模式很远
结果之后才出现的列 它会制造 leakage
几乎总是一样的列 信息量可能太小
意义重复的列 解释变长了,但实际收益可能不大
在真实运营里收集不稳定的列 到 service 阶段可能难以复现

这个列表不是数学公式,而是读者第一次打开数据时就能立刻拿来用的检查表。

用一个小例子挑一挑特征候选

下面是一个假设客户流失预测问题的很小例子。

列 纳入判断 原因
customer_id 排除 标识符
visits_30d 纳入 最近活动信号
support_tickets_30d 纳入 不满或流失征兆信号
contract_cancelled_at 排除 已经发生结果之后才有的值
promo_code_used_30d 保留观察 意义会随场景变化,需要追加检查
churn_next_month 排除 label

这个例子里,重要的不是 纳入 本身,而是能不能解释 为什么纳入或为什么排除。

直接应用好特征标准

慢慢检查好特征的五个标准

下面五组练习,是为了把前面看到的标准,按 小场景 -> 先问什么 -> 一次判断 -> 解释 的顺序重新抓回来。重点不是背一个答案,而是确认自己能不能一步一步说明 为什么这个特征留下来,而那个特征会被保留观察或淘汰。

一开始最好不要急着直接选答案,而是故意把下面两个步骤分开看。

  1. 这个特征表面上为什么看起来不错?
  2. 即使如此,又该根据什么理由决定留下、保留观察、还是删除?

如果先把这个判断顺序压成一个短流程,可以读成下面这样。

flowchart TB
  A["特征候选"]
  B["为什么看起来有用?"]
  C["五项检查<br/>信号 / 噪声 / 时点<br/>重复 / 运维"]
  D["保留"]
  E["暂缓"]
  F["移除"]

  A --> B
  B --> C
  C -->|明确且可用| D
  C -->|还需判断| E
  C -->|有风险或不适合| F

这张图的作用,不是让你 直接猜答案,而是先抓住 先说为什么它看起来不错,再用五个标准去分成留下 / 保留观察 / 删除 这样的顺序。下面五个小练习,就是按这条线慢慢拆开的。

1. 有没有 signal

例子:

在在线课程流失预测里,比较下面两个特征候选。

特征候选 观察
videos_completed_7d 最近 7 天完课数越少,流失越多的趋势看起来更明显
user_id_suffix 用户 ID 末位数字和流失之间看不出明显差异

先问什么:

  • 这两个特征里,哪个更像在装 行为变化?
  • 哪个更容易用语言解释它为什么和问题有关?

在这个场景里,videos_completed_7d 是更好的候选,因为最近学习行为看起来和流失相关。相反,user_id_suffix 虽然也是数字,但它说不出和问题有关的 signal。

练习:

在下面两个候选里,选出哪个更应该先作为一次候选留下,并把判断拆成两句话写下来。

候选 备注
failed_logins_14d 最近 2 周登录失败次数
account_number_last_digit 账号末位数字

写的时候顺序可以这样:

  1. 表面上两者都是数字,但哪个更接近真实行为信号?
  2. 为什么另一个更像标识符碎片,而不是泛化模式?

解释:

应该先留下 failed_logins_14d。它更像账号访问问题或使用不便带来的行为信号。account_number_last_digit 更接近标识符碎片,很难期待它提供可泛化的模式。

2. noise 会不会太大

例子:

在客户投诉预测里,考虑下面两个特征。

特征候选 观察
complaint_count_30d 由系统自动聚合,记录标准比较一致
agent_feeling_score 客服主观打 1 到 5 分,不同人标准不同

先问什么:

  • 这个值是谁做出来的?
  • 即使同样场景再次发生,它还能按类似标准重复记录吗?

两者都可能和投诉有关,但 agent_feeling_score 更容易出现输入标准摇晃。这样的特征,可能会把 noise 拉得比 signal 更高。

练习:

从下面两个候选里,选出哪个更可能有更大的 noise,并从记录方式角度解释原因。

候选 备注
delivery_delay_minutes 配送系统自动记录
customer_mood_text 客服自由写下的心情备注

写的时候顺序可以这样:

  1. 哪个特征的记录格式更稳定?
  2. 哪个特征更容易因为不同人写法不同,而难以比较同一个意思?

解释:

customer_mood_text 的 noise 风险更大。因为同一个场景也可能被不同人写得不一样,记录格式也不稳定。自动记录的配送延迟时间则有更稳定的标准。

3. 在预测时点到底能不能用

例子:

在支付流失预测里,有下面两个特征候选。

特征候选 观察
failed_payments_30d 预测时点之前的支付失败次数
refund_confirmed_at 只有退款确认后才会产生的时间

先问什么:

  • 在按下预测按钮的那一刻,这个值是不是已经知道?
  • 还是要等结果发生后才会确定?

refund_confirmed_at 在训练表里可能看起来像很强的 signal,但在真实预测时点,它往往还不知道。这样的值不是好特征,而是有 leakage 风险的特征。

练习:

从下面两个候选里,选出时点正当性更高的那个,并顺便写出 为什么另一个危险。

候选 备注
next_14d_spend 接下来 14 天的支付金额
spend_14d_before_prediction 预测前 14 天支付金额

写的时候顺序可以这样:

  1. 两个值里,有没有一个是在看 未来区间?
  2. 真实 service 的预测时点,哪个值能立刻算出来?

解释:

只有 spend_14d_before_prediction 能被正当地使用。next_14d_spend 是把未来信息提前拿来用了,所以不能作为预测 model 的输入。

4. redundancy 会不会太大

例子:

假设在购买流失预测里,准备把下面几列一起放进去。

特征候选 观察
spend_7d 最近 7 天支付金额
spend_30d 最近 30 天支付金额
spend_90d 最近 90 天支付金额

先问什么:

  • 这些特征是在展示不同的行为吗?
  • 还是只是在用不同时间窗重复同一种行为?

这并不意味着三列都没用。但如果它们只是把同样的消费规模按不同时间窗重复表达,那么增加的可能不是信息,而是同一解释的重复。

练习:

从下面两个组合里,选出哪个更需要先做 redundancy 检查,并区分它是在 重复同一种角色,还是 组合不同角色。

组合 备注
spend_7d, spend_30d, spend_90d 都是支付规模聚合
spend_30d, support_tickets_30d, visits_30d 分别代表消费、咨询、访问

写的时候顺序可以这样:

  1. 第一个组合里的三个值,归根到底都在说什么?
  2. 第二个组合为什么能被看成不同视角的互补?

解释:

第一个组合更应该先检查 redundancy。因为它是在用不同时间窗重复同一种角色的特征。第二个组合则展示的是不同行为侧面,所以角色区分更明显。

5. 在运营里能不能再做出来

例子:

在实时推荐 model 里,有下面两个特征候选。

特征候选 观察
clicks_10m 最近 10 分钟点击数可以直接从日志里聚合出来
manual_quality_note 运营人员会在事后手写质量备注

先问什么:

  • 在每次推理时点,这个特征能不能用同样方式重新做出来?
  • 它会不会因为成本、延迟或人工输入而经常缺失或来晚?

manual_quality_note 在训练表里看起来可能很像样,但在真实推理时点它不一定能准时到位。这样的特征,运营复现性就偏低。

练习:

从下面两个候选里,选出哪个运营复现性更高,并分别从 收集稳定性 和 运营成本 两个角度解释。

候选 备注
api_partner_score 外部 API 故障频繁,调用成本高
sessions_1h 可从内部日志按小时稳定计算

写的时候顺序可以这样:

  1. 哪个特征更容易被故障或外部依赖拖着摇晃?
  2. 哪个特征能在每次推理时点更便宜、更稳定地重新计算?

解释:

sessions_1h 是更安全的候选,因为它可以基于内部日志重复计算。api_partner_score 则会因为成本、延迟和故障,在推理时点让复现性摇晃。

用 Python 做一轮特征候选初筛

下面这个例子模仿的是实务里非常常见的第一次筛查。它会先抓出标识符、label 列、结果之后产生的列,以及常数列。

问题场景:

  • 第一次把特征候选摊开时,往往不会立刻知道该把什么留下来作为输入、把什么排除掉

输入(input):

  • 客户行列表 rows

期望输出(output):

  • 每一列的纳入/排除判断
  • 这个判断的原因

确认概念:

  • feature selection 的第一步,是在 model 学习之前,先按危险信号去检查候选列
  • 把标识符、label、结果之后的值先读成排除候选,会更安全

可以改动的值:

  • 如果把 rows 里的 membership_tier 或 country 改成彼此不同的值,原本因为常数列被排除的候选会进入初筛候选列表。
  • 如果加入像 contract_cancelled_at 这样名称以 _at 结尾的事后时点列,它会被时点风险规则抓住。
# 这个例子比较原始特征候选和已选择的特征组合,用来设计模型输入空间。
rows = [
    {
        "customer_id": "C001",
        "visits_30d": 12,
        "support_tickets_30d": 0,
        "contract_cancelled_at": "",
        "membership_tier": "gold",
        "country": "KR",
        "churn_next_month": 0,
    },
    {
        "customer_id": "C002",
        "visits_30d": 3,
        "support_tickets_30d": 2,
        "contract_cancelled_at": "2026-05-14",
        "membership_tier": "gold",
        "country": "KR",
        "churn_next_month": 1,
    },
    {
        "customer_id": "C003",
        "visits_30d": 7,
        "support_tickets_30d": 1,
        "contract_cancelled_at": "",
        "membership_tier": "gold",
        "country": "KR",
        "churn_next_month": 0,
    },
]

target = "churn_next_month"
columns = list(rows[0].keys())

selected = []
rejected = []

for column in columns:
    values = [row[column] for row in rows]
    unique_count = len(set(values))

    if column == target:
        rejected.append((column, "label"))
    elif column.endswith("_id"):
        rejected.append((column, "identifier"))
    elif column.endswith("_at"):
        rejected.append((column, "post-outcome timestamp"))
    elif unique_count == 1:
        rejected.append((column, "constant value"))
    else:
        selected.append((column, "keep as candidate"))

print("selected candidates:")
for name, reason in selected:
    print("-", name, "->", reason)

print()
print("rejected candidates:")
for name, reason in rejected:
    print("-", name, "->", reason)

执行结果如下。

selected candidates:
- visits_30d -> keep as candidate
- support_tickets_30d -> keep as candidate

rejected candidates:
- customer_id -> identifier
- contract_cancelled_at -> post-outcome timestamp
- membership_tier -> constant value
- country -> constant value
- churn_next_month -> label

这个输出并不意味着 好特征已经被完全决定好了。它只是让读者在第一次打开数据表时,知道该先怀疑什么。

细部学习内容补充

在 scikit-learn 里常看到的特征选择方式是什么

在实务里,下面这些方式会经常出现。

方式 非常短的说明 在这一节里的位置
low variance 去除 减少几乎不变的列 只介绍直觉
univariate selection 给每列打分,再选一部分 只介绍直觉
model-based selection 根据 model 给出的重要度来减少 只介绍直觉
recursive feature elimination 反复删掉重要度较低的列 只介绍名字

这里要留下的标准不是把这些算法背下来,而是先抓住:能不能说明为什么这样选。 算法只是下一步帮忙的工具。

检查清单

  • 你是不是先在检查,这一列在预测时点到底存在不存在?
  • 就算看起来像有 signal,那些在运营里无法稳定收集的特征,你有没有另外标出来?
  • 你能不能把 feature selection 和 preprocessing 分开解释成 留下什么 和 怎样改造?
  • 预测时点不知道的列是否已经去掉?
  • label 或 label 之后的信息有没有混进去?
  • 标识符和常数列有没有先检查?
  • 留下来的特征是否都能在真实 service 里稳定拿到?
  • 有没有把 feature selection 和 preprocessing 混成一件事?
  • 你能不能说明,特征(feature)是把现实信息改造成 model 输入的表达,而 feature selection 更接近 挑出可以正当地使用的输入,而不是 多放一点?
  • 你能不能说明,为什么最先要检查 leakage、相关性和运营可用性?

出处与参考资料