跳至主要内容

Pandas .apply():它的作用、适用场景与更快的替代方案

了解 Python pandas 的 .apply 是什么,以及如何在 DataFrame 中使用它。今天就学习如何用 .apply() 函数迭代 DataFrame!
· 5分钟 阅读

使用 AI 探索

许多人使用 .apply() 来“避免循环”,并期待它既快又直接。实际上,按行的 .apply(axis=1) 仍然是 Python 层面的循环。在大数据量上可能很慢,而且有时会返回出乎意料的形状。解决方法很简单:常见任务使用向量化的 pandas/NumPy 操作,把 .apply() 留给确实需要跨多列处理的逻辑。

本指南介绍 .apply() 的当前行为,指出常见陷阱,并提供更快更清晰的可替换用法模式。

什么是 DataFrame.apply() 与 Series.apply()?

DataFrame.apply(func, axis=0) 默认对每一列调用 func。使用 axis=1 时,它会对每一行调用 func。而 Series.apply(func) 则对每个元素调用 func(或对整个 Series,这取决于新版本中的函数签名)。

  • 按行 vs. 按列: axis=1 意为“按行”。axis=0(默认)意为“按列”。
  • 返回形状规则(pandas ≥0.23): 如果您的函数对每行/列返回一个标量,得到的是 Series。如果返回 Series 或 dict,pandas 会将这些键展开为 DataFrame。如果返回列表/数组,除非设置 result_type='expand',否则您将得到一个由列表组成的 Series。
  • 更严格的错误(pandas ≥2.0): 列表类返回值的长度不匹配现在会抛出错误,而不是静默地产生不一致的结果。

优先推荐:向量化操作

大多数涉及合并或转换列的任务,使用向量化表达式或内置方法更快也更清晰。下面的示例在不使用 .apply() 的情况下计算一支棒球队的得失分差。

import pandas as pd

team_stats = pd.DataFrame({
    "Team": ["ARI", "ATL", "BAL"],
    "League": ["NL", "NL", "AL"],
    "Year": [2012, 2012, 2012],
    "RunsScored": [734, 700, 712],
    "RunsAllowed": [688, 600, 705],
    "Wins": [81, 94, 93],
    "Games": [162, 162, 162],
    "Playoffs": [0, 1, 1],
})

# Vectorized: fast and idiomatic
team_stats["RunDiff"] = team_stats["RunsScored"] - team_stats["RunsAllowed"]
print(team_stats[["Team", "RunsScored", "RunsAllowed", "RunDiff"]])

当您确实需要时再用按行 .apply()

当您的逻辑确实跨越多列且不易向量化时(例如依赖多个字段的条件规则),请使用 .apply(axis=1)。

从多列计算派生值

此模式使用多个输入为每行计算一个值。仍然更推荐上面的向量化方法,但这里展示了正确的按行用法,以及会影响速度与输出的选项。

def compute_run_diff(row):
    # Treat the row as read-only; return a scalar
    return row["RunsScored"] - row["RunsAllowed"]

# Row-wise apply (Python-level loop; can be slow on large data)
team_stats["RunDiff_apply"] = team_stats.apply(compute_run_diff, axis=1)

当函数是数值型并且只需要原始数组时,传入 raw=True 会直接向您的函数提供一个 NumPy 数组,从而跳过部分 pandas 的开销。

import numpy as np

def compute_run_diff_raw(values):
    # values is a NumPy array when raw=True
    # Order matches the column order we select
    rs, ra = values
    return rs - ra

team_stats["RunDiff_raw"] = team_stats[["RunsScored", "RunsAllowed"]].apply(
    compute_run_diff_raw, axis=1, raw=True
)

先验证一次函数输入

在我接入一个新的按行函数时,我会先打印一行来确认传入的是什么。

def debug_row(row):
    # Print the first row only
    if row.name == team_stats.index[0]:
        print("Example row:", row.to_dict())
    return 0

_ = team_stats.apply(debug_row, axis=1)

案例研究:求和、赛季总计与文本标记

针对光芒队(Rays)逐年数据,尽量使用内置方法和按元素工具,能不用 .apply() 就不用。

rays_by_year = pd.DataFrame(
    {
        "Year": [2012, 2011, 2010, 2009, 2008],
        "RunsScored": [697, 707, 802, 803, 774],
        "RunsAllowed": [577, 614, 649, 754, 671],
        "Wins": [90, 91, 96, 84, 97],
        "Playoffs": [0, 1, 1, 0, 1],
    }
).set_index("Year")

高效获取列求和

使用 DataFrame.sum() 代替 .apply(sum)。它更快也更清晰。

# Preferred
totals = rays_by_year.sum(axis=0)
print(totals)

# If you must use apply (not recommended here)
totals_apply = rays_by_year.apply(sum, axis=0)

计算某赛季的总得分与失分

跨列做向量化运算;无需使用 .apply()。

rays_by_year["TotalRuns"] = rays_by_year["RunsScored"] + rays_by_year["RunsAllowed"]
print(rays_by_year[["RunsScored", "RunsAllowed", "TotalRuns"]].head())

将 0/1 标记转换为文本

对单列进行按元素转换时,优先使用 Series.map() 或 replace()。

# Using map with a dict
rays_by_year["PlayoffsText"] = rays_by_year["Playoffs"].map({0: "No", 1: "Yes"})

# Equivalent with replace
rays_by_year["PlayoffsText2"] = rays_by_year["Playoffs"].replace({0: "No", 1: "Yes"})

控制 .apply() 的输出形状

当您的函数每行返回多个值时,请显式指定输出形状。这可以避免意外以及未来版本变更带来的问题。

返回列表并展开为多列

使用 result_type='expand' 将列表/数组拆分为多个列。

def wins_losses(row):
    # Derive wins and losses as two outputs
    wins = row["Wins"]
    losses = row["Games"] - row["Wins"] if "Games" in row else np.nan
    return [wins, losses]

# Example using team_stats, which has "Wins" and "Games"
expanded = team_stats.apply(wins_losses, axis=1, result_type="expand")
expanded.columns = ["WinsOut", "LossesOut"]
team_stats = pd.concat([team_stats, expanded], axis=1)

返回 Series 或 dict 以自动命名列

从每行返回一个 Series 或 dict 会生成一个列标签匹配的 DataFrame。

def summary_row(row):
    return pd.Series(
        {
            "IsWinningSeason": row["Wins"] >= 90,
            "RunRatio": (row["RunsScored"] / row["RunsAllowed"]),
        }
    )

summary = team_stats.apply(summary_row, axis=1)
team_stats = pd.concat([team_stats, summary], axis=1)

关键性能建议

按行的 .apply(axis=1) 很方便,但在大 DataFrame 上很慢,因为它会对每一行调用一次您的 Python 函数。以下模式可避免这一瓶颈。

  • 优先使用向量化的 pandas/NumPy 操作以及 sum、mean、clip、where、astype 以及字符串/访问器方法等内置方法。
  • 对于可接收数组的数值型按行/按列操作,传入 raw=True 以获取 NumPy 数组并降低 pandas 开销。
  • 针对单列的按元素处理,使用 Series.map() 或向量化方法,而非 DataFrame.apply(axis=1)。
  • 避免在函数内部修改传入的行/列;请返回新值。

您需要了解的版本说明(pandas 2.x)

近期的 pandas 版本收紧了 .apply() 的行为,使结果更可预测。

  • 类列表返回值: 自 2.0+ 起,长度不匹配会抛出错误。请确保长度一致,并在需要多列时设置 result_type='expand'。
  • 输出展开: 返回 Series 或 dict 会展开为以这些键为列名的 DataFrame(自 0.23 起稳定)。
  • Series.apply() 的变更: convert_dtype 参数已弃用。若需要混合类型,请先转换为 object(例如 s.astype("object").apply(fn))。较新的签名允许在更多场景下控制 fn 接收标量或 Series(例如 by_row)。

.apply() vs. .map() vs. .applymap() vs. .agg()

请选择与您的变换“形状”相匹配的 API。

  • Series.map(func_or_dict): 对单列进行按元素变换。最适合查表或简单函数。
  • DataFrame.apply(func, axis=1): 需要多列参与的按行逻辑。
  • DataFrame.apply(func, axis=0): 按列逻辑(每次输入是代表一列的 Series)。
  • DataFrame.applymap(func): 对每个单元格按元素操作。谨慎使用;向量化方法更快。
  • .agg()/.transform(): 聚合与组内变换;在 groupby 流水线中更应优先使用。

常见错误与快速修复

以下问题最常导致结果不正确或性能下降,并附有相应修复方法。

  • 忘记为按行逻辑设置 axis=1: 如果您的函数突然收到的是列而不是行,请添加 axis=1。
  • 意外得到列表的 Series: 当每行返回列表/数组时,设置 result_type='expand' 以拆分为多列。
  • 按行代码很慢: 用向量化表达式或内置函数替代;如不可行,考虑使用 raw=True 降低开销。
  • 修改输入行: 将输入视为只读,请返回新值。
  • Dtype 漂移: 如果您的函数有时返回非整数,整数列可能会被提升为 float 或 object。必要时可使用 astype 进行回转。

结语

.apply() 是个灵活的工具,但并不是性能捷径。对于算术、聚合以及单列的按元素变换,请使用向量化操作。只有当您的逻辑确实需要按行访问多列时,才使用 DataFrame.apply(axis=1)。在使用时,通过 result_type 控制输出形状,数值函数可考虑 raw=True,并留意 dtypes。这些模式在现代 pandas 中能产出可预测的结果,并随数据增长而更好扩展。

主题