4、Yeo-Johnson变换:当数据出现负数时,我该怎么办?
聊完了Box-Cox变换,你可能会问:那如果数据里有负数怎么办?
嗯,这个问题我在实际项目中遇到过不止一次。记得有一次处理股票收益率数据,里面天然就有负值——毕竟不是每天都能赚钱的。当时我第一反应是用Box-Cox,结果发现数据里有负数,直接报错。后来查资料才找到Yeo-Johnson这个神器。
Yeo-Johnson变换的原理
Yeo-Johnson变换是Box-Cox的升级版。它最大的特点就是:不需要数据全部为正。说白了,它把Box-Cox的适用范围从正数扩展到了全体实数。
它的数学表达式长这样:
对于任意实数y:
当λ ≠ 0时:
ψ(y, λ) = ((y + 1)^λ - 1) / λ 当 y ≥ 0
ψ(y, λ) = -((-y + 1)^(2-λ) - 1) / (2-λ) 当 y < 0
当λ = 0时:
ψ(y, λ) = ln(y + 1) 当 y ≥ 0
ψ(y, λ) = -ln(-y + 1) 当 y < 0
看着有点复杂对吧?其实核心思想很简单:
- 正数部分:和Box-Cox类似,只是把y换成了(y+1)
- 负数部分:通过对称变换处理,保证变换后的数据连续且单调
关键点:Yeo-Johnson变换在y=0处是光滑的,不会出现断点。这一点在实际应用中非常重要。
处理负值数据的实战经验
我个人习惯在以下场景优先使用Yeo-Johnson:
- 金融收益率数据:涨跌都有,负值很正常
- 标准化后的特征:比如z-score后的数据,均值0,有正有负
- 差值或变化率:比如价格变动、增长率等
我的小技巧:如果数据中负值占比很小(比如不到5%),我会先尝试给所有数据加一个常数偏移,再用Box-Cox。但如果负值较多,直接上Yeo-Johnson更省心。
与Box-Cox的对比
我整理了一个对比表,方便你快速决策:
| 对比维度 | Box-Cox变换 | Yeo-Johnson变换 |
|---|---|---|
| 数据要求 | 必须全部为正 | 任意实数 |
| 数学复杂度 | 较低 | 稍高 |
| 参数λ范围 | 无限制 | 通常λ ∈ [-2, 2] |
| 计算速度 | 快 | 稍慢(但可忽略) |
| 适用场景 | 正数数据(价格、成交量) | 含负值数据(收益率、残差) |
你想想看,如果数据全是正数,用Box-Cox就够了,没必要上Yeo-Johnson。但一旦出现负数,Box-Cox就无能为力了。
Python实现:从理论到代码
下面是我在实际项目中常用的代码模板。注意,我用了scipy库,它已经内置了Yeo-Johnson变换:
import numpy as np
import pandas as pd
from scipy.stats import yeojohnson
from scipy.special import yeojohnson_normmax
import matplotlib.pyplot as plt
# 生成含负值的数据
np.random.seed(42)
data = np.random.randn(1000) * 2 + 1 # 均值1,标准差2,肯定有负数
# 1. 自动寻找最优λ
optimal_lambda = yeojohnson_normmax(data)
print(f"最优λ值: {optimal_lambda:.4f}")
# 2. 执行变换
transformed_data, fitted_lambda = yeojohnson(data)
print(f"变换后的数据形状: {transformed_data.shape}")
print(f"拟合的λ: {fitted_lambda:.4f}")
# 3. 逆变换(还原数据)
from scipy.special import yeojohnson_inv
recovered_data = yeojohnson_inv(transformed_data, fitted_lambda)
print(f"还原误差: {np.max(np.abs(data - recovered_data)):.2e}")
# 4. 可视化对比
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
# 原始数据分布
axes[0].hist(data, bins=50, alpha=0.7, color='steelblue')
axes[0].set_title('原始数据分布')
axes[0].axvline(x=0, color='red', linestyle='--', label='零线')
axes[0].legend()
# 变换后数据分布
axes[1].hist(transformed_data, bins=50, alpha=0.7, color='coral')
axes[1].set_title(f'Yeo-Johnson变换后 (λ={fitted_lambda:.2f})')
plt.tight_layout()
plt.show()
我曾经踩过的坑:有一次我直接用yeojohnson函数处理整个DataFrame,结果发现不同列用了不同的λ值。这本身没问题,但后续做逆变换时忘记保存每列的λ,导致数据还原不回去。所以我的建议是:一定要保存每列的λ值,最好存到一个字典里。
实际案例:处理股票收益率
让我给你看一个真实场景。假设我们有一支股票的日收益率数据:
# 模拟股票收益率数据
np.random.seed(123)
returns = np.random.laplace(loc=0.0005, scale=0.02, size=500)
# 检查是否有负值
print(f"最小值: {returns.min():.4f}")
print(f"负值比例: {(returns < 0).mean()*100:.1f}%")
# 应用Yeo-Johnson
transformed_returns, lambda_ret = yeojohnson(returns)
# 对比变换前后的偏度和峰度
from scipy.stats import skew, kurtosis
print("变换前:")
print(f" 偏度: {skew(returns):.4f}")
print(f" 峰度: {kurtosis(returns):.4f}")
print("变换后:")
print(f" 偏度: {skew(transformed_returns):.4f}")
print(f" 峰度: {kurtosis(transformed_returns):.4f}")
运行这段代码,你会发现变换后的数据更接近正态分布。这对后续的线性模型、因子分析都很有帮助。
Yeo-Johnson的核心逻辑图
下面我用SVG画了一张流程图,帮你理清Yeo-Johnson的决策逻辑:
几点补充建议
- 参数λ的搜索范围:我一般限制在[-2, 2]之间,超出这个范围变换效果往往不稳定
- 数据量要求:Yeo-Johnson对样本量有一定要求,少于50个样本时λ估计可能不准
- 与标准化结合:我习惯先做Yeo-Johnson变换,再做z-score标准化,效果更好
一句话总结:数据有负数?别慌,Yeo-Johnson就是为你准备的。它保留了Box-Cox的所有优点,同时兼容了负值数据。我个人在量化因子处理中,80%的情况都用它。
公众号:蓝海资料掘金营,微信deep3321