pandas 的变换三兄弟 map / apply / applymap 是数据加工的核心工具:map 逐元素映射、apply 逐行/逐列处理、applymap 全表逐元素处理。本篇文章讲清它们的区别、用法与性能。
目录
1. 三兄弟的区别一张表看懂
2. map:Series 逐元素映射
3. applymap:DataFrame 逐元素
4. apply:Series 逐元素 / DataFrame 逐行逐列
5. apply 与 axis 参数
6. transform:与 apply 的对比
7. pipe:管道式处理
8. 性能对比与选择建议
9. 常见坑与注意事项
10. 本章小结与练习
1. 三兄弟的区别一张表看懂
| 方法 | 作用对象 | 处理粒度 | 传入函数参数 | 典型场景 |
|------|----------|----------|--------------|----------|
| Series.map | Series | 每个元素 | 元素值 | 字典映射、元素级清洗 |
| Series.apply | Series | 每个元素 | 元素值 | 逐元素复杂处理 |
| DataFrame.applymap | DataFrame | 每个元素 | 元素值 | 全表格式化 |
| DataFrame.apply | DataFrame | 每行/每列 | 一行/一列数据 | 行/列级聚合、处理 |
先准备数据:
import pandas as pd
import numpy as np
df = pd.DataFrame({
"姓名": ["张三", "李四", "王五"],
"部门": ["技术", "销售", "市场"],
"工资": [12000, 9000, 11000],
})
print(df)
2. map:Series 逐元素映射
map 可以接受字典、函数或Series,把每个元素映射成新值。
2.1 用字典映射(最常用)
# 部门编号映射为中文
dept_map = {"技术": "Tech", "销售": "Sales", "市场": "Mkt"}
df["部门英文"] = df["部门"].map(dept_map)
print(df)
姓名 部门 工资 部门英文
0 张三 技术 12000 Tech
1 李四 销售 9000 Sales
2 王五 市场 11000 Mkt
经典应用:分类编码 / 打标签
# 工资分档
df["工资等级"] = df["工资"].map(lambda x: "高" if x >= 10000 else "低")
# 性别编码
df["性别"] = ["男", "女", "男"]
df["性别编码"] = df["性别"].map({"男": 1, "女": 0})
2.2 用函数映射
df["工资千元"] = df["工资"].map(lambda x: x / 1000)
# 任何函数都行
import math
df["工资对数"] = df["工资"].map(math.log)
2.3 字典中没有的键
# 未覆盖的键 -> NaN
s = pd.Series(["a", "b", "c"])
print(s.map({"a": 1, "b": 2}))
# 0 1.0
# 1 2.0
# 2 NaN
map 遇到字典中不存在的值返回 NaN,适合做"白名单"映射。2.4 与 replace 的区别
s = pd.Series(["a", "b", "c"])
# map:未匹配 -> NaN
print(s.map({"a": 1}))
# replace:未匹配保持不变
print(s.replace({"a": 1}))
# 0 1
# 1 b
# 2 c
3. applymap:DataFrame 逐元素
applymap 对 DataFrame 的每个单元格应用函数,返回同形状的 DataFrame。
df_num = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
print(df_num.applymap(lambda x: x * 10))
# a b
# 0 10 30
# 1 20 40
# 格式化应用:所有值保留两位小数并加单位
df_fmt = df_num.applymap(lambda x: f"{x:.2f}元")
print(df_fmt)
pandas 2.1+ 推荐改用df.map(...)(新 API),功能相同。老版本请继续用applymap。
4. apply:Series 逐元素 / DataFrame 逐行逐列
4.1 Series.apply:逐元素
s = pd.Series([1, 2, 3])
# 等价于 map
print(s.apply(lambda x: x ** 2))
# 0 1
# 1 4
# 2 9
Series.apply与Series.map在传单元素函数时几乎等价,但map支持字典、apply支持复杂参数。
4.2 DataFrame.apply:逐行/逐列
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
# 默认 axis=0:函数接收每一列(Series)
print(df.apply(sum))
# a 6
# b 15
# axis=1:函数接收每一行(Series)
print(df.apply(lambda row: row["a"] + row["b"], axis=1))
# 0 5
# 1 7
# 2 9
# 返回多列结果
def row_metrics(row):
return pd.Series({
"总和": row["a"] + row["b"],
"差值": row["a"] - row["b"],
})
print(df.apply(row_metrics, axis=1))
# 总和 差值
# 0 5 -3
# 1 7 -3
# 2 9 -3
4.3 apply 的常见场景
# 1. 对每列做非标准统计
df.apply(lambda col: col.max() - col.min())
# a 2
# b 2
# 2. 对每行做复杂业务逻辑
def calc(row):
"""根据多列计算奖金"""
base = row["a"] * 100
if row["b"] > 5:
return base * 1.2
return base
df["奖金"] = df.apply(calc, axis=1)
# 3. 条件分支(apply 里可以写 if/else)
df["标签"] = df.apply(
lambda row: "大" if row["a"] + row["b"] > 8 else "小", axis=1
)
4.4 apply 传参
def scale(x, factor):
return x * factor
# 用 args 传位置参数
print(df["a"].apply(scale, args=(10,)))
# 用 **kwargs 传关键字参数
print(df["a"].apply(lambda x: x + 100)) # 或直接闭包
4.5 result_type 控制返回形状
# 返回 Series 时,result_type="expand" 展开成 DataFrame
print(df.apply(lambda col: pd.Series({"max": col.max(), "min": col.min()}), result_type="expand"))
# a b
# max 3 6
# min 1 4
5. apply 与 axis 参数
再强调一次 axis(结合第 3 章):
| axis | 含义 | apply 传入的内容 |
|------|------|------------------|
| axis=0(默认) | 按列 | 每列一个 Series |
| axis=1 | 按行 | 每行一个 Series |
# axis=0:对列操作
df.apply(np.mean) # 每列均值
# axis=1:对行操作
df.apply(np.mean, axis=1) # 每行均值
记忆:apply的 axis 语义与sum/mean完全一致:axis=0 竖直方向(跨行)→ 按列;axis=1 水平方向(跨列)→ 按行。
6. transform:与 apply 的对比
transform 与 apply 的重要区别:transform 保持形状不变(一对一变换),且可以同时传多个函数。
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
# apply 可以改变形状(聚合)
print(df.apply(sum)) # 返回一行
# transform 保持形状(逐列变换,结果与原形状相同)
print(df.transform(lambda x: x * 2))
# a b
# 0 2 8
# 1 4 10
# 2 6 12
# 多个函数同时应用
print(df.transform(["sum", "mean"]))
# a b
# sum 6.0 15.0
# mean 2.0 5.0
transform 常用于 groupby 分组后"每组内做变换并保持原行数"(详见第 13 章)。
7. pipe:管道式处理
当你要把一连串变换串联起来时,pipe 让代码更可读、可复用:
def add_col(df, col_name, values):
df[col_name] = values
return df
def double_col(df, col):
df[col] = df[col] * 2
return df
# 链式调用(函数必须返回 DataFrame)
df_pipe = (df
.pipe(add_col, "c", [7, 8, 9])
.pipe(double_col, "c"))
print(df_pipe)
等价写法对比:
# 传统写法(嵌套,难读)
double_col(add_col(df, "c", [7, 8, 9]), "c")
# pipe 写法(顺序清晰)
df.pipe(add_col, "c", [7, 8, 9]).pipe(double_col, "c")
8. 性能对比与选择建议
| 操作 | 性能 | 建议 |
|------|------|------|
| 向量化运算(df["a"] * 2) | ⭐⭐⭐⭐⭐ | 能向量化绝不 apply |
| map / replace(字典) | ⭐⭐⭐⭐ | 映射首选 |
| apply 逐列 | ⭐⭐⭐ | 需要复杂逻辑时用 |
| apply 逐行(axis=1) | ⭐⭐ | 大表慎用,性能差 |
| Python 循环(for) | ⭐ | 大数据量尽量避免 |
# 性能对比:1 百万行
import time
n = 1_000_000
df = pd.DataFrame({"x": np.random.rand(n)})
# 向量化(最快)
t0 = time.time()
r1 = df["x"] * 2
print(f"向量化: {time.time()-t0:.3f}s")
# map
t0 = time.time()
r2 = df["x"].map(lambda v: v * 2)
print(f"map: {time.time()-t0:.3f}s")
# apply 逐列
t0 = time.time()
r3 = df["x"].apply(lambda v: v * 2)
print(f"apply: {time.time()-t0:.3f}s")
# 循环(最慢,勿用于生产)
# for i in range(n): ...
原则:能向量化(+、*、>, 内置方法)就不要 apply;能用 map 就不要 apply;apply 只用于"向量化写不出来"的复杂逻辑。
9. 常见坑与注意事项
| 坑 | 现象 | 解决办法 |
|----|------|----------|
| map 的字典没覆盖全 | 静默变 NaN | 确认映射完整,或改用 replace |
| apply 忘写 axis=1 | 处理对象错误 | 逐行处理必须 axis=1 |
| apply 返回形状不一致 | 结果混乱 | 返回 Series 时指定列名/result_type |
| 循环替代向量化 | 慢到怀疑人生 | 优先向量化、map、apply |
| apply 修改原表副作用 | 数据被意外改动 | 在函数内用 copy() |
| applymap 处理日期列 | 报错 | 先 select_dtypes 过滤出需要的列 |
10. 本章小结与练习
小结
map:Series 逐元素,字典/函数/Series 映射,未命中→NaN;applymap(新版本df.map):DataFrame 逐元素格式化;apply:Series 逐元素、DataFrame 逐行(axis=1)/逐列(axis=0);transform:保持形状的一对一变换,支持多函数;pipe:链式管道,代码可读可复用;- 性能:向量化 > map > apply 逐列 > apply 逐行 > 循环。
练习题
1. 用字典 map 把"春夏秋冬"映射为 1/2/3/4。
2. 用 apply 给每行计算"工资×0.9 后取整"。
3. 用 applymap 把 DataFrame 所有数值格式化为带千分位的字符串。
4. 用 transform 同时计算每列的 sum 和 mean。
5. 写一个 pipe 链:加一列 → 对列做平方 → 过滤大于 10 的行。
下一篇预告:第 11 章 数据合并:concat / merge / join —— 把多张表拼在一起的三种武器。
文章回复
0 条公开回复