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)
为什么使用对数?
- 处理右偏分布
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()稳定方差(减少异方差)
系数解释为弹性
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、排名 |
| 系数可比性 | 标准化 |
| 非线性关系 | 多项式、分箱 |
练习题
对一个右偏的收入变量进行对数转换,比较转换前后的分布。
创建年龄的四次多项式项,并在回归中检验非线性关系。
对教育年限进行分箱(高中以下、高中、大学、研究生),创建虚拟变量并回归。
下一步
下一节:数据合并与重塑
参考:
- Box, G. E., & Cox, D. R. (1964). "An analysis of transformations"
- Wooldridge (2020): Chapter 6 - Functional Form