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的决策逻辑:

Yeo-Johnson变换决策流程 原始数据 数据含负值? (min < 0) Yeo-Johnson 变换 Box-Cox 变换 变换后数据 保存λ值,用于逆变换

几点补充建议

  • 参数λ的搜索范围:我一般限制在[-2, 2]之间,超出这个范围变换效果往往不稳定
  • 数据量要求:Yeo-Johnson对样本量有一定要求,少于50个样本时λ估计可能不准
  • 与标准化结合:我习惯先做Yeo-Johnson变换,再做z-score标准化,效果更好

一句话总结:数据有负数?别慌,Yeo-Johnson就是为你准备的。它保留了Box-Cox的所有优点,同时兼容了负值数据。我个人在量化因子处理中,80%的情况都用它。


公众号:蓝海资料掘金营,微信deep3321