6.1 向量化列转换
第 5 章在保留原始证据的前提下清洗了数据列。接下来要从这些列派生分析变量:用数量和单价计算收入,用时间戳计算延迟,或用多个条件生成复核标签。向量化转换(vectorized transformation)一次描述整个 Series 或数组的关系,而不是手动让 Python 逐行处理。
“向量化”不表示每个操作都会神奇地变快。它首先是一种表达方式:代码直接操作完整的 pandas 或 NumPy 对象,让数据关系可见,并把重复计算交给经过优化的数组操作。
把公式看成对齐的列关系
假设 orders 有 units、unit_price 和 cost 三列。逐行公式为
整列写法是:
orders["profit"] = (
orders["units"] * orders["unit_price"]
- orders["cost"]
)这些 Series 共享 DataFrame 索引,因此 pandas 会先按标签对齐再做算术。合并独立生成的 Series 时,这是一项重要保护:长度相同不等于行身份相同。如果索引不一致,结果会采用标签并集,没有配对标签的位置成为缺失。
对于互相依赖的派生列,assign 可以不修改源对象,并清楚显示管线顺序:
analysis = orders.assign(
revenue=lambda d: d["units"] * d["unit_price"],
profit=lambda d: d["revenue"] - d["cost"],
margin=lambda d: (
d["profit"] / d["revenue"]
).where(d["revenue"] > 0),
)每个 callable 都会收到当前步骤产生的 DataFrame,所以后面的 profit 能引用刚建立的 revenue。只有收入为正时才计算利润率,避免用无穷大伪装一个未定义比率。括号也不只是排版:它明确表达分子和保护条件,并让表达式安全换行。
不要为了显示整齐而提前舍入中间值。分析列保留完整精度,只在表格或图表的展示层舍入;过早舍入可能改变阈值判断与累计总量。
用布尔掩码组合决策
orders["delay_minutes"] > 30 会返回与订单索引对齐的布尔掩码(Boolean mask)。为掩码命名后再组合,规则会更容易审计:
invalid_amount = orders["amount"].isna() | orders["amount"].lt(0)
severe_late = orders["delay_minutes"].ge(60)
high_value_late = (
orders["amount"].ge(500)
& orders["delay_minutes"].gt(30)
)逐元素的与、或、非分别使用 &、| 和 ~。每个比较都要加括号,因为 Python 运算符优先级不一定符合自然语言直觉:
eligible = (orders["amount"] >= 100) & orders["customer_id"].notna()两个结果可以使用 where、mask 或 np.where。不过,可空布尔值参与比较时可能继续保留 pd.NA。此时需要明确决定:未知继续保持未知,成为单独类别,还是进入人工复核。直接把未知当成假,会改变业务政策。
多个互斥结果可交给 np.select,它选择第一个为真的条件:
import numpy as np
conditions = [invalid_amount, severe_late, high_value_late]
choices = ["invalid_amount", "severe_late", "high_value_late"]
reason = np.select(conditions, choices, default="ok")
analysis = orders.assign(
review_reason=pd.Series(reason, index=orders.index, dtype="string"),
needs_review=invalid_amount | severe_late | high_value_late,
)条件可能重叠。例如,负金额订单也可能严重逾期。因此,条件列表的顺序属于业务规则,并非格式细节。应给掩码命名、记录优先级,并用交叉表测试重要的重叠情况。
常见的其他向量化转换包括:
clip(lower=..., upper=...):建立有声明的截尾场景,而不是覆盖原值。
where(condition):条件为真时保留值,其余位置标为缺失。
mask(condition, replacement):条件为真时替换值。
np.log1p等 NumPy 通用函数(universal function, ufunc):在检查输入定义域后进行数学变换。
np.log1p(x) 在零附近能准确计算 ,但 时没有实数定义。函数是向量化的,不代表它不需要数据契约。
验证转换含义,而不只是验证语法
表达式成功执行,也可能使用了错误总体或单位。每个派生列都应记录输入、公式、缺失策略、单位和有效范围,并测试不变量:
assert analysis.index.equals(orders.index)
assert analysis["revenue"].ge(0).all(skipna=True)
assert analysis.loc[analysis["revenue"].le(0), "margin"].isna().all()还要比较转换前后的计数,检查边界行,并保留源字段。下一节处理内置表达式不够用的场景,并学习如何在 map、apply、聚合等接口之间做出有依据的选择。