Chapter 06

数据变换与计算

apply/map/transform 选择原则、向量化操作、rolling/expanding 窗口函数

apply() vs map() vs transform()

Pandas 中有三个容易混淆的变换方法,选错会导致性能问题或错误结果:

map()(Series 方法)
对 Series 的每个元素应用函数或映射字典。只能用于 Series,不能用于 DataFrame。是最简单的元素级操作。
apply()(Series 和 DataFrame 方法)
DataFrame.apply() 对每行或每列应用函数(axis=0 对列,axis=1 对行)。Series.apply() 对每个元素应用。灵活但慢,应尽量用向量化替代。
transform()(通常与 groupby 配合)
与 groupby 配合时,transform 对每组分别计算,但结果广播回原始 DataFrame 的形状(保持行数不变)。这是 apply/agg 做不到的。
import pandas as pd
import numpy as np

df = pd.DataFrame({
    'name': ['Alice', 'Bob', 'Carol'],
    'salary': [50000, 60000, 55000],
    'dept': ['Dev', 'Dev', 'HR']
})

# map():简单元素映射
dept_cn = {'Dev': '开发部', 'HR': '人事部'}
df['dept_cn'] = df['dept'].map(dept_cn)  # 用字典映射
df['salary_w'] = df['salary'].map(lambda x: x / 10000)  # 用函数映射(万元)

# apply():更复杂的逐行逻辑
def salary_grade(row):
    """薪资等级(需要多列数据)"""
    if row['dept'] == 'Dev' and row['salary'] > 55000:
        return '高级'
    return '普通'

df['grade'] = df.apply(salary_grade, axis=1)  # axis=1 表示对每行应用

# transform():广播计算结果(行数不变)
# 计算每个员工薪资与所在部门平均薪资的差
df['dept_avg_salary'] = df.groupby('dept')['salary'].transform('mean')
df['salary_vs_dept'] = df['salary'] - df['dept_avg_salary']

向量化操作原则:避免 apply() 循环

# apply() 本质是 Python 循环,对大数据集很慢
# 尽量用 Pandas 内置向量化操作替代

n = 100_000
df = pd.DataFrame({'x': np.random.randn(n), 'y': np.random.randn(n)})

# ❌ 慢:apply() 循环
# %timeit df.apply(lambda r: (r['x']**2 + r['y']**2)**0.5, axis=1) → ~2s

# ✅ 快:向量化运算
# %timeit np.sqrt(df['x']**2 + df['y']**2) → ~2ms(1000倍差距!)

result = np.sqrt(df['x']**2 + df['y']**2)

# 常见的向量化替代方案:
# apply(lambda x: x > 0) → df['col'] > 0
# apply(lambda x: x.strip()) → df['col'].str.strip()
# apply(lambda x: x['a'] + x['b'], axis=1) → df['a'] + df['b']

窗口函数(rolling / expanding)

dates = pd.date_range('2024-01-01', periods=10, freq='D')
ts = pd.Series([10, 12, 9, 15, 11, 14, 8, 13, 16, 12], index=dates)

# rolling():固定窗口大小的滚动计算
ts.rolling(window=3).mean()    # 3日滚动均值(前3个结果为 NaN)
ts.rolling(window=3).std()     # 滚动标准差
ts.rolling(window=3).max()     # 滚动最大值

# expanding():从开始到当前位置的累计计算
ts.expanding().mean()          # 累计均值(截止到当前时间点的均值)
ts.expanding().sum()           # 累计求和

# 布尔参数:min_periods — 最少非空值才计算
ts.rolling(window=5, min_periods=3).mean()  # 至少3个值才计算,减少NaN

本章小结