数据变换
规范化
定量变量规范化
标准化
标准化即将变量的位置和尺度作变换. 以 modeldata::credit_data 为例, 其包含不同尺度的数据:
由于其形式简单, 有多种实现方式
- 使用
dplyr::mutate() - 使用
recipes::step_normalize() - 使用
recipes::step_mutate()
这里我们以前者为例:
手动进行变换的好处是可以完全控制变换的细节, 如使用均值/中位数作中心化, 使用标准差/四分位距作尺度化等. 但缺点是需要自己编写代码, 且不易与已有的建模框架集成.
recipes 包也提供了一系列 dplyr steps 实现与 dplyr 函数类似的功能, 如
step_mutate()step_filter()step_select()- …
这里我们可以使用 step_mutate() 实现和刚刚一样的功能:
然而, 这里一个隐藏的问题是, 将上述 step 用于训练集和测试集时, 会分别使用其各自的均值/标准差, 造成数据泄露.
也可以使用 recipes::step_normalize 来实现标准化:
但 recipes::step_normalize 的默认实现是使用均值和标准差进行中心化和尺度化. 如果需要使用其他统计量, 则需要额外的工作, 我们之后介绍.
最小值-最大值规范化
在 recipes 中直接用 step_range() 即可, 默认把数值缩放到 \([0, 1]\).
显然, 其效果受到数据中异常值的影响.
如果希望缩放到其他范围, 可以设置 min 和 max 参数:
需要注意的是, 当对于 new_data 中的某个变量的最小值或最大值超出训练数据中的范围时, 其值会被截断到指定的范围内. 例如, 当 new_data 中的 Income 超过训练数据中的最大值时, 其值会被截断到 1.
稳健规范化
为解决规范化中异常值敏感的问题, 可以使用稳健规范化. 其中心化使用中位数, 尺度化使用四分位距.
recipes 包中没有直接的稳健规范化函数, 需要自定义一个 step.
Box-Cox / Yeo-Johnson 变换
Box-Cox 和 Yeo-Johnson 变换将非正态分布的数值变量转换为接近正态分布. Box-Cox 适用于正值数据,Yeo-Johnson 可扩展到包含零和负值的数据.
可以使用 step_BoxCox() 和 step_YeoJohnson() 来实现这两种变换:
在 step_BoxCox() 和 step_YeoJohnson() 中, 其会自动选择最优的 lambda 参数来进行变换. 也可以通过 limits 和 num_unique 参数来控制 lambda 的搜索范围和精度.
行归一化
行归一化将每个样本的多个特征缩放到一个特定范围内, 如 [0, 1] 或 [-1, 1]. 适用于只关心特征之间的相对比例,而完全不在乎数据的绝对大小时.
可以先使用 step_mutate() 计算出行和, 再除以行和以实现行归一化:
注意这里不涉及数据泄露的问题, 因为行归一化只使用了每个样本自己的数据, 而没有从训练集中学习到参数.
定性变量规范化
独热编码
独热编码将每个类别转换为一个二进制特征, 适用于无序的分类变量. 可以使用 step_dummy() 来实现独热编码:
其 one_hot 参数可以控制是否省略其中一个类别以避免多重共线性:
有序编码
有时, 分类变量具有内在的顺序关系. 这种情况下, 可以使用有序编码将类别映射到整数值. 可以使用 step_ordinalscore() 来实现有序编码:
目标编码
目标编码将分类变量的每个类别替换为该类别对应的目标变量的统计量(如均值). 适用于高基数的分类变量. 可以使用 embed::step_lencode() (Likelihood Encoding) 来实现目标编码:
其暂时只支持数值/二分类目标变量. 对于数值型变量, 其提供 smooth 参数 (默认为 TRUE) 来控制是否进行平滑处理:
离散化
无监督离散化
组距分组
组距分组按照特定组距将数据分为若干个区间. 组距可以根据预定的组数/频数决定, 即等距分组/等频分组.
使用 step_cut() 来实现等距分组:
注意其中等距只体现在我们设置的 break 上. 实际上 step_cut() 是一个相当通用的离散化函数, 其 break 参数指定为不同的值即可实现不同的离散化方法.
使用 step_discretize() 来实现等频分组:
均值-标准差分组
在 step_cut() 中设置 breaks 为 \(\mu \pm \sigma\) 可以实现均值-标准差分组:
K-means 分箱法
recipes 包中没有直接的 K-means 分箱函数, 但可以通过向 step_cut() 提供 K-means 聚类中心的中点作为 breaks 来实现:
有监督离散化
ChiMerge 算法
recipes 包中没有直接的 ChiMerge 分箱函数. 但 discretization 包提供了一个 chiM() 函数来实现 ChiMerge 算法:
也可以将结果中的 cutp 传给 step_cut() 的 breaks, 实现 ChiMerge 分箱:
决策树离散化
目前 recipes 包没有内置的决策树分箱 step,但可以通过自定义 breaks 实现类似效果. 只需用 rpart 包训练决策树,然后提取分割点作为 breaks 即可:
自定义 step
recipes 包提供了一个框架, 允许用户创建自定义的 step 来实现特定的数据预处理. 其基本步骤包括
- 定义一个 step constructor, 其接受必要的参数并返回一个带有特定 S3 类属性的 step 对象.
- 对这个 S3 类实现
prep()和bake()方法, 在其中定义 step 的处理细节. - 创建一个面向用户的 step_*() 函数, 其调用 step constructor 来创建 step 对象, 并使用 add_step() 将其添加到 recipe 中.
step_robust_normalize()
下面我们以稳健规范化为例. 其中心化使用中位数, 尺度化使用四分位距.
首先定义一个 step constructor, 其在内部调用 recipes 提供的 step() 来创建一个 step 对象:
接下来, 为这个 S3 类实现 prep() 和 bake() 方法:
最后, 创建一个面向用户的 step_robust_normalize(), 其调用 step constructor 来创建 step 对象, 并使用 recipes 包提供的 add_step() 将其添加到 recipe 中.
注意在此我们才开始添加默认参数, 以便用户使用.
我们验证一下这个 step 的效果:
可以看到, 现在两个变量放缩的尺度不再显著受其中的异常值影响.
元编程
可以看到, 刚刚编写的几个函数具有很强的规律性, 如 step_*_new() 中只是简单调用了 step(), step_*() 中只是简单调用了 add_step(). 真正自定义的部分只包含在 prep.step_*() 和 bake.step_*() 中. 因此, 理论上可以通过元编程来自动生成这些函数. 以下是为任意 step constructor 生成对应的面向用户的 step_*() 函数的一个示例:
我们验证其生成的 step_robust_normalize() 是否正常工作:
上述 generate_step_user() 只是一个启发性的例子. 对于其他函数/泛型方法的生成在理论上也是可行的.
练习
练习 1
使用 modeldata::credit_data 完成以下任务:
- 去除缺失值, 并仅保留
Income、Assets与Status这几个变量 - 划分训练集与测试集
- 以训练集为基础, 建立一个以
Income和Assets为自变量的基础recipe对象 - 参考
step_normalize()与step_robust_normalize()的实现方式, 自定义step_mad_normalize()- 在
prep()中计算所选变量的中位数与中位数绝对偏差 (\(m(x) = median(|x - median(x)|)\)) - 使用中位数作为中心化参数, 使用中位数绝对偏差作为尺度化参数, 在
bake()中对新数据完成变换
- 在
- 使用
step_mad_normalize()对训练集和测试集进行处理, 并检查变换后各变量的中位数是否接近 0 - 再基于同一个基础
recipe, 分别完成以下规范化:step_normalize()step_range()
- 分别计算不同方法下
Income与Assets的:- 均值
- 标准差
- 中位数
- 四分位距
- 使用
ggplot2可视化比较不同方法的效果:- 用散点图比较
Income与Assets的联合分布 - 用箱线图、密度图或直方图比较两变量的边际分布
- 用散点图比较
练习 2
使用 modeldata::credit_data 完成以下任务:
- 去除缺失值, 并仅保留
Income与Status这两个变量 - 划分训练集与测试集
- 以训练集为基础, 建立一个对
Income进行离散化的基础recipe对象 - 参考 K-means 分箱法一节, 自定义
step_kmeans_cut()- 在
prep()中对Income进行kmeans()聚类, 保存聚类结果 - 在
bake()中根据聚类结果对新数据进行离散化
- 在
- 使用
step_kmeans_cut(Income, centers = 4)对Income分箱 - 再基于同一份基础
recipe, 分别使用以下方法对Income离散化:step_cut()实现等距分组step_discretize()实现等频分组
prep()后分别处理训练集与测试集- 比较不同方法在箱数、箱宽/箱频以及类别分布上的差异
- 使用
ggplot2可视化比较不同方法的效果:- 比较各方法下各箱的样本量
- 比较各方法下各箱中
Status的"good"比例