Python技术迷

别再写np.where嵌套地狱了!Pandas条件逻辑的向量化写法让效率翻倍

np.where 一层套一层,写到第三层的时候,代码其实已经不是“条件判断”了,是在给下一个接手的人埋雷。

我平时看这种代码,第一眼先不看逻辑对不对,先看它敢不敢改。敢不敢加一个新分支,敢不敢排查线上脏数据,敢不敢回头解释“为什么这批数据标签打错了”。很多 np.where 嵌套写法,跑是能跑,维护基本靠忍。

像下面这种,就挺常见:

import numpy as np
import pandas as pd

df = pd.DataFrame({
"amount": [19, 88, 188, 399, 520],
"is_member": [0, 1, 1, 0, 1],
"refund_flag": [0, 0, 1, 0, 1]
})

df["level"] = np.where(
    df["refund_flag"] == 1, "退款单",
    np.where(
        df["amount"] >= 300, "大额订单",
        np.where(
            (df["amount"] >= 100) & (df["is_member"] == 1), "会员中单",
"普通订单"
        )
    )
)

这段不算长,已经开始别扭了。再多两个条件,括号一绕,谁都不敢碰。更麻烦的是,np.where 适合二选一,不适合拿来堆复杂业务规则。你硬堆,它也不会报错,只会把可读性一点点吃干净。

这种场景我更愿意直接上向量化条件写法,先把“规则表”摆出来。

import numpy as np
import pandas as pd

df = pd.DataFrame({
"amount": [19, 88, 188, 399, 520],
"is_member": [0, 1, 1, 0, 1],
"refund_flag": [0, 0, 1, 0, 1]
})

conditions = [
    df["refund_flag"].eq(1),
    df["amount"].ge(300),
    df["amount"].ge(100) & df["is_member"].eq(1)
]

choices = [
"退款单",
"大额订单",
"会员中单"
]

df["level"] = np.select(conditions, choices, default="普通订单")
print(df)

这就顺眼多了。

np.select 这个东西,特别适合“多条件、多分支、按优先级命中”的场景。上面三条规则,谁优先谁靠前,最后没命中的走 default。不是说它多高级,主要是业务判断终于像业务判断了,不再像在拆炸弹。

再往前走一步,很多人其实不是不会写条件,是喜欢把清洗、分类、兜底全塞进一行。这个味道一出来,后面排查数据问题就很累。比如订单风控标签,经常会写成这种:

df["risk_tag"] = np.where(
    df["device_id"].isna(), "设备缺失",
    np.where(
        df["pay_amount"] > 1000, "高金额",
        np.where(
            df["city"].isin(["异地", "未知"]), "地域异常",
"正常"
        )
    )
)

这种我一般会拆,顺手把中间判断也留下来。因为线上真出问题时,你不是只想要结果列,你还想知道是哪条规则打中的。

df["missing_device"] = df["device_id"].isna()
df["high_amount"] = df["pay_amount"].gt(1000)
df["bad_city"] = df["city"].isin(["异地", "未知"])

df["risk_tag"] = np.select(
    [
        df["missing_device"],
        df["high_amount"],
        df["bad_city"]
    ],
    [
"设备缺失",
"高金额",
"地域异常"
    ],
    default="正常"
)

这样写有个很实在的好处:你随手就能统计每条规则命中了多少数据。

print(df[["missing_device", "high_amount", "bad_city"]].sum())

排查的时候,这个比盯着一长串嵌套 np.where 强太多了。到底是设备字段空得多,还是金额阈值打狠了,一眼就能看出来。

再说效率。很多人一看复杂条件,就开始 apply(lambda row: ...)。这玩意儿不是不能用,但只要数据量上来,基本就慢得肉眼可见。Pandas 该走向量化的时候,就别把它用回 for 循环思维。

比如会员折扣规则:

# 别这么写
df["discount"] = df.apply(
lambda row: 0.3if row["vip"] == 1and row["amount"] >= 500
else0.2if row["vip"] == 1
else0.05,
    axis=1
)

换成向量化:

df["discount"] = np.select(
    [
        df["vip"].eq(1) & df["amount"].ge(500),
        df["vip"].eq(1)
    ],
    [0.3, 0.2],
    default=0.05
)

这类写法快不快,数据一大就知道了。尤其是几十万、上百万行的时候,apply 和向量化根本不是一个量级。

当然也不是说 np.where 不能用。简单二选一它很好,干脆,直接:

df["is_big"] = np.where(df["amount"].ge(100), 1, 0)

就这一层,没问题。真到了三层四层,你还坚持往里套,我一般就默认这段代码后面迟早要返工。

写 Pandas 条件逻辑,判断标准其实很朴素:

代码跑得快是一层; 别人能看懂是一层; 三天后你自己还能改,这才是真的稳。

能用 np.select 把优先级摊开写,就别把条件塞进 np.where 嵌套地狱里。数据处理代码不是为了秀技巧,是为了下次查问题的时候,别先把自己看烦了。参考气质校准见