文章

特征工程入门——从原始数据到特征矩阵

特征工程入门——从原始数据到特征矩阵

特征工程入门——从原始数据到特征矩阵

深入理解特征工程的重要性,掌握使用 Featuretools 进行自动特征生成的方法。从第一性原理出发,理解为什么需要特征工程,以及各种特征方法为什么有效。

目录

  1. 什么是特征工程?
  2. 特征工程的常见方法
  3. Featuretools 自动特征生成
  4. Featuretools 的工作机制
  5. 特征工程实战示例
  6. 特征工程的最佳实践
  7. 常见问题与解决方案

1. 什么是特征工程?

1.1 特征的本质

特征的定义:对原始数据的抽象表示。

这句话包含了三个关键词:

  1. 原始数据:未经处理的原始信息
  2. 抽象:提取关键信息,去除细节
  3. 表示:转化为模型可理解的形式

例子:理解特征的本质

原始数据(活动记录表):

1
2
3
4
5
6
7
人员ID | 时间       | 地点   | 类型   | 交通工具
-------|-----------|--------|--------|----------
001    | 20XX-01-15| A市   | 旅游   | 飞机
001    | 20XX-02-20| B市   | 商务   | 高铁
001    | 20XX-03-10| A市   | 旅游   | 飞机
001    | 20XX-04-05| C市   | 旅游   | 飞机
001    | 20XX-05-12| A市   | 探亲   | 火车

特征工程后(特征向量):

1
2
3
人员ID | 活动次数 | 不同城市数 | 最常见类型 | 最常见交通工具 | 平均间隔天数
-------|-----------|-----------|-----------|--------------|------------
001    | 5         | 3         | 旅游      | 飞机         | 28

关键理解

  • 原始数据:5条记录(详细信息)
  • 特征:5个数值(抽象概括)
  • 信息压缩:从多条记录压缩为单个特征向量
  • 模式提取:提取了”频率”、”多样性”、”模式”等信息

1.2 为什么不能直接用原始数据?

问题1:数据格式不统一

原始数据示例

1
2
3
4
5
样本一:
  - 姓名:"某甲"(文本)
  - 年龄:30(数值)
  - 出生日期:"19XX-05-15"(日期)
  - 是否高风险:True(布尔)

问题

  • 模型只能处理数值型输入
  • 文本、日期、布尔值需要转换

特征工程后

1
2
3
4
5
样本一:
  - 年龄:30(数值,可直接使用)
  - 年龄组:3(编码:20-30岁=3)
  - 出生年份:1993(从日期提取)
  - 是否高风险:1(布尔转数值:True=1)

问题2:数据包含噪声和冗余

原始数据示例

1
2
3
4
样本二的活动记录:
  - 记录1:20XX-01-15, A市, 旅游, 飞机, 经济舱, 窗口座位, ...
  - 记录2:20XX-02-20, B市, 商务, 高铁, 一等座, 过道座位, ...
  ...

问题

  • 包含大量细节(座位号、舱位等级等)
  • 这些细节可能与目标变量(风险类型)无关
  • 噪声会干扰模型学习

特征工程后

1
2
3
4
5
样本二的特征:
  - 活动次数:5(关键信息)
  - 不同城市数:3(关键信息)
  - 最常见类型:旅游(关键信息)
  - (去除了座位号、舱位等无关细节)

问题3:数据粒度不匹配

原始数据:多条记录(一人多记录)

模型需求:每个样本一个特征向量(一人一向量)

特征工程的作用:将多条记录聚合为单个特征向量

流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
原始数据(多表结构)
    ↓
┌─────────────────────────────────┐
│  人员信息表(1条记录/人)        │
│  - 姓名、年龄、标识号          │
└─────────────────────────────────┘
    ↓
┌─────────────────────────────────┐
│  活动记录表(多条记录/人)     │
│  - 记录1:时间、地点、类型        │
│  - 记录2:时间、地点、类型        │
│  - 记录3:时间、地点、类型        │
│  ...                            │
└─────────────────────────────────┘
    ↓
特征工程(聚合)
    ↓
┌─────────────────────────────────┐
│  特征矩阵(1条记录/人)          │
│  - COUNT(活动记录) = 5         │
│  - NUM_UNIQUE(地点) = 3          │
│  - MODE(类型) = "旅游"           │
│  ...                            │
└─────────────────────────────────┘

1.3 特征工程的重要性

名言:”数据决定上限,模型决定逼近上限的能力”

理解

  • 数据(特征)决定上限:如果特征不好,模型性能的上限就很低
  • 模型决定逼近上限的能力:好的模型能更好地逼近这个上限

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
场景1:特征质量差
  - 特征:随机数(与目标无关)
  - 模型:XGBoost(最好的模型)
  - 结果:准确率 ≈ 50%(随机猜测)
  → 特征差,再好的模型也无法弥补 ❌

场景2:特征质量好
  - 特征:与目标高度相关
  - 模型:简单决策树
  - 结果:准确率 = 85%
  → 特征好,简单模型也能表现好 ✅

场景3:特征质量好 + 模型好
  - 特征:与目标高度相关
  - 模型:XGBoost
  - 结果:准确率 = 92%
  → 特征好 + 模型好 = 最佳性能 ✅

关键洞察:特征工程通常是提升模型性能最有效的方法。

1.4 特征工程的目标

目标1:提取有用信息

  • 保留与目标变量相关的信息
  • 去除无关信息和噪声

目标2:降低模型学习难度

  • 让模式更容易被模型发现
  • 减少模型需要学习的复杂度

例子:时间特征工程

原始数据

1
日期:"20XX-01-15"

特征工程后

1
2
3
4
5
6
7
- 年份:2023
- 月份:1
- 日期:15
- 星期几:星期日
- 是否节假日:是
- 季度:Q1
- 是否周末:是

为什么这样更好?

  • 模型更容易学习”周末”、”节假日”等模式
  • 比直接使用日期字符串更容易理解
  • 提取了与目标变量可能相关的信息

2. 特征工程的常见方法

2.1 统计特征的本质

为什么统计特征有效?

核心原理:用少量数值概括大量数据的分布特征。

例子:活动次数(COUNT)

原始数据

1
2
3
4
5
6
样本一的活动记录:
  - 记录1:20XX-01-15
  - 记录2:20XX-02-20
  - 记录3:20XX-03-10
  - 记录4:20XX-04-05
  - 记录5:20XX-05-12

特征

1
COUNT(活动记录) = 5

为什么有效?

  • 信息压缩:5条记录 → 1个数值
  • 模式提取:反映了”活跃度”(次数多 = 活跃)
  • 与目标相关:活跃度可能与风险相关

常见统计特征及其含义

COUNT(计数)

  • 定义:记录的数量
  • 含义:频率、活跃度
  • 例子COUNT(活动记录) = 5 → 活动5次

SUM(求和)

  • 定义:数值的总和
  • 含义:总量
  • 例子SUM(交易金额) = 50000 → 总交易金额5万

MEAN(平均值)

  • 定义:数值的平均值
  • 含义:平均水平
  • 例子MEAN(间隔天数) = 28 → 平均28天一次

MAX/MIN(最大值/最小值)

  • 定义:数值的最大值/最小值
  • 含义:极值、范围
  • 例子MAX(间隔天数) = 60 → 最长间隔60天

STD(标准差)

  • 定义:数值的离散程度
  • 含义:稳定性、规律性
  • 例子STD(间隔天数) = 10 → 间隔比较规律(标准差小)

可视化理解

1
2
3
4
5
6
7
8
9
10
11
12
13
原始数据分布:
间隔天数:[20, 25, 30, 28, 32]

统计特征:
- MEAN = 27(平均水平)
- STD = 4.5(离散程度小,规律)
- MAX = 32(最大值)
- MIN = 20(最小值)

含义:
- 平均27天一次(频率)
- 标准差小(规律性强)
- 范围20-32天(稳定性)

2.2 类别特征的本质

MODE(众数)

定义:出现最频繁的值。

为什么有效?

  • 反映了”典型情况”、”最常见模式”
  • 从无序的类别数据中提取有序信息

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
样本一的活动类型:
  - 记录1:旅游
  - 记录2:商务
  - 记录3:旅游
  - 记录4:旅游
  - 记录5:探亲

MODE(类型) = "旅游"(出现3次,最多)

含义:
- 最常见的类型是"旅游"
- 反映了人员的行为模式
- 可能与风险类型相关(旅游 vs 商务)

NUM_UNIQUE(唯一值数量)

定义:不同取值的数量。

为什么有效?

  • 反映了”多样性”、”离散程度”
  • 多样性可能与目标变量相关

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
样本一去过的城市:
  - A市、B市、C市、A市、A市

NUM_UNIQUE(城市) = 3(去过3个不同城市)

样本二去过的城市:
  - A市、A市、A市、A市、A市

NUM_UNIQUE(城市) = 1(只去过1个城市)

对比:
- 样本一:多样性高(去过多个城市)
- 样本二:多样性低(只去一个城市)
- 多样性可能与风险相关

2.3 时间特征的本质

DIFF(差值)

定义:相邻时间点的差值。

为什么有效?

  • 反映了”变化量”、”趋势”、”稳定性”
  • 时间间隔的模式可能包含重要信息

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
样本一的活动时间:
  - 20XX-01-15
  - 20XX-02-20(间隔36天)
  - 20XX-03-10(间隔18天)
  - 20XX-04-05(间隔26天)
  - 20XX-05-12(间隔37天)

DIFF特征:
- MEAN(DIFF) = 29.25天(平均间隔)
- STD(DIFF) = 8.5天(标准差,反映规律性)
- MIN(DIFF) = 18天(最短间隔)
- MAX(DIFF) = 37天(最长间隔)

含义:
- 平均约一个月一次(频率)
- 间隔比较规律(标准差小)
- 反映了行为模式

TREND(趋势)

定义:时间序列的方向性(增长/下降/稳定)。

为什么有效?

  • 反映了”动态变化”、”发展趋势”
  • 趋势可能与目标变量相关

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
样本一的每月活动次数:
  - 1月:1次
  - 2月:1次
  - 3月:1次
  - 4月:2次(增加)
  - 5月:3次(增加)

TREND = 上升趋势

含义:
- 活动频率在增加
- 可能反映了行为变化
- 可能与风险相关(频率增加 = 风险增加?)

2.4 组合特征的本质

为什么需要组合特征?

核心原理:特征的交互可能包含非线性的重要信息。

例子:年龄 × 收入

单独特征

1
2
年龄:30岁
收入:50000元/月

组合特征

1
年龄 × 收入 = 1500000

为什么组合特征可能更有用?

  • 非线性关系:年龄和收入的交互可能比单独特征更有预测力
  • 例子
    • 30岁 + 50000元 = 正常(年轻高收入)
    • 50岁 + 50000元 = 可能异常(年龄大但收入不高)
    • 组合特征能捕捉这种交互

可视化理解

1
2
3
4
5
6
单独特征(线性):
年龄 → 风险(线性关系)
收入 → 风险(线性关系)

组合特征(非线性):
年龄 × 收入 → 风险(非线性关系,更复杂)

常见的组合方式

乘积特征A × B

  • 适用于:两个数值特征的交互

条件特征IF A THEN B

  • 适用于:条件依赖关系

字符串拼接A + "_" + B

  • 适用于:类别特征的组合

例子

1
2
3
4
5
6
特征A:最常去城市 = "A市"
特征B:最常见类型 = "旅游"

组合特征:
- 城市_类型 = "A市_旅游"
- 可能比单独特征更有预测力

2.5 特征工程的理论基础

信息论视角

好特征的标准

  • 高信息量:特征与目标变量的相关性高
  • 低冗余:特征之间的相关性低(信息不重复)

互信息(Mutual Information)

  • 量化特征与目标的信息关联
  • 值越大,特征越有用

例子

1
2
3
4
5
6
7
8
9
10
特征1:年龄
  - 与目标变量的互信息:0.3(中等相关)

特征2:随机数
  - 与目标变量的互信息:0.001(几乎无关)
  → 应该删除

特征3:活动次数
  - 与目标变量的互信息:0.6(高度相关)
  → 应该保留

维度灾难

问题:为什么不能无限制地增加特征?

原因

  • 特征维度增加 → 需要的样本数指数级增长
  • 例子
    • 1个特征:需要10个样本
    • 2个特征:需要100个样本(10²)
    • 10个特征:需要10¹⁰个样本(不可行)

解决:特征选择

  • 在信息量和维度之间平衡
  • 只保留最重要的特征

偏差-方差权衡在特征工程中的体现

特征太少

  • 信息不足 → 高偏差(欠拟合)
  • 例子:只用1个特征,无法捕捉复杂模式

特征太多

  • 噪声干扰 → 高方差(过拟合)
  • 例子:用1000个特征,但只有100个样本

平衡点

  • 通过交叉验证找到最优特征数量
  • 例子:20-50个特征可能是最优的

3. Featuretools 自动特征生成

3.1 什么是 Featuretools?

Featuretools 的设计理念

核心思想:自动化特征工程,减少人工工作量。

传统方法 vs Featuretools

传统方法(手工特征工程)

流程

1
2
3
4
5
6
7
8
9
10
11
手工编写代码
    ↓
为每个特征编写计算逻辑
    - 活动次数:统计记录数
    - 平均间隔:计算时间差平均值
    - 最常去地点:统计众数
    - 不同城市数:统计唯一值数量
    ↓
需要写大量代码
    ↓
容易遗漏重要特征组合

Featuretools(自动特征生成)

流程

1
2
3
4
5
6
7
8
9
10
11
定义实体集(EntitySet)
    ↓
设置目标实体(persons)
    ↓
设置特征深度(max_depth=2)
    ↓
Featuretools自动生成
    ↓
输出:特征矩阵 + 特征定义
    - 自动生成所有可能的特征组合
    - 自动发现隐藏模式

为什么需要自动特征生成?

问题1:手工特征工程工作量大

  • 需要领域专家知识
  • 容易遗漏重要特征
  • 难以维护和更新

问题2:特征组合爆炸

  • 2个特征的组合:A, B, A×B
  • 10个特征的组合:2¹⁰ = 1024种可能
  • 手工无法穷尽所有组合

问题3:特征发现困难

  • 人类可能想不到某些特征组合
  • 自动生成可以发现隐藏模式

3.2 实体集(EntitySet)构建

理解实体和关系

实体(Entity):数据表

关系(Relationship):表之间的关联

例子:多表数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌─────────────────────┐
│  人员信息表         │
│  (persons)          │
│  - 标识号(主键) │
│  - 姓名             │
│  - 年龄             │
└─────────────────────┘
         │
         │ 1对多关系
         ↓
┌─────────────────────┐
│  活动记录表       │
│  (activity_records)   │
│  - 记录ID(主键)   │
│  - 标识号(外键) │
│  - 时间             │
│  - 地点             │
│  - 类型             │
└─────────────────────┘

实体集构建流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
步骤1:创建实体集
    创建空的EntitySet(标识:'人员数据')
    ↓
步骤2:添加实体(表)
    添加人员实体(persons)
        - 主键:标识号
        - 数据:人员信息表
    ↓
    添加活动记录实体(activity_records)
        - 主键:记录ID
        - 数据:活动记录表
    ↓
步骤3:添加关系
    建立关系:persons.标识号 ← → activity_records.标识号
        (一个人员对应多条活动记录)
    ↓
步骤4:完成实体集构建
    实体集包含:2个实体 + 1个关系

关键理解

  • 实体:数据表
  • 关系:表之间的外键关联
  • 目标实体:最终要生成特征的实体(通常是主表,如persons)

3.3 深度特征合成(DFS)

max_depth 参数的含义

depth = 1(深度1)

  • 只对目标实体本身应用特征函数
  • 例子
    1
    2
    3
    
    目标实体:persons
    特征:AGE(年龄)、GENDER(性别)
    → 直接特征
    

depth = 2(深度2)

  • 对目标实体 + 相关实体应用特征函数
  • 例子
    1
    2
    3
    4
    5
    6
    
    目标实体:persons
    相关实体:activity_records
      
    特征:
    - COUNT(activity_records) = 5(深度2:聚合特征)
    - MODE(activity_records.地点) = "A市"(深度2:聚合特征)
    

depth = 3(深度3)

  • 对更深层的实体应用特征函数
  • 例子
    1
    2
    3
    4
    5
    6
    7
    8
    
    目标实体:persons
    相关实体1:activity_records
    相关实体2:visa_records
      
    特征:
    - COUNT(activity_records) = 5(深度2)
    - COUNT(visa_records) = 3(深度2)
    - COUNT(activity_records) × COUNT(visa_records) = 15(深度3:交叉特征)
    

特征爆炸问题

问题:深度增加 → 特征数量指数级增长

例子

1
2
3
depth = 1:10个特征
depth = 2:100个特征(10²)
depth = 3:1000个特征(10³)

解决方案

  1. 控制深度:通常 depth = 2 就够用了
  2. 特征选择:训练后选择重要特征
  3. 特征过滤:根据字段配置过滤不需要的特征

4. Featuretools 的工作机制

4.1 特征函数类型

聚合函数(Aggregation Primitives)

定义:对相关实体的多条记录进行聚合。

例子

1
2
3
4
5
6
7
目标实体:persons(人员)
相关实体:activity_records(活动记录)

聚合函数:
- COUNT(activity_records) → 活动次数
- MEAN(activity_records.间隔天数) → 平均间隔
- MODE(activity_records.地点) → 最常去地点

工作原理

1
2
3
4
5
6
7
8
9
10
11
12
样本一的活动记录:
  - 记录1:A市
  - 记录2:B市
  - 记录3:A市
  - 记录4:A市

MODE(activity_records.地点):
  1. 统计每个地点的出现次数
     - A市:3次
     - B市:1次
  2. 返回出现次数最多的地点
     → "A市"

变换函数(Transform Primitives)

定义:对单个实体的字段进行变换。

例子

1
2
3
4
5
6
目标实体:persons

变换函数:
- YEAR(出生日期) → 出生年份
- MONTH(出生日期) → 出生月份
- DIFF(时间1, 时间2) → 时间差

4.2 特征生成过程:”先收集再过滤”

为什么需要”先收集再过滤”?

Featuretools 的限制

  • API不支持字段级别的特征函数配置
  • 只能传入全局的特征函数列表
  • 会为所有字段生成所有类型的特征

例子

Featuretools API调用

1
2
3
4
5
6
7
8
9
10
调用深度特征合成(DFS)
    ↓
输入参数:
    - 实体集:es
    - 目标实体:persons
    - 聚合函数:['mean', 'mode'](全局配置)
    ↓
限制:不能指定字段级别的配置
    - 不能指定:字段A只用mean,字段B只用mode
    - 只能全局配置:所有字段都用mean和mode

结果

  • Featuretools会为所有字段生成mean和mode特征
  • 例如:MEAN(地点)、MODE(地点)、MEAN(时间)、MODE(时间)…
  • 但有些特征可能没有意义(如MEAN(地点))

解决方案:”先收集再过滤”

流程

1
2
3
4
5
6
7
8
9
10
11
12
13
步骤1:传入所有可能的特征函数
  agg_primitives = ['count', 'sum', 'mean', 'max', 'min', 'mode', ...]
  ↓
步骤2:Featuretools生成所有可能的特征
  生成:MEAN(地点)、MODE(地点)、MEAN(时间)、MODE(时间)、...
  ↓
步骤3:根据字段配置过滤
  配置:地点只用mode,时间只用mean
  过滤:保留MODE(地点)、MEAN(时间)
  删除:MEAN(地点)、MODE(时间)
  ↓
步骤4:得到最终特征矩阵
  只包含需要的特征

为什么这样做?

  • Featuretools的设计就是这样的(不支持字段级配置)
  • 过滤阶段很高效(只是过滤DataFrame的列)
  • 代码简单,充分利用Featuretools的能力

5. 特征工程实战示例

5.1 多表数据的特征提取流程

完整流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
原始数据(Excel文件)
    ↓
┌─────────────────────────────────┐
│  数据加载                        │
│  - 读取4个sheet页                │
│  - 解析为DataFrame               │
└─────────────────────────────────┘
    ↓
┌─────────────────────────────────┐
│  数据预处理                      │
│  - 清洗(去除空值、异常值)      │
│  - 类型转换(文本→类别)         │
│  - 格式规范化                    │
└─────────────────────────────────┘
    ↓
┌─────────────────────────────────┐
│  实体集构建                      │
│  - 创建EntitySet                 │
│  - 添加实体(表)                 │
│  - 添加关系(外键)               │
└─────────────────────────────────┘
    ↓
┌─────────────────────────────────┐
│  特征生成(DFS)                  │
│  - 设置max_depth=2               │
│  - 传入特征函数列表               │
│  - 自动生成特征                   │
└─────────────────────────────────┘
    ↓
┌─────────────────────────────────┐
│  特征过滤                        │
│  - 根据字段配置过滤               │
│  - 去除不需要的特征               │
└─────────────────────────────────┘
    ↓
┌─────────────────────────────────┐
│  特征矩阵                        │
│  - 每个人员一行                  │
│  - 每个特征一列                  │
│  - 保存特征定义                  │
└─────────────────────────────────┘

具体例子

输入数据

人员信息表(persons)

1
2
3
4
标识号    | 姓名 | 年龄
-----------|------|------
ID_A  | 某甲 | 30
ID_B  | 某乙 | 25

活动记录表(activity_records)

1
2
3
4
5
6
7
记录ID | 标识号    | 时间       | 地点 | 类型
-------|-----------|-----------|------|------
1      | ID_A | 20XX-01-15| A市 | 旅游
2      | ID_A | 20XX-02-20| B市 | 商务
3      | ID_A | 20XX-03-10| A市 | 旅游
4      | ID_B | 20XX-01-10| C市 | 旅游
5      | ID_B | 20XX-02-15| C市 | 旅游

特征生成过程

步骤1:实体集构建

流程

1
2
3
4
5
6
7
8
9
10
11
12
13
创建实体集(EntitySet)
    ↓
添加人员实体(persons)
    - 主键:标识号
    - 数据:人员信息表
    ↓
添加活动记录实体(activity_records)
    - 主键:记录ID
    - 数据:活动记录表
    ↓
添加关系
    - persons.标识号 ← → activity_records.标识号
    - 关系类型:一对多(一个人员对应多条记录)

步骤2:深度特征合成

流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
调用深度特征合成(DFS)
    ↓
输入参数:
    - 实体集:es
    - 目标实体:persons(人员)
    - 最大深度:2(包含相关实体特征)
    - 聚合函数:['count', 'mean', 'mode', 'num_unique']
    - 变换函数:['diff']
    ↓
自动生成特征
    ↓
输出:
    - 特征矩阵(feature_matrix)
    - 特征定义(feature_defs)

步骤3:生成的特征矩阵

输出特征矩阵

1
2
3
4
标识号    | COUNT(activity_records) | NUM_UNIQUE(activity_records.地点) | MODE(activity_records.类型)
-----------|----------------------|------------------------------|------------------------
ID_A  | 3                    | 2                            | 旅游
ID_B  | 2                    | 1                            | 旅游

特征解释

  • COUNT(activity_records) = 3:某甲有3条活动记录
  • NUM_UNIQUE(activity_records.地点) = 2:去过2个不同城市
  • MODE(activity_records.类型) = "旅游":最常见类型是旅游

5.2 特征命名规则的理解

Featuretools 的特征命名规则

格式FUNCTION(ENTITY.FIELD)

例子

  • COUNT(activity_records):对activity_records实体计数
  • MODE(activity_records.地点):对activity_records实体的”地点”字段取众数
  • MEAN(activity_records.间隔天数):对activity_records实体的”间隔天数”字段取平均值

深度特征命名

深度2特征

  • COUNT(activity_records):直接聚合

深度3特征

  • COUNT(activity_records) × COUNT(visa_records):交叉特征

5.3 特征定义文件的保存与使用

为什么需要保存特征定义?

问题:训练时和预测时特征必须一致

场景

1
2
3
4
5
6
7
8
训练时:
  特征1:COUNT(activity_records)
  特征2:MODE(activity_records.地点)
  → 训练模型

预测时:
  如果特征顺序或名称不一致
  → 模型无法正确预测 ❌

解决方案:保存特征定义文件

保存流程

1
2
3
4
5
6
训练时:
    生成特征定义(feature_defs)
    ↓
    保存到文件(feature_definitions.pkl)
    ↓
    文件包含:特征名称、特征函数、特征结构等信息

加载流程

1
2
3
4
5
6
预测时:
    加载特征定义文件(feature_definitions.pkl)
    ↓
    使用相同的特征定义生成特征
    ↓
    确保特征与训练时完全一致

关键:确保训练和预测时特征完全一致。


6. 数据预处理完整流程

6.1 数据预处理的本质

数据预处理的定义:在特征工程之前,对原始数据进行清洗、转换、分割等操作,使其适合模型训练。

为什么需要数据预处理?

问题:原始数据通常存在各种问题,不能直接用于模型训练。

例子

1
2
3
4
5
6
原始数据的问题:
- 缺失值:某些字段为空
- 异常值:年龄=200岁(不合理)
- 重复值:同一条记录出现多次
- 格式不一致:日期格式"20XX-01-15" vs "2023/1/15"
- 量纲不同:年龄(20-80)vs 收入(1000-50000)

数据预处理的作用

  • 提高数据质量:去除噪声、异常值、重复值
  • 统一数据格式:确保数据格式一致
  • 准备训练数据:将数据分割为训练集和测试集
  • 处理不平衡数据:通过采样解决类别不平衡问题

6.2 数据清洗

异常值处理

异常值的定义:明显偏离正常范围的值。

识别方法

1
2
3
4
5
6
7
8
9
方法1:3σ原则(正态分布)
    超出均值±3σ的数据可能是异常值
    
方法2:IQR方法(四分位距)
    超出Q1-1.5×IQR或Q3+1.5×IQR的数据可能是异常值
    
方法3:业务规则
    年龄>150岁 → 异常值
    收入<0 → 异常值

处理方法

1
2
3
4
5
6
7
8
方法1:删除异常值
    直接删除异常样本
    
方法2:替换异常值
    用中位数、均值或边界值替换
    
方法3:保留异常值
    如果异常值有业务意义(如真实的高收入),保留

重复值处理

问题:同一条记录出现多次。

识别方法

1
2
检查主键是否重复
    如果主键重复 → 重复记录

处理方法

1
2
3
4
5
方法1:删除重复值
    保留第一条,删除后续重复记录
    
方法2:合并重复值
    如果重复记录有不同信息,合并

错误值处理

问题:数据格式错误、类型错误。

例子

1
2
3
4
错误值示例:
- 日期格式:"20XX-01-15" vs "2023/1/15"(格式不一致)
- 类型错误:年龄="30岁"(应该是数值30)
- 范围错误:年龄=-5(负数不合理)

处理方法

1
2
3
4
5
6
7
8
方法1:格式规范化
    统一日期格式、统一单位等
    
方法2:类型转换
    文本转数值、日期字符串转日期对象
    
方法3:范围检查
    检查数值是否在合理范围内

6.3 数据转换

对数变换

适用场景:数据分布严重偏斜(右偏)。

作用:将偏斜分布转换为近似正态分布。

例子

1
2
3
4
5
6
7
8
9
原始数据(收入):
    [1000, 2000, 5000, 10000, 50000]
    ↓
    分布:严重右偏(少数大值)
    
对数变换后:
    [log(1000), log(2000), ...] = [6.9, 7.6, 8.5, 9.2, 10.8]
    ↓
    分布:近似正态分布 ✅

标准化与归一化

标准化(Z-score)

1
2
3
公式:标准化值 = (原始值 - 均值) / 标准差
作用:将数据转换为均值0、标准差1的分布
适用:梯度下降算法、正态分布数据

归一化(MinMax)

1
2
3
公式:归一化值 = (原始值 - 最小值) / (最大值 - 最小值)
作用:将数据映射到[0, 1]范围
适用:相似度计算、神经网络

为什么需要?

  • 消除量纲影响:不同特征的量纲不同(年龄vs收入)
  • 提高训练效率:标准化后的数据训练更快
  • 提高模型性能:归一化后的数据模型性能更好

注意:不是所有模型都需要归一化

  • 需要归一化的模型:神经网络、线性回归、KNN、SVM等(基于梯度下降或距离计算)
  • 不需要归一化的模型:决策树、随机森林、XGBoost(树模型基于排序分裂,不受尺度影响)

训练前归一化的作用

即使树模型不强制要求归一化,但归一化可能带来好处

1. 提升计算效率

  • 直方图加速:某些树模型实现(如LightGBM)使用直方图加速,归一化可能提升效率
  • 数值稳定性:归一化可以提高数值计算的稳定性

2. 特征公平性

  • 权重分配:虽然树模型基于排序,但归一化后所有特征在同一尺度,权重分配更公平
  • 特征重要性:归一化后的特征重要性评估更准确

3. 混合模型场景

  • 特征选择:如果后续需要特征选择或特征融合,归一化后的特征更容易比较
  • 模型组合:如果使用多个模型(树模型+线性模型),归一化可以统一特征尺度

建议

  • 树模型:归一化不是必需的,但可以尝试(可能提升效率或公平性)
  • 梯度下降类模型:归一化是必需的
  • 距离计算类模型:归一化是必需的

6.4 数据分割

训练集与测试集

为什么需要分割?

问题:不能在同一份数据上训练和评估。

原因

  • 过拟合风险:如果在训练数据上评估,模型可能”死记硬背”
  • 泛化能力:需要在未见过的新数据上评估模型

分割方法

1
2
3
4
5
6
7
8
9
方法1:随机分割
    训练集:80%
    测试集:20%
    
方法2:分层分割(分类问题)
    保持各类别比例一致
    
方法3:时间分割(时间序列)
    按时间顺序分割(早期数据训练,后期数据测试)

注意事项

  • 数据泄露:测试集不能参与训练过程
  • 分布一致性:训练集和测试集应该来自同一分布
  • 样本量:测试集要有足够样本量(至少几百个样本)

6.5 数据采样

类别不平衡问题

问题:不同类别的样本数量差异很大。

例子

1
2
3
4
5
6
正类样本:100个
负类样本:10000个
    ↓
    类别不平衡(1:100)
    ↓
    问题:模型可能只学习到负类模式

采样方法

方法1:过采样(Oversampling)

1
2
3
4
5
增加少数类样本
    ↓
    方法:SMOTE(合成少数类样本)
    ↓
    结果:正类样本从100增加到1000

方法2:欠采样(Undersampling)

1
2
3
4
5
减少多数类样本
    ↓
    方法:随机删除多数类样本
    ↓
    结果:负类样本从10000减少到1000

方法3:组合采样

1
2
3
同时使用过采样和欠采样
    ↓
    平衡各类别样本数量

6.6 缺失值处理

缺失值的类型

完全随机缺失(MCAR):缺失与数据值无关。

随机缺失(MAR):缺失与观测到的数据有关。

非随机缺失(MNAR):缺失与数据值本身有关。

处理方法

方法1:删除缺失值

1
2
适用:缺失值很少(<5%)
方法:直接删除包含缺失值的样本或特征

方法2:填充缺失值

1
2
3
4
5
6
7
8
数值型特征:
    - 均值填充
    - 中位数填充
    - 众数填充
    
类别型特征:
    - 众数填充
    - "未知"类别填充

方法3:模型预测填充

1
2
3
4
5
使用其他特征预测缺失值
    ↓
    方法:KNN、回归模型等
    ↓
    结果:更准确的填充值

方法4:保留缺失值

1
2
3
某些模型(如XGBoost)可以自动处理缺失值
    ↓
    不需要填充,模型会学习如何处理缺失值

6.7 数据预处理的完整流程

完整流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
原始数据
    ↓
┌─────────────────────────────────┐
│  数据清洗                        │
│  - 异常值处理                    │
│  - 重复值处理                    │
│  - 错误值处理                    │
└─────────────────────────────────┘
    ↓
┌─────────────────────────────────┐
│  数据转换                        │
│  - 对数变换(如需要)            │
│  - 标准化/归一化                 │
│  - 类型转换                      │
└─────────────────────────────────┘
    ↓
┌─────────────────────────────────┐
│  缺失值处理                      │
│  - 删除或填充                    │
│  - 或保留(XGBoost)             │
└─────────────────────────────────┘
    ↓
┌─────────────────────────────────┐
│  数据分割                        │
│  - 训练集(80%)                 │
│  - 测试集(20%)                 │
└─────────────────────────────────┘
    ↓
┌─────────────────────────────────┐
│  数据采样(如需要)              │
│  - 处理类别不平衡                │
└─────────────────────────────────┘
    ↓
预处理后的数据(可用于特征工程)

关键原则

  • 训练集和测试集分开处理:测试集不能参与训练过程的任何步骤
  • 保存预处理参数:保存均值、标准差、min、max等,用于新数据预处理
  • 一致性:训练和预测时使用相同的预处理方法

7. 特征工程的最佳实践

7.1 如何选择特征函数?

根据字段类型选择

数值型字段

  • COUNT、SUM、MEAN、MAX、MIN、STD
  • 例子:间隔天数 → MEAN(间隔天数)、STD(间隔天数)

类别型字段

  • MODE、NUM_UNIQUE
  • 例子:地点 → MODE(地点)、NUM_UNIQUE(地点)

时间型字段

  • DIFF、TREND
  • 例子:时间 → DIFF(时间)、TREND(时间)

根据业务含义选择

例子:活动记录

字段:地点

  • MODE(地点):最常去的地点(行为模式)
  • NUM_UNIQUE(地点):去过的不同城市数(多样性)

字段:类型

  • MODE(类型):最常见的类型(行为模式)

字段:间隔天数

  • MEAN(间隔天数):平均间隔(频率)
  • STD(间隔天数):间隔的规律性(稳定性)

6.2 如何控制特征数量?

方法1:控制深度

depth = 1:只生成直接特征(数量少) depth = 2:包含聚合特征(数量中等) depth = 3:包含交叉特征(数量多)

建议:通常 depth = 2 就够用了

方法2:特征选择

训练后选择

  1. 训练模型
  2. 提取特征重要性
  3. 只保留TopK重要特征

例子

1
2
3
4
5
6
7
8
9
生成的特征:100个
特征重要性排序:
  - 特征1:0.15(重要)
  - 特征2:0.12(重要)
  - ...
  - 特征50:0.001(不重要)

选择Top20:
  → 只保留前20个重要特征

方法3:字段级配置

配置每个字段使用的特征函数

1
2
3
4
5
6
field_config:
  activity_records:
    地点:
      features: ['mode', 'num_unique']  # 只用这两个
    间隔天数:
      features: ['mean', 'std']  # 只用这两个

效果:减少生成的特征数量

6.3 特征质量如何评估?

方法1:特征重要性

训练模型后查看特征重要性

流程

1
2
3
4
5
6
7
训练XGBoost模型
    ↓
模型自动计算特征重要性(feature_importances_)
    ↓
按重要性值排序(从高到低)
    ↓
得到特征重要性排序列表

例子

1
2
3
4
5
6
特征重要性:
  - MODE(activity_records.地点):0.15(最重要)
  - COUNT(activity_records):0.12
  - MEAN(activity_records.间隔天数):0.10
  - MODE(activity_records.类型):0.08
  - ...(其他特征重要性较低)

方法2:相关性分析

计算特征与目标变量的相关性

流程

1
2
3
4
5
6
7
8
特征矩阵(feature_matrix)
    ↓
计算每个特征与目标变量的相关系数
    ↓
得到相关性值(-1到1之间)
    - 接近1:正相关
    - 接近-1:负相关
    - 接近0:无关

例子

1
2
3
4
特征与目标的相关性:
  - COUNT(activity_records):0.6(高度相关)
  - MODE(activity_records.地点):0.4(中等相关)
  - 随机特征:0.01(几乎无关,应该删除)

方法3:Permutation Importance

打乱特征值,看性能下降

流程

1
2
3
4
5
6
7
8
9
10
11
12
步骤1:计算基准性能
    用原始特征矩阵预测 → 得到基准分数(如F1=0.85)
    ↓
步骤2:打乱某个特征的值
    随机打乱特征A的值 → 得到打乱后的特征矩阵
    ↓
步骤3:用打乱后的数据预测
    用打乱后的特征矩阵预测 → 得到新分数(如F1=0.80)
    ↓
步骤4:计算重要性
    重要性 = 基准分数 - 打乱后分数
    = 0.85 - 0.80 = 0.05(特征A有用)

例子

1
2
3
4
5
6
7
基准性能:F1 = 0.85

打乱特征A后:F1 = 0.80
  → 重要性 = 0.05(特征A有用)

打乱特征B后:F1 = 0.84
  → 重要性 = 0.01(特征B不太重要)

6.4 特征一致性如何保证?

问题:训练和预测时特征不一致

场景

1
2
3
4
5
6
7
8
9
训练时:
  特征顺序:[特征1, 特征2, 特征3]
  特征名称:['COUNT(...)', 'MODE(...)', 'MEAN(...)']

预测时:
  特征顺序:[特征2, 特征1, 特征3]  # 顺序不同
  特征名称:['COUNT(...)', 'MODE(...)', 'MEAN(...)']  # 名称相同

结果:模型预测错误 ❌

解决方案

方案1:保存特征定义文件

  • 训练时保存特征定义
  • 预测时加载并使用相同的定义

方案2:特征对齐

流程

1
2
3
4
5
6
7
8
步骤1:确保特征顺序一致
    查询特征矩阵 → 按训练时的特征顺序重新排列
    ↓
步骤2:确保特征名称一致
    查询特征矩阵 → 使用训练时的特征名称
    ↓
步骤3:完成对齐
    特征顺序和名称与训练时完全一致

方案3:缺失特征处理

流程

1
2
3
4
5
6
7
8
9
10
11
步骤1:检查缺失特征
    训练时的特征列表 vs 查询时的特征列表
    ↓
步骤2:找出缺失的特征
    缺失特征 = 训练特征 - 查询特征
    ↓
步骤3:填充默认值
    为每个缺失特征填充默认值(0或均值)
    ↓
步骤4:完成处理
    查询特征矩阵包含所有训练时的特征

7. 常见问题与解决方案

7.1 特征缺失值处理

问题:特征矩阵中有缺失值

原因

  • 原始数据有缺失值
  • 某些聚合函数无法计算(如所有值都是NaN)

解决方案

方法1:填充默认值

流程

1
2
3
4
5
6
7
8
9
10
检查特征矩阵中的缺失值
    ↓
数值特征:
    - 缺失值 → 填充0或均值
    - 例如:COUNT特征缺失 → 填充0
    - 例如:MEAN特征缺失 → 填充该特征的均值
    ↓
类别特征:
    - 缺失值 → 填充众数或"未知"
    - 例如:MODE特征缺失 → 填充"未知"

方法2:删除缺失值过多的特征

流程

1
2
3
4
5
6
7
计算每个特征的缺失值比例
    ↓
缺失值比例 = 缺失值数量 / 总样本数
    ↓
判断:缺失值比例 > 50%?
    ├─→ 是 → 删除该特征(缺失太多,信息不足)
    └─→ 否 → 保留该特征(填充缺失值)

方法3:在模型训练时处理

说明

  • XGBoost模型可以自动处理缺失值
  • 但需要确保训练和预测时处理方式一致
  • 建议:统一在特征工程阶段处理缺失值

7.2 特征类型转换

问题:特征类型不匹配

场景

1
2
3
4
5
6
7
训练时:
  特征A:整数类型(int)
  
预测时:
  特征A:浮点数类型(float)
  
结果:可能导致预测错误

解决方案

流程

1
2
3
4
5
6
7
检查特征类型
    ↓
统一转换为数值类型
    - 所有特征 → 浮点数类型(float)
    - 或所有特征 → 整数类型(int,如果适用)
    ↓
确保训练和预测时类型一致

7.3 特征对齐问题

问题:训练和预测时特征不一致

场景1:特征顺序不同

问题

1
2
训练时:特征顺序 = [特征1, 特征2, 特征3]
预测时:特征顺序 = [特征2, 特征1, 特征3](顺序不同)

解决流程

1
2
3
4
5
查询特征矩阵
    ↓
按训练时的特征顺序重新排列
    ↓
确保顺序一致

场景2:特征名称不同

问题

1
2
训练时:'COUNT(activity_records)'
预测时:'COUNT(activity_records) '(有空格差异)

解决流程

1
2
3
4
5
标准化特征名称
    - 去除前后空格
    - 统一大小写
    ↓
确保名称一致

场景3:缺少某些特征

问题

1
2
训练时:有特征A、B、C
预测时:只有特征A、B(缺少C)

解决流程

1
2
3
4
5
6
7
8
找出缺失的特征
    缺失特征 = 训练特征集合 - 查询特征集合
    ↓
为每个缺失特征填充默认值
    - 填充0(数值特征)
    - 或使用训练时的默认值
    ↓
确保所有特征都存在

7.4 特征版本管理

问题:特征定义可能变化

场景

1
2
3
4
5
6
7
版本1:使用特征A、B、C训练模型1
版本2:使用特征A、B、D训练模型2(特征C改为D)

问题:
- 模型1需要特征C
- 模型2需要特征D
- 如何管理不同版本的特征?

解决方案

方法1:特征定义文件版本化

1
2
feature_definitions_v1.pkl  # 版本1的特征定义
feature_definitions_v2.pkl  # 版本2的特征定义

方法2:模型元数据中包含特征定义

流程

1
2
3
4
5
6
7
每个模型文件包含:
    - 模型本身(model)
    - 特征定义(feature_definitions)
    - 特征名称列表(feature_names)
    - 版本号(version)
    ↓
模型文件自包含,包含所有必要信息

方法3:特征数据库

流程

1
2
3
4
5
6
7
将特征定义存储在数据库中
    ↓
支持版本管理
    - 不同版本的特征定义
    - 版本查询和切换
    ↓
支持特征定义查询和管理

总结

本文深入介绍了特征工程的核心概念和实践方法。关键要点:

  1. 特征的本质:对原始数据的抽象表示,信息压缩与模式提取
  2. 为什么需要特征工程:原始数据格式不统一、包含噪声、粒度不匹配
  3. 特征工程的重要性:数据决定上限,特征工程是提升性能最有效的方法
  4. Featuretools:自动化特征生成,减少人工工作量
  5. 特征一致性:训练和预测时特征必须完全一致
本文由作者按照 CC BY 4.0 进行授权