6.2 正确使用 map 与 apply
6.1 节说明,普通算术和条件组合应优先写成向量化表达式。有些转换确实需要查找表或自定义函数。此时不要先问“apply 能不能做”,而要问“这个操作需要看到什么形状的信息”。
根据输入与输出形状选择接口
不同 API 表达不同契约:
| 需求 | 自然操作 | 函数看到的内容 |
|---|---|---|
| 整列算术、字符串或日期操作 | 向量化 Series 操作 | 通常不需要 Python 回调 |
| 用字典或标量函数替换 Series 的每个值 | Series.map | 一个值或一次映射查找 |
| 转换 DataFrame 的每个单元格 | DataFrame.map | 一个标量单元格 |
| 对每一列或每一行执行自定义逻辑 | DataFrame.apply | 一列或一行组成的 Series |
| 生成摘要 | agg 或命名聚合 | 把一列或一组缩减成结果 |
| 组内计算后保持原行数 | transform | 一个组,并返回与原行对齐的结果 |
选择能够准确表达任务的最窄接口。查找任务写成查找,比包装成任意逐行函数更容易理解和测试。
受控代码适合使用字典映射:
zone_names = {
"N": "North",
"S": "South",
"W": "West",
}
mapped = orders["zone_code"].map(zone_names)
unknown = orders["zone_code"].notna() & mapped.isna()普通字典会把未命中的键映射为缺失。这正是受控词表需要的失败信号:审计 orders.loc[unknown, "zone_code"],不要用一个看似合理的默认区域隐藏新代码。
使用函数映射时,na_action="ignore" 可以让缺失值不进入 callable:
labels = orders["customer_name"].map(
lambda value: value.strip().casefold(),
na_action="ignore",
)这个例子本身更适合向量化字符串方法:orders["customer_name"].str.strip().str.casefold()。它更直接地表达文本领域。真正的映射或没有专用操作的标量逻辑,才是 map 最清楚的使用场景。
使用 DataFrame.apply 前先理解轴
DataFrame.apply 每次向函数传入一个 Series:
axis=0是默认值,依次传入列,Series 的索引是行标签。
axis=1依次传入行,Series 的索引是列名。
跨越同一观测多个字段的规则可以写成逐行函数:
def shipping_fee(row):
if pd.isna(row["weight_kg"]):
return pd.NA
surcharge = 8.0 if row["zone"] == "remote" else 0.0
return 5.0 + 1.2 * row["weight_kg"] + surcharge
fee = shipments.apply(shipping_fee, axis=1).astype("Float64")如果遗漏 axis=1,函数收到的是整列,row["weight_kg"] 就不再符合变量名暗示的含义。调试时可以先调用 shipping_fee(shipments.iloc[0]),或临时观察函数收到的 Series。
函数返回值会影响输出形状。每行返回标量会生成 Series;返回带名称的 Series 可以展开为多列:
def fee_details(row):
base = 5.0 + 1.2 * row["weight_kg"]
surcharge = 8.0 if row["zone"] == "remote" else 0.0
return pd.Series({"base_fee": base, "surcharge": surcharge})
details = shipments.apply(fee_details, axis=1)返回长度不一致的列表或混合类型,会让结果形状难以预测。应定义稳定的返回模式。传入 apply 的用户自定义函数(User-Defined Function, UDF)也不能修改它收到的行或列对象;这种内部变异不受支持,可能产生意外结果。
普通逐行 apply 本质上仍是 Python 层循环。建立正确的参考实现后,应寻找向量化等价写法:
fee_vectorized = (
5.0
+ 1.2 * shipments["weight_kg"].astype("Float64")
+ shipments["zone"].eq("remote").mul(8.0)
).where(shipments["weight_kg"].notna())
pd.testing.assert_series_equal(
fee,
fee_vectorized,
check_names=False,
)只有确认缺失、dtype、索引和值全部等价后,才在代表性数据上比较性能。实现了另一套政策的“更快代码”不叫优化。
区分聚合与同形转换
多行输入缩减为少量摘要值时使用 agg:
summary = orders[["amount", "delay_minutes"]].agg(["min", "median", "max"])组内计算需要返回与原行逐一对齐的结果时使用 transform:
zone_median = orders.groupby("zone")["amount"].transform("median")
orders["amount_vs_zone"] = orders["amount"] - zone_median输出形状是可靠判断线索:聚合会减少观测,转换会保留观测数。下一节把这些思想用于三类参数本身就带有分析含义的转换:分箱、排名与类别编码。