Pandas 惯用法
优秀的 Pandas 代码应当清晰展示数据变换的步骤,并优先使用最精确的操作方法。
变换风格
import pandas as pd
orders = pd.DataFrame({
"customer_id": ["A", "A", "B"], "status": ["paid", "paid", "pending"],
"gross": [100, 50, 80], "fee": [10, 5, 8],
})
result = (
orders.loc[lambda x: x["status"].eq("paid")]
.assign(net=lambda x: x["gross"] - x["fee"])
.groupby("customer_id", as_index=False)
.agg(total_net=("net", "sum"), orders=("net", "size"))
.sort_values("total_net", ascending=False)
)
当链式调用变得难以阅读、复用或调试时,将其拆分为具名步骤。方法链旨在提升可读性,而非保证性能。
操作选型
def require_positive(frame, column):
if frame[column].isna().any() or not frame[column].ge(0).all():
raise ValueError(f"{column} must be present and non-negative")
return frame
clean = orders.pipe(require_positive, "gross")
优先向量化,慎用 apply
# 优先使用向量化操作
df = pd.DataFrame({"numerator": [10.0, 0.0], "denominator": [2.0, 0.0]})
df["ratio"] = df["numerator"].div(df["denominator"].replace(0, float("nan")))
# 仅当逻辑确实依赖整行数据且无法向量化时,才使用 apply
def classify_row(row):
if pd.isna(row["ratio"]):
return "undefined"
return "high" if row["ratio"] >= 4 else "low"
df["label"] = df.apply(classify_row, axis="columns")
逐行 apply 会为每一行构造 Series 对象并反复调用 Python 解释器,开销较大。请先确保表达式逻辑正确且清晰,并在具有代表性的数据上验证后,再进行性能基准测试。
修改规则
优先使用返回新对象的表达式和单步 .loc 赋值。避免链式赋值(Chained Assignment),也不要依赖 inplace=True 作为性能优化手段。
看清返回对象
方法链返回一行:客户 A、total_net=135、orders=2;原始 orders 仍有三行,也没有 net 列。pipe 直接返回函数的返回值。这里 require_positive 只检查数据并返回传入的表格,因此 clean.equals(orders) 为真;数据值没有被清洗。不要依赖跨版本的对象身份:pandas 3.0 会把浅副本传给函数。
除法示例明确把分母零映射为缺失,结果为 [5.0, NaN],标签为 high、undefined。若不处理,非零数除以零可能得到无穷大。这个小分类函数只是演示逐行回调的输入输出,简单逻辑也能向量化。DataFrame.map 要求 pandas 2.1 或更新版本。DataFrame.apply 默认逐列(axis=0),axis=1 则逐行;输出形状取决于回调返回什么。