Skip to content

3.5 数据转换

"Data transforms are as important as the algorithms themselves.""数据转换与算法本身同样重要。"— Hadley Wickham, Chief Scientist at RStudio (RStudio首席科学家)

让数据更适合统计分析


本节目标

  • 掌握标准化与归一化
  • 理解对数转换的应用场景
  • 学习分箱(Binning)技术
  • 了解何时需要数据转换

标准化(Standardization)

Z-score 标准化

python
from scipy import stats
import pandas as pd
import numpy as np

# 方法 1:scipy
df['income_std'] = stats.zscore(df['income'])

# 方法 2:手动计算
df['income_std'] = (df['income'] - df['income'].mean()) / df['income'].std()

# 性质:均值=0,标准差=1

应用场景

  • 比较不同量纲变量的回归系数
  • 机器学习算法(如 SVM、KNN)
  • 检测异常值(|z| > 3)

Min-Max 归一化

python
# 缩放到 [0, 1]
df['income_norm'] = (df['income'] - df['income'].min()) / \
                    (df['income'].max() - df['income'].min())

# 缩放到 [a, b]
a, b = 0, 100
df['income_scaled'] = a + (b - a) * df['income_norm']

标准化回归系数

python
import statsmodels.api as sm

# 原始回归
X = sm.add_constant(df[['education', 'experience']])
y = df['income']
model_raw = sm.OLS(y, X).fit()

# 标准化回归
df_std = df[['income', 'education', 'experience']].apply(stats.zscore)
X_std = sm.add_constant(df_std[['education', 'experience']])
y_std = df_std['income']
model_std = sm.OLS(y_std, X_std).fit()

print("原始系数:", model_raw.params)
print("标准化系数:", model_std.params)

# 标准化系数解释:
# β_std = 0.4 → 教育增加 1 个标准差,收入增加 0.4 个标准差

对数转换(Log Transformation)

为什么使用对数?

  1. 处理右偏分布
python
import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# 原始分布(右偏)
axes[0].hist(df['income'], bins=50, edgecolor='black')
axes[0].set_title('原始收入分布(右偏)')

# 对数转换后(接近正态)
axes[1].hist(np.log(df['income']), bins=50, edgecolor='black', color='green')
axes[1].set_title('log(收入) 分布(接近正态)')

plt.show()
  1. 稳定方差(减少异方差)

  2. 系数解释为弹性

python
# log-level 模型
# log(Y) = β₀ + β₁*X + ε
# 解释:X 增加 1 单位,Y 增长约 100×β₁%

# log-log 模型
# log(Y) = β₀ + β₁*log(X) + ε
# 解释:X 增加 1%,Y 增长 β₁%(弹性)

# level-log 模型
# Y = β₀ + β₁*log(X) + ε
# 解释:X 增加 1%,Y 增加 β₁/100 单位

四种对数模型

python
import statsmodels.api as sm

# 原始数据
df['log_wage'] = np.log(df['wage'])
df['log_education'] = np.log(df['education'])

# 模型 1:Level-Level
X1 = sm.add_constant(df['education'])
model1 = sm.OLS(df['wage'], X1).fit()
print("Level-Level: β₁ =", model1.params['education'])
print("解释:教育增加 1 年,工资增加 β₁ 元")

# 模型 2:Log-Level  
X2 = sm.add_constant(df['education'])
model2 = sm.OLS(df['log_wage'], X2).fit()
print("Log-Level: β₁ =", model2.params['education'])
print(f"解释:教育增加 1 年,工资增长 {model2.params['education']*100:.2f}%")

# 模型 3:Level-Log
X3 = sm.add_constant(df['log_education'])
model3 = sm.OLS(df['wage'], X3).fit()
print("Level-Log: β₁ =", model3.params['log_education'])
print(f"解释:教育增加 1%,工资增加 {model3.params['log_education']/100:.2f} 元")

# 模型 4:Log-Log(弹性)
X4 = sm.add_constant(df['log_education'])
model4 = sm.OLS(df['log_wage'], X4).fit()
print("Log-Log: β₁ =", model4.params['log_education'])
print("解释:教育增加 1%,工资增长 β₁%")

处理零值和负值

python
# 问题:log(0) = -∞,log(负数) = NaN

# 解决方法 1:log(x + 1)
df['log_income_plus1'] = np.log(df['income'] + 1)

# 解决方法 2:log(x + c),c 为最小正值的一半
c = df[df['income'] > 0]['income'].min() / 2
df['log_income_c'] = np.log(df['income'] + c)

# 解决方法 3:IHS 变换(反双曲正弦)
df['ihs_income'] = np.arcsinh(df['income'])
# 优势:可以处理负值,接近对数性质

️ 分箱(Binning)

为什么分箱?

  • 处理非线性关系
  • 稳健于异常值
  • 便于解释
  • 检验分段线性假设

等宽分箱

python
# 将年龄分为 4 组
df['age_group'] = pd.cut(
    df['age'],
    bins=4,
    labels=['青年', '中年', '中老年', '老年']
)

# 自定义边界
df['age_group'] = pd.cut(
    df['age'],
    bins=[0, 25, 40, 60, 100],
    labels=['<25', '25-40', '40-60', '60+']
)

等频分箱(分位数)

python
# 分为 4 个等频组(每组样本量相同)
df['income_quartile'] = pd.qcut(
    df['income'],
    q=4,
    labels=['Q1', 'Q2', 'Q3', 'Q4']
)

# 自定义分位数
df['income_decile'] = pd.qcut(
    df['income'],
    q=10,
    labels=[f'D{i}' for i in range(1, 11)]
)

回归中使用分箱

python
# 创建虚拟变量
age_dummies = pd.get_dummies(df['age_group'], prefix='age', drop_first=True)
df = pd.concat([df, age_dummies], axis=1)

# 回归
X = sm.add_constant(df[['education', 'age_中年', 'age_中老年', 'age_老年']])
y = df['log_wage']
model = sm.OLS(y, X).fit()

print(model.summary())

# 系数解释:
# age_中年 的系数:中年人相比青年人(参照组)的工资差异

可视化分箱效果

python
# 计算各组的平均值
age_group_means = df.groupby('age_group')['wage'].mean()

# 绘图
age_group_means.plot(kind='bar', figsize=(10, 6))
plt.xlabel('年龄组')
plt.ylabel('平均工资')
plt.title('年龄与工资的关系(分箱)')
plt.show()

其他转换

Box-Cox 变换

python
from scipy.stats import boxcox

# 自动选择最优的 λ 参数
df['income_boxcox'], lambda_opt = boxcox(df['income'])

print(f"最优 λ = {lambda_opt:.4f}")
# λ = 0 → 对数转换
# λ = 1 → 无转换
# λ = 0.5 → 平方根转换

平方根转换

python
# 适用于计数数据(Poisson 分布)
df['sqrt_count'] = np.sqrt(df['count'])

Rank 转换(非参数)

python
# 将数据转换为排名(对异常值极其稳健)
df['income_rank'] = df['income'].rank()

# 百分位排名
df['income_pct_rank'] = df['income'].rank(pct=True)

小结

转换方法对比

转换方法适用场景Python 代码
标准化系数可比、机器学习stats.zscore()
归一化[0,1] 范围(x - min) / (max - min)
对数右偏分布、弹性解释np.log()
分箱非线性、稳健pd.cut(), pd.qcut()
Box-Cox自动选择最优转换boxcox()
排名极端稳健df.rank()

何时转换?

问题转换方法
右偏分布对数、Box-Cox
异方差对数、平方根
异常值多Winsorize、排名
系数可比性标准化
非线性关系多项式、分箱

练习题

  1. 对一个右偏的收入变量进行对数转换,比较转换前后的分布。

  2. 创建年龄的四次多项式项,并在回归中检验非线性关系。

  3. 对教育年限进行分箱(高中以下、高中、大学、研究生),创建虚拟变量并回归。


下一步

下一节:数据合并与重塑


参考

  • Box, G. E., & Cox, D. R. (1964). "An analysis of transformations"
  • Wooldridge (2020): Chapter 6 - Functional Form

基于 MIT 许可证发布。内容版权归作者所有。