正态分布 Python 3.12 实战:3种数据转换方法对比与68-95-99.7法则验证
正态分布 Python 3.12 实战:3种数据转换方法对比与68-95-99.7法则验证
在数据科学和机器学习领域,正态分布的重要性不言而喻。许多经典算法如线性回归、逻辑回归等都假设数据服从或近似服从正态分布。然而现实中的数据往往并不完美,这就需要我们掌握将非正态数据转换为正态分布的技术。本文将使用Python 3.12,通过完整代码示例演示三种主流的数据转换方法,并验证正态分布的经典68-95-99.7法则。
1. 环境准备与数据生成
在开始之前,我们需要准备好Python环境和必要的库。Python 3.12带来了许多性能改进和新特性,特别是在科学计算方面。以下是我们的环境配置:
# 导入所需库
import numpy as np
import scipy.stats as stats
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.preprocessing import PowerTransformer
from sklearn.model_selection import train_test_split
# 设置随机种子保证结果可复现
np.random.seed(42)
# 生成非正态分布数据 - 这里使用指数分布
original_data = np.random.exponential(scale=2.0, size=10000)
# 可视化原始数据分布
plt.figure(figsize=(12, 6))
sns.histplot(original_data, kde=True, bins=50)
plt.title("原始数据分布(指数分布)")
plt.xlabel("数值")
plt.ylabel("频数")
plt.show()
这段代码会生成一个右偏的指数分布数据集,这是我们进行转换的理想起点。从可视化结果可以明显看出数据不服从正态分布。
2. 数据转换方法对比
2.1 对数变换(Log Transformation)
对数变换是最简单也最常用的数据转换方法之一,特别适用于右偏数据:
# 对数变换
log_transformed = np.log1p(original_data) # 使用log1p避免对0取对数
# 可视化对数变换结果
plt.figure(figsize=(12, 6))
sns.histplot(log_transformed, kde=True, bins=50)
plt.title("对数变换后的数据分布")
plt.xlabel("数值")
plt.ylabel("频数")
plt.show()
# 计算并打印偏度和峰度
log_skew = stats.skew(log_transformed)
log_kurtosis = stats.kurtosis(log_transformed)
print(f"对数变换后数据 - 偏度: {log_skew:.4f}, 峰度: {log_kurtosis:.4f}")
对数变换通过压缩数据范围,特别是较大的值,使分布更对称。但它的局限性在于只能处理正值数据,且要求数据没有零值(因此我们使用log1p而不是log)。
2.2 Box-Cox变换
Box-Cox变换是对数变换的推广,它通过一个参数λ来优化变换效果:
# Box-Cox变换 - 要求数据必须为正数
positive_data = original_data + 1e-6 # 确保所有值为正
boxcox_transformed, lambda_boxcox = stats.boxcox(positive_data)
# 可视化Box-Cox变换结果
plt.figure(figsize=(12, 6))
sns.histplot(boxcox_transformed, kde=True, bins=50)
plt.title(f"Box-Cox变换后的数据分布 (λ={lambda_boxcox:.4f})")
plt.xlabel("数值")
plt.ylabel("频数")
plt.show()
# 计算并打印偏度和峰度
boxcox_skew = stats.skew(boxcox_transformed)
boxcox_kurtosis = stats.kurtosis(boxcox_transformed)
print(f"Box-Cox变换后数据 - 偏度: {boxcox_skew:.4f}, 峰度: {boxcox_kurtosis:.4f}")
Box-Cox变换会自动寻找最优的λ值,使变换后的数据尽可能接近正态分布。当λ=0时,Box-Cox变换就等同于对数变换。
2.3 Yeo-Johnson变换
Yeo-Johnson变换是Box-Cox变换的扩展,可以处理包含零和负值的数据:
# Yeo-Johnson变换
pt = PowerTransformer(method='yeo-johnson', standardize=False)
yeojohnson_transformed = pt.fit_transform(original_data.reshape(-1, 1)).flatten()
# 可视化Yeo-Johnson变换结果
plt.figure(figsize=(12, 6))
sns.histplot(yeojohnson_transformed, kde=True, bins=50)
plt.title("Yeo-Johnson变换后的数据分布")
plt.xlabel("数值")
plt.ylabel("频数")
plt.show()
# 计算并打印偏度和峰度
yeojohnson_skew = stats.skew(yeojohnson_transformed)
yeojohnson_kurtosis = stats.kurtosis(yeojohnson_transformed)
print(f"Yeo-Johnson变换后数据 - 偏度: {yeojohnson_skew:.4f}, 峰度: {yeojohnson_kurtosis:.4f}")
Yeo-Johnson变换比Box-Cox更灵活,因为它不需要数据必须为正数。这使得它在实际应用中更为方便。
3. 转换方法效果对比
为了更直观地比较三种转换方法的效果,我们可以将它们的结果放在一起:
# 创建对比图
fig, axes = plt.subplots(2, 2, figsize=(15, 12))
# 原始数据
sns.histplot(original_data, kde=True, bins=50, ax=axes[0, 0])
axes[0, 0].set_title("原始数据分布")
# 对数变换
sns.histplot(log_transformed, kde=True, bins=50, ax=axes[0, 1])
axes[0, 1].set_title("对数变换后分布")
# Box-Cox变换
sns.histplot(boxcox_transformed, kde=True, bins=50, ax=axes[1, 0])
axes[1, 0].set_title("Box-Cox变换后分布")
# Yeo-Johnson变换
sns.histplot(yeojohnson_transformed, kde=True, bins=50, ax=axes[1, 1])
axes[1, 1].set_title("Yeo-Johnson变换后分布")
plt.tight_layout()
plt.show()
# 创建统计量对比表格
methods = ["原始数据", "对数变换", "Box-Cox变换", "Yeo-Johnson变换"]
skewness = [stats.skew(original_data), log_skew, boxcox_skew, yeojohnson_skew]
kurtosis = [stats.kurtosis(original_data), log_kurtosis, boxcox_kurtosis, yeojohnson_kurtosis]
print("转换方法效果对比:")
print("方法\t\t\t偏度\t\t峰度")
for method, skew, kurt in zip(methods, skewness, kurtosis):
print(f"{method.ljust(15)}\t{skew:.4f}\t\t{kurt:.4f}")
从结果可以看出,三种方法都能有效减少数据的偏度,使分布更接近正态。Box-Cox和Yeo-Johnson变换通常能获得更好的效果,特别是对于偏态严重的数据。
4. 68-95-99.7法则验证
正态分布的一个关键特性是68-95-99.7法则,即:
- 约68%的数据落在均值±1个标准差范围内
- 约95%的数据落在均值±2个标准差范围内
- 约99.7%的数据落在均值±3个标准差范围内
我们可以用转换后的数据验证这一法则:
def validate_empirical_rule(data, method_name):
mean = np.mean(data)
std = np.std(data)
# 计算各区间内的数据比例
within_1std = np.sum((data >= mean - std) & (data <= mean + std)) / len(data)
within_2std = np.sum((data >= mean - 2*std) & (data <= mean + 2*std)) / len(data)
within_3std = np.sum((data >= mean - 3*std) & (data <= mean + 3*std)) / len(data)
print(f"\n{method_name}验证68-95-99.7法则:")
print(f"均值: {mean:.4f}, 标准差: {std:.4f}")
print(f"μ±1σ范围内数据比例: {within_1std*100:.2f}% (理论68.27%)")
print(f"μ±2σ范围内数据比例: {within_2std*100:.2f}% (理论95.45%)")
print(f"μ±3σ范围内数据比例: {within_3std*100:.2f}% (理论99.73%)")
# 对每种转换方法进行验证
validate_empirical_rule(log_transformed, "对数变换")
validate_empirical_rule(boxcox_transformed, "Box-Cox变换")
validate_empirical_rule(yeojohnson_transformed, "Yeo-Johnson变换")
验证结果将显示转换后的数据在多大程度上符合正态分布的特性。理想情况下,这些比例应该接近理论值。
5. 实际应用建议
在实际项目中应用这些转换方法时,有几个关键注意事项:
- 数据分割时机 :务必在划分训练集和测试集之后再进行数据转换,避免数据泄露。转换参数(如λ值)应该仅从训练数据中学习。
# 正确的数据分割和转换流程
X_train, X_test = train_test_split(original_data, test_size=0.2, random_state=42)
# 在训练集上拟合转换器
pt = PowerTransformer(method='yeo-johnson')
pt.fit(X_train.reshape(-1, 1))
# 然后转换训练集和测试集
X_train_transformed = pt.transform(X_train.reshape(-1, 1)).flatten()
X_test_transformed = pt.transform(X_test.reshape(-1, 1)).flatten()
-
模型性能影响 :并非所有机器学习模型都要求输入数据服从正态分布。决策树类模型对数据分布不敏感,而线性模型则可能受益于正态化转换。
-
逆变换 :如果需要将预测结果转换回原始尺度,记得保存转换参数以便进行逆变换:
# Box-Cox逆变换示例
from scipy.special import inv_boxcox
# 假设我们有一些预测结果需要转换回原始尺度
predicted_transformed = np.array([1.0, 1.5, 2.0]) # 示例数据
predicted_original = inv_boxcox(predicted_transformed, lambda_boxcox)
- 替代方案 :对于严重偏离正态分布的数据,可以考虑使用分位数变换(QuantileTransformer)或直接选择对分布不敏感的模型。
更多推荐
所有评论(0)