3. 单变量特征构造:基于数学变换
做因子挖掘这几年,我越来越觉得:原始数据就像一块璞玉,不经过打磨,很难发光。很多同学拿到数据就直接往模型里塞,结果效果不好,还怪模型不行。其实啊,很多时候问题出在特征本身——分布太偏、尺度差异大、非线性关系没处理好。
今天咱们聊聊单变量特征构造里最基础也最实用的一类:数学变换。说白了,就是对一个特征做点「数学手术」,让它更适合建模。
核心思想:通过非线性变换,改变特征的分布形态或尺度,使其更符合模型的假设(如正态性、线性关系)。
3.1 对数变换:处理长尾数据的利器
先说说对数变换。我个人习惯把它叫做「长尾杀手」。你想想看,金融数据里最常见的分布是什么?右偏分布。比如股票收益率、成交量、波动率——这些数据往往有一堆小值,偶尔蹦出几个大值,尾巴拖得老长。
对数变换的公式很简单:
新特征 = log(原始特征) # 自然对数
新特征 = log10(原始特征) # 常用对数
新特征 = log(1 + 原始特征) # 处理零值
为什么有效?因为对数函数在值大的区域压缩得更厉害。举个例子:
- 原始值 1 → log(1) = 0
- 原始值 10 → log(10) = 2.30
- 原始值 100 → log(100) = 4.61
- 原始值 1000 → log(1000) = 6.91
你看,原始值差了1000倍,变换后只差了不到7。这就是对数变换的「压缩」效果——把指数级差异变成线性差异。
我的经验:做股票因子时,成交额这个特征我几乎必做对数变换。原始成交额从几百万到几百亿,跨度太大,直接扔进模型,大值会主导学习过程。取对数后,分布就「乖」多了。
代码实现也很简单:
import numpy as np
import pandas as pd
# 原始特征
df['volume'] = [100, 500, 2000, 50000, 1000000]
# 对数变换(处理零值)
df['log_volume'] = np.log1p(df['volume']) # log(1+x)
# 或者用 log10
df['log10_volume'] = np.log10(df['volume'] + 1)
print(df)
# volume log_volume log10_volume
# 0 100 4.615121 2.004321
# 1 500 6.216606 2.698970
# 2 2000 7.600902 3.301030
# 3 50000 10.819778 4.698970
# 4 1000000 13.815511 6.000000
注意:如果特征包含零值,直接用 log(x) 会得到负无穷。我建议用 log1p(x) 或 log(x + 1)。如果特征有负值,那就得先做偏移,比如 log(x - min(x) + 1)。
3.2 平方根变换:计数数据的温柔处理
平方根变换,我把它叫做「温和版的对数变换」。它也有压缩效果,但力度比对数变换小。什么时候用?计数数据——比如某只股票一个月内涨停的次数、新闻出现的频率、机构调研的次数。
公式:
新特征 = sqrt(原始特征)
新特征 = sqrt(原始特征 + 0.5) # 处理零值,常用在泊松分布数据
为什么用平方根?因为计数数据往往服从泊松分布,而泊松分布的方差等于均值。取平方根后,方差就稳定了——这叫方差稳定化变换。
举个例子:
# 计数数据:某因子过去20天出现的次数
counts = [0, 1, 2, 3, 5, 8, 13, 21]
# 平方根变换
sqrt_counts = np.sqrt(counts)
sqrt_counts_adj = np.sqrt(np.array(counts) + 0.5)
print("原始:", counts)
print("sqrt:", np.round(sqrt_counts, 3))
print("sqrt+0.5:", np.round(sqrt_counts_adj, 3))
# 输出:
# 原始: [0, 1, 2, 3, 5, 8, 13, 21]
# sqrt: [0.0, 1.0, 1.414, 1.732, 2.236, 2.828, 3.606, 4.583]
# sqrt+0.5: [0.707, 1.225, 1.581, 1.871, 2.345, 2.915, 3.674, 4.637]
什么时候选平方根而不是对数?我个人的经验是:如果数据范围不大(比如0-100),且零值较多,平方根比对数更合适。因为 log(0+1)=0,但 sqrt(0+0.5)≈0.707,保留了零值和非零值之间的区分度。
3.3 Box-Cox变换:自动寻找最优变换
前面两种变换都是「拍脑袋」选一个函数。但有没有一种方法,能自动找到最适合当前数据的变换?有——Box-Cox变换。
公式长这样:
BoxCox(x, λ) = (x^λ - 1) / λ, λ ≠ 0
BoxCox(x, 0) = log(x), λ = 0
你看,当 λ=0 时,Box-Cox 退化为对数变换。当 λ=0.5 时,它近似于平方根变换。所以它其实是一个包含了对数和平方根的广义变换族。
关键是怎么选 λ?用最大似然估计,让变换后的数据最接近正态分布。
from scipy import stats
import numpy as np
# 生成一个右偏分布的数据
np.random.seed(42)
data = np.random.exponential(scale=2, size=1000)
# Box-Cox变换,自动寻找最优λ
transformed_data, lambda_opt = stats.boxcox(data)
print(f"最优 λ = {lambda_opt:.4f}")
# 输出:最优 λ = 0.2112
# 如果想用整数λ,可以四舍五入
lambda_rounded = round(lambda_opt * 2) / 2
print(f"取整 λ = {lambda_rounded}")
# 手动应用Box-Cox
def boxcox_transform(x, lam):
if lam == 0:
return np.log(x)
else:
return (x**lam - 1) / lam
manual_transformed = boxcox_transform(data, lambda_opt)
避坑指南:我曾经在一个项目里直接用 Box-Cox 处理所有特征,结果发现有些特征变换后反而更差了。后来才意识到——Box-Cox 要求数据严格为正(x > 0)。如果有零值或负值,需要用 Yeo-Johnson 变换(Box-Cox 的改进版,支持零和负值)。
3.4 三种变换的对比与选择
说了这么多,到底什么时候用哪个?我整理了一个表格,方便你对照:
| 变换方法 | 适用场景 | 优点 | 缺点 | 我的推荐指数 |
|---|---|---|---|---|
| 对数变换 | 强右偏、长尾、乘性关系 | 解释性强,压缩效果好 | 不能处理零/负值 | ⭐⭐⭐⭐⭐ |
| 平方根变换 | 计数数据、轻度偏态 | 保留零值信息,方差稳定 | 压缩力度有限 | ⭐⭐⭐⭐ |
| Box-Cox变换 | 任意偏态,自动寻优 | 自动化,效果好 | 需正值,解释性差 | ⭐⭐⭐⭐⭐ |
我个人习惯的做法是:
- 先看分布——画个直方图或Q-Q图,判断偏斜程度
- 轻度偏斜(偏度 < 1)→ 平方根变换
- 中度偏斜(1 < 偏度 < 3)→ 对数变换
- 重度偏斜(偏度 > 3)→ Box-Cox变换
- 不确定→ 三种都试一下,看哪个让分布更接近正态
重要提醒:变换后的特征一定要做逆变换检查。什么意思?就是变换后的值能不能合理地映射回原始空间。比如你预测了 log(成交量),最终要换算回原始成交量才有实际意义。这个步骤很多人会忘,但非常重要。
3.5 实战:一个完整的例子
最后,咱们用真实场景串一遍。假设你在做股票因子挖掘,有一个特征叫「日内振幅」(最高价/最低价 - 1),它的分布长这样:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats
# 模拟日内振幅数据(右偏分布)
np.random.seed(42)
amplitude = np.random.exponential(scale=0.03, size=10000) + 0.001
# 计算偏度
skewness = stats.skew(amplitude)
print(f"原始偏度: {skewness:.4f}") # 输出:原始偏度: 2.1345
# 尝试三种变换
log_amp = np.log1p(amplitude)
sqrt_amp = np.sqrt(amplitude)
boxcox_amp, lam = stats.boxcox(amplitude)
# 计算变换后的偏度
print(f"对数变换后偏度: {stats.skew(log_amp):.4f}")
print(f"平方根变换后偏度: {stats.skew(sqrt_amp):.4f}")
print(f"Box-Cox变换后偏度: {stats.skew(boxcox_amp):.4f}, λ={lam:.4f}")
# 输出:
# 对数变换后偏度: 0.3421
# 平方根变换后偏度: 0.8912
# Box-Cox变换后偏度: 0.0213, λ=0.1523
你看,原始偏度 2.13,属于中度偏斜。对数变换把它降到了 0.34,效果不错。Box-Cox 更狠,直接干到 0.02,几乎完美正态。平方根变换效果最差,因为数据偏斜程度超出了它的「舒适区」。
我的建议:在实际项目中,我不会只依赖偏度这一个指标。我还会看变换后的特征与目标变量的相关性有没有提升。毕竟我们做因子挖掘,最终目的是预测,不是追求正态分布本身。有时候变换后分布更「正态」了,但预测能力反而下降了——这种情况我也遇到过,那就得重新评估了。
嗯,关于单变量数学变换,今天就聊到这儿。这三种方法——对数、平方根、Box-Cox——是特征工程里最基础也最实用的工具。记住一个原则:先看分布,再选变换,最后验证效果。别一上来就 Box-Cox 全自动,也别永远只用 log。灵活搭配,才是老手的做法。
公众号:蓝海资料掘金营,微信deep3321