上期我们讲了DataFrame的筛选、排序、变形三板斧。有鱼油问我:大龙虾,filter和select我都学了,但感觉有些操作写起来还是很麻烦,比如:
这些问题,表达式引擎都能搞定。
表达式引擎是Polars最核心的功能。学会了它,你的代码不仅更快,还更优雅、更Pythonic。SQL怎么写,Polars就怎么写。
01 | 什么是表达式?
先说个容易理解的类比:
表达式就是"加工指令"。
你有一列数据,想要:过滤、转换、求和、平均、排名……每一种操作都是一个"指令"。Polars的表达式,就是把这些指令封装成对象,可以组合、可以链式调用、可以嵌套。
举个例子:
# 这个就是表达式
pl.col("年龄") * 2 + 10
# 等价于SQL里的
# (年龄 * 2) + 10
表达式不是直接执行的值,而是"计算规则"。等你把它放到 .select() 或 .with_columns() 里,它才会真正被执行。
02 | col()、lit()、struct():表达式的基石
2.1 col():引用列
col() 是最常用的,用来引用某列:
pl.col("姓名") # 引用"姓名"列
pl.col("年龄", "城市") # 同时引用多列(返回列表)
pl.col("*") # 所有列
pl.col(pl.Float64) # 所有Float64类型的列
2.2 lit():字面量(常量)
lit() 用来创建常量值:
pl.lit(100) # 数字100
pl.lit("北京") # 字符串"北京"
pl.lit(None) # 空值
pl.lit(datetime.now()) # 当前时间
常用于:
2.3 struct():打包成结构体
struct() 把多列打包成一个结构体列,类似于JSON对象:
df = pl.DataFrame({
"姓名": ["张三", "李四"],
"年龄": [25, 30]
})
# 把两列打包成结构体
result = df.select(
pl.struct(["姓名", "年龄"]).alias("人物信息")
)
print(result)
输出:
shape: (2,)
┌────────────────────────┐
│ 人物信息 │
│ struct[2] │
╞════════════════════════╡
│ {("张三", 25)} │
│ {("李四", 30)} │
╞════════════════════════╡
03 | when().then().otherwise():条件逻辑
这是Polars表达式里最强大的功能之一,类似于SQL的CASE WHEN。
语法:
when(条件1).then(值1)
.when(条件2).then(值2)
...
.otherwise(默认值)
3.1 基础用法:单条件
df = pl.DataFrame({
"姓名": ["张三", "李四", "王五", "赵六"],
"年龄": [25, 30, 18, 45]
})
# 年龄分段:大于等于30是"中年",否则是"青年"
result = df.select(
"姓名",
"年龄",
pl.when(pl.col("年龄") >= 30)
.then("中年")
.otherwise("青年")
.alias("年龄段")
)
print(result)
输出:
┌──────┬──────┬────────┐
│ 姓名 ┆ 年龄 ┆ 年龄段 │
│ str ┆ i64 ┆ str │
╞══════╪══════╪════════╡
│ 张三 ┆ 25 ┆ 青年 │
│ 李四 ┆ 30 ┆ 中年 │
│ 王五 ┆ 18 ┆ 青年 │
│ 赵六 ┆ 45 ┆ 中年 │
└──────┴──────┴────────┘
3.2 多条件:链式when
# 年龄分段:18以下=未成年,18-30=青年,30-60=中年,60+=老年
result = df.select(
"姓名",
"年龄",
pl.when(pl.col("年龄") < 18)
.then("未成年")
.when(pl.col("年龄") < 30)
.then("青年")
.when(pl.col("年龄") < 60)
.then("中年")
.otherwise("老年")
.alias("年龄段")
)
print(result)
输出:
┌──────┬──────┬────────┐
│ 姓名 ┆ 年龄 ┆ 年龄段 │
│ str ┆ i64 ┆ str │
╞══════╪══════╪════════╡
│ 张三 ┆ 25 ┆ 青年 │
│ 李四 ┆ 30 ┆ 青年 │
│ 王五 ┆ 18 ┆ 青年 │
│ 赵六 ┆ 45 ┆ 中年 │
└──────┴──────┴────────┘
注意:Polars的when条件是按顺序匹配的,第一个命中的就返回,不会继续往下走。
3.3 条件计算
when-then-otherwise不仅可以返回字符串,还可以返回计算结果:
# 根据年龄段计算不同的增长率:青年+10%,中年+5%,其他不变
result = df.select(
"姓名",
"年龄",
pl.when(pl.col("年龄") < 30)
.then(pl.col("年龄") * 1.1) # 青年+10%
.when(pl.col("年龄") < 60)
.then(pl.col("年龄") * 1.05) # 中年+5%
.otherwise(pl.col("年龄"))
.alias("调整后年龄")
)
print(result)
输出:
┌──────┬──────┬────────────┐
│ 姓名 ┆ 年龄 ┆ 调整后年龄 │
│ str ┆ i64 ┆ f64 │
╞══════╪══════╪════════════╡
│ 张三 ┆ 25 ┆ 27.5 │
│ 李四 ┆ 30 ┆ 31.5 │
│ 王五 ┆ 18 ┆ 19.8 │
│ 赵六 ┆ 45 ┆ 47.25 │
└──────┴──────┴────────────┘
04 | 聚合表达式:sum()、mean()、count()...
表达式和group_by配合,才是真正的威力所在。
4.1 基础聚合
sales_df = pl.DataFrame({
"月份": ["1月", "1月", "2月", "2月", "3月", "3月"],
"产品": ["A", "B", "A", "B", "A", "B"],
"销售额": [100, 200, 150, 250, 180, 220],
"成本": [60, 120, 90, 150, 100, 130]
})
# 按月份分组,计算各种统计指标
result = (
sales_df
.group_by("月份")
.agg(
pl.col("销售额").sum().alias("总销售额"),
pl.col("销售额").mean().alias("平均销售额"),
pl.col("成本").mean().alias("平均成本"),
pl.col("销售额").count().alias("订单数"),
pl.col("销售额").min().alias("最低销售额"),
pl.col("销售额").max().alias("最高销售额"),
)
)
print(result)
输出:
┌──────┬──────────┬─────────────┬────────────┬────────┬────────────┬────────────┐
│ 月份 ┆ 总销售额 ┆ 平均销售额 ┆ 平均成本 ┆ 订单数 ┆ 最低销售额 ┆ 最高销售额 │
│ str ┆ i64 ┆ f64 ┆ f64 ┆ u32 ┆ i64 ┆ i64 │
╞══════╪═══════════╪═════════════╪════════════╪════════╪════════════╪════════════╡
│ 1月 ┆ 300 ┆ 150.0 ┆ 90.0 ┆ 2 ┆ 100 ┆ 200 │
│ 2月 ┆ 400 ┆ 200.0 ┆ 120.0 ┆ 2 ┆ 150 ┆ 250 │
│ 3月 ┆ 400 ┆ 200.0 ┆ 115.0 ┆ 2 ┆ 180 ┆ 220 │
└──────┴──────────┴─────────────┴────────────┴────────┴────────────┴────────────┘
4.2 组合表达式:一列多算
一个 agg() 里可以对同一列做多种计算:
result = (
sales_df
.group_by("月份")
.agg(
pl.col("销售额").sum().alias("总销售额"),
pl.col("销售额").mean().round(2).alias("平均销售额"),
pl.col("成本").sum().alias("总成本"),
(pl.col("销售额").sum() - pl.col("成本").sum()).alias("总利润"),
((pl.col("销售额").sum() - pl.col("成本").sum()) / pl.col("成本").sum() * 100).round(2).alias("利润率%"),
)
)
print(result)
输出:
┌──────┬──────────┬────────────┬────────┬────────┬──────────┐
│ 月份 ┆ 总销售额 ┆ 平均销售额 ┆ 总成本 ┆ 总利润 ┆ 利润率% │
│ str ┆ i64 ┆ f64 ┆ i64 ┆ i64 ┆ f64 │
╞══════╪═══════════╪════════════╪════════╪════════╪══════════╡
│ 1月 ┆ 300 ┆ 150.0 ┆ 180 ┆ 120 ┆ 66.67 │
│ 2月 ┆ 400 ┆ 200.0 ┆ 240 ┆ 160 ┆ 66.67 │
│ 3月 ┆ 400 ┆ 200.0 ┆ 230 ┆ 170 ┆ 73.91 │
└──────┴──────────┴────────────┴────────┴────────┴──────────┘
05 | 窗口函数:over() 的魔法
这是Polars表达式最神奇的地方。
窗口函数 = 在不减少行数的情况下,对每行数据进行分组聚合。
用人话说就是:计算每行的时候"偷偷"用了分组,但结果还是一行一条。
5.1 基础窗口函数
df = pl.DataFrame({
"姓名": ["张三", "李四", "王五", "赵六", "钱七"],
"部门": ["销售", "销售", "技术", "技术", "销售"],
"月薪": [8000, 12000, 15000, 9000, 10000]
})
# 计算每个员工的月薪在其部门的排名
result = df.select(
"姓名",
"部门",
"月薪",
pl.col("月薪")
.rank(method="dense", descending=True)
.over("部门")
.alias("部门内排名")
)
print(result)
输出:
┌──────┬──────┬──────┬──────────┐
│ 姓名 ┆ 部门 ┆ 月薪 ┆ 部门内排名 │
│ str ┆ str ┆ i64 ┆ u32 │
╞══════╪══════╪═══════╪══════════╡
│ 张三 ┆ 销售 ┆ 8000 ┆ 3 │
│ 李四 ┆ 销售 ┆ 12000 ┆ 1 │
│ 王五 ┆ 技术 ┆ 15000 ┆ 1 │
│ 赵六 ┆ 技术 ┆ 9000 ┆ 2 │
│ 钱七 ┆ 销售 ┆ 10000 ┆ 2 │
╞══════╴══════╴═══════╴══════════╡
注意看:行数没变! 原来5行还是5行,但每行多了一个"部门内排名"列。这就是窗口函数厉害的地方——分组聚合但不影响原数据的行数。
5.2 窗口函数计算部门平均
# 计算每个员工的月薪与部门平均的差
result = df.select(
"姓名",
"部门",
"月薪",
pl.col("月薪").mean().over("部门").alias("部门平均月薪"),
(pl.col("月薪") - pl.col("月薪").mean().over("部门")).alias("与部门平均差")
)
print(result)
输出:
┌──────┬──────┬──────┬────────────┬─────────────┐
│ 姓名 ┆ 部门 ┆ 月薪 ┆ 部门平均月薪 ┆ 与部门平均差 │
│ str ┆ str ┆ i64 ┆ f64 ┆ f64 │
╞══════╪══════╪═══════╪════════════╪═════════════╡
│ 张三 ┆ 销售 ┆ 8000 ┆ 10000.0 ┆ -2000.0 │
│ 李四 ┆ 销售 ┆ 12000 ┆ 10000.0 ┆ 2000.0 │
│ 王五 ┆ 技术 ┆ 15000 ┆ 12000.0 ┆ 3000.0 │
│ 赵六 ┆ 技术 ┆ 9000 ┆ 12000.0 ┆ -3000.0 │
│ 钱七 ┆ 销售 ┆ 10000 ┆ 10000.0 ┆ 0.0 │
└──────┴──────┴──────┴────────────┴─────────────┘
销售部平均10000,技术部平均12000,每个员工都能看到自己部门平均值和自己的差距。再也不用写自连接了!
5.3 累计求和
# 累计求和:每天的累计销售额
sales_df = pl.DataFrame({
"日期": ["2024-01-01", "2024-01-02", "2024-01-03", "2024-01-04", "2024-01-05"],
"销售额": [100, 200, 150, 300, 250]
})
result = sales_df.select(
"日期",
"销售额",
pl.col("销售额").cum_sum().alias("累计销售额")
)
print(result)
输出:
┌────────────┬──────┬──────────┐
│ 日期 ┆ 销售额 ┆ 累计销售额 │
│ str ┆ i64 ┆ i64 │
╞════════════╪═══════╪══════════╡
│ 2024-01-01 ┆ 100 ┆ 100 │
│ 2024-01-02 ┆ 200 ┆ 300 │
│ 2024-01-03 ┆ 150 ┆ 450 │
│ 2024-01-04 ┆ 300 ┆ 750 │
│ 2024-01-05 ┆ 250 ┆ 1000 │
└────────────┴──────┴──────────┘
06 | 常用表达式函数速查表
| | |
| pl.col("x").sum() | |
| pl.col("x").mean() | |
| pl.col("x").count() | |
| pl.col("x").max() | |
| pl.col("x").min() | |
| pl.col("x").n_unique() | |
| pl.col("x").rank() | |
| pl.col("x").cum_sum() | |
| pl.col("x").str.to_uppercase() | |
| pl.col("x").str.contains("子串") | |
| pl.col("x").dt.year() | |
| pl.when(...).then(...).otherwise(...) | |
| pl.col("x").mean().over("group") | |
| pl.col("x").fill_null(0) | |
| pl.col("x").cast(pl.Int32) | |
| pl.col("x").abs() | |
| pl.col("x").round(2) | |
07 | 实战:用户留存分析
留存分析是产品运营的灵魂,用Polars表达式来做,简洁到哭:
events_df = pl.DataFrame({
"用户ID": [1, 1, 1, 2, 2, 3, 3, 3, 3],
"日期": [
"2024-01-01", "2024-01-02", "2024-01-10",
"2024-01-01", "2024-01-03",
"2024-01-01", "2024-01-02", "2024-01-03", "2024-01-10"
],
"事件": ["注册", "登录", "登录", "注册", "登录", "注册", "登录", "登录", "登录"]
})
# 找出每个用户的首次活跃日期和统计
user_stats = (
events_df
.group_by("用户ID")
.agg(
pl.col("日期").min().alias("首次活跃"),
pl.col("日期").n_unique().alias("活跃天数"),
pl.col("日期").max().alias("最后活跃"),
pl.col("事件").count().alias("总事件数")
)
)
print(user_stats)
输出:
┌────────┬────────────┬────────┬────────────┬────────────┐
│ 用户ID ┆ 首次活跃 ┆ 活跃天数 ┆ 最后活跃 ┆ 总事件数 │
│ i64 ┆ str ┆ u32 ┆ str ┆ u32 │
╞════════╪════════════╪════════╪════════════╪════════════╡
│ 1 ┆ 2024-01-01 ┆ 3 ┆ 2024-01-10 ┆ 3 │
│ 2 ┆ 2024-01-01 ┆ 2 ┆ 2024-01-03 ┆ 2 │
│ 3 ┆ 2024-01-01 ┆ 4 ┆ 2024-01-10 ┆ 4 │
└────────┴────────────┴────────┴────────────┴────────────┘
08 | 避坑指南
坑1:over() 后面忘了排序列
# ❌ 错误:over()里没指定分组列,结果不对
df.select(
pl.col("月薪").mean().alias("平均月薪")
)
# ✅ 正确:明确指定分组的列
df.select(
pl.col("月薪").mean().over("部门").alias("部门平均月薪")
)
坑2:when-then 忘记写 otherwise
# ❌ 错误:不写otherwise,Polars默认填None
df.select(
pl.when(pl.col("年龄") > 30)
.then("中年")
# 30岁及以下的会变成 null!
)
# ✅ 正确:写全
df.select(
pl.when(pl.col("年龄") > 30)
.then("中年")
.otherwise("非中年")
)
坑3:聚合表达式写在 select 而不是 agg 里
# ❌ 错误:在 select 里写 sum() 是对所有行求和,不是分组
df.select(
pl.col("销售额").sum().alias("总销售额") # 这是全局求和!
)
# ✅ 正确:分组聚合要用 group_by + agg
df.group_by("月份").agg(
pl.col("销售额").sum().alias("总销售额")
)
记住:select 不改变行数,agg 会改变行数。
09 | 练习题 🐟
练习题:
有个用户行为日志:
用Polars表达式完成:
- 3. 用 when-then 给消费分级:>=200=高消费,>=100=中等,<100=低消费
- 4. 用窗口函数计算每个用户的消费金额占总消费的比例
答案下期公布! 先自己动手做,做出来的鱼油评论区见 💪
10 | 总结
今天学了什么:
- 1. 表达式本质:
col() 引用列、lit() 创建常量、struct() 打包结构体 - 2. 条件逻辑:
when().then().otherwise() 实现分支判断 - 3. 聚合操作:在
agg() 里对分组数据做 sum/mean/count/max/min - 4. 窗口函数:
over() 实现分组聚合但不降维,保留原数据行数 - 5. 常用函数:cum_sum、rank、str操作、dt操作等
下期预告: 窗口函数深度实战 —— rank、rolling、lag、lead,数据分析的瑞士军刀 🔥
学完这期你觉得Polars表达式最香的地方是哪里?when-then还是窗口函数?评论区聊聊!