数据清洗和筛选任务里,字典是最常用的数据结构之一。比如你手里有一份用户行为数据,另有一份以用户ID为键、以标签或布尔值为值的字典,现在需要根据字典把符合条件的行为记录留下来。在pandas时代,很多人习惯用apply加lambda逐行查字典,数据量一大就明显吃力。Polars基于Rust实现,提供了向量化表达式和惰性执行机制,处理同样的问题有更高效的路径。本文把几种典型写法梳理一遍,对比它们的性能表现,并给出在具体场景下的选择建议。

基础方案:is_in与条件表达式的组合
最直接的做法是把字典的键取出来,转成列表后交给is_in做成员判断。这种写法完全走Polars的内部向量化路径,不需要任何Python层面的循环,通常是最好的起点。假设字典的形式是{用户ID: 标签},只保留字典中存在的ID对应的行,代码可以写成下面这样。
import polars as pl
df = pl.DataFrame({
"user_id": [101, 102, 103, 104, 105],
"action": ["click", "view", "buy", "click", "view"]
})
# 字典:键为用户ID,值为标签
user_dict = {101: "vip", 103: "normal", 105: "vip"}
result = df.filter(pl.col("user_id").is_in(list(user_dict.keys())))
print(result)
如果字典的值本身也是筛选条件的一部分,例如只保留标签为vip的记录,可以在过滤后继续用表达式处理,也可以把两步合成一步。注意is_in接受的参数可以是列表、Series或另一个表达式,当字典较大时,先把它转成pl.Series再传入,比普通Python列表的构造开销更低。
# 一步完成:只保留字典中存在且标签为vip的行
vip_ids = [k for k, v in user_dict.items() if v == "vip"]
result = df.filter(pl.col("user_id").is_in(vip_ids))
进阶方案:把字典值映射成新列再过滤
很多时候我们不只是要判断存在性,还希望把字典的值作为新列带出来参与后续计算。这时可以用map_elements逐行查字典,写法直观,但要清楚它的代价:每一行都会触发一次Python函数调用,等于把向量化优势丢掉了。数据量在几万行以内问题不大,到了百万行级别就会慢一个数量级以上。
# 写法一:map_elements逐行查字典(直观但较慢)
df_with_tag = df.with_columns(
pl.col("user_id").map_elements(
lambda x: user_dict.get(x, "unknown"),
return_dtype=pl.Utf8
).alias("tag")
).filter(pl.col("tag") == "vip")
更推荐的做法是构造一个临时的两列DataFrame,然后用join代替逐行查找。join在Polars内部是高度优化的哈希连接,字典哪怕有几十万个键,性能也几乎不受影响。这种思路上的转变——把字典查找转化为表连接——是Polars和Spark这类工具中非常典型的优化技巧。
# 写法二:字典转DataFrame后join(推荐,性能好)
dict_df = pl.DataFrame({
"user_id": list(user_dict.keys()),
"tag": list(user_dict.values())
})
result = df.join(dict_df, on="user_id", how="inner")
print(result)
两种写法在功能上等价,但基准测试的差距很明显。在一千万行的DataFrame上配合三百万键的字典,join方案通常能在亚秒级完成,而map_elements方案往往需要数十秒。如果只需要过滤不需要带出标签列,is_in配合pl.Series是最快的;需要值时就用join,两者都可以完全避开Python循环。
复杂条件下的when_then与结构化字典
当字典的值不是简单的标量,而是包含多个字段的元组或嵌套结构时,例如{城市: (省份, 区域)},直接join仍然可行,只需要在构造字典DataFrame时用pl.struct或拆分成多列即可。另一种情况是字典的值本身是布尔条件或阈值参数,需要参与表达式计算,这时when_then.otherwise配合映射列会更灵活。
import polars as pl
df = pl.DataFrame({
"city": ["杭州", "苏州", "合肥"],
"sales": [120, 80, 60]
})
# 字典值为阈值:每个城市有不同的达标线
thresholds = {"杭州": 100, "苏州": 90, "合肥": 50}
th_df = pl.DataFrame({
"city": list(thresholds.keys()),
"threshold": list(thresholds.values())
})
result = (
df.join(th_df, on="city", how="left")
.filter(pl.col("sales") >= pl.col("threshold"))
)
print(result)
这种写法的好处是把“每个城市标准不同”这类业务规则变成了数据驱动的一列,后续如果阈值调整,只需要更新字典而不必改动过滤逻辑。相比之下,如果用map_elements把阈值查出来再比较,代码可读性相近,但性能差距会随数据规模线性放大。
惰性执行与流式处理的注意事项
前面所有示例用的都是即时执行的DataFrame API。Polars还提供LazyFrame,通过lazy()进入惰性模式,查询会在collect()时经过查询优化器统一规划。对于“字典过滤后再聚合”这类组合操作,惰性模式可以调整执行顺序,比如先做过滤再做join,减少中间数据量,实际收益可能非常可观。
lazy_result = (
df.lazy()
.join(dict_df.lazy(), on="user_id", how="inner")
.group_by("tag")
.agg(pl.len().alias("cnt"))
.collect()
)
还有几个细节值得注意。第一,join的how参数决定了保留哪些行,inner相当于过滤,left配合filter(pl.col("tag").is_not_null())也能达到同样效果但语义不同,按需选择。第二,如果字典键和DataFrame列的数据类型不一致(比如一边是int一边是str),join和is_in都会静默匹配不到任何行,这是实际使用中最常见的坑,务必先统一类型。第三,对于超出内存的大表,可以开启streaming=True让collect阶段走流式引擎,字典端作为较小的一侧会被构建成哈希表广播出去,内存占用可控。
总结一下选择思路:只需要按字典键过滤时,用is_in加pl.Series;需要把字典值带出来时,用字典转DataFrame再join;字典值参与条件计算时,join出参数列后用表达式比较;任何情况下都尽量避免在map_elements里做高频字典查找。掌握“字典即小表”这个观念,大部分相关的性能问题都能迎刃而解。
Polars数据过滤字典映射polars filter修改时间:2026-09-12 07:44:45