今天不聊虚的,整点硬货。
有个东西叫 Polars,你们听说过没?这两年在数据圈火得一塌糊涂。简单说就一句话:比 pandas 快 5 到 50 倍。
别急着说“我不信”,往下看。
01 | 先给你们看个真实的测试结果
我自己拿 100 万行的销售数据跑过分组聚合:
pandas: 4.23秒
Polars: 0.31秒
差了 13 倍。
就是同一个需求,同一台电脑,同样的代码逻辑,就因为换了个库,快了这么多。
pandas 处理百万级数据还在喘气的时候,Polars 已经叼着烟喝着茶把活干完了 😏
为什么这么快?
Polars 是 Rust 写的,pandas 是 C 写的。
Rust 那家伙,天生支持多线程,内存管理又精细,执行效率比 C 高出一大截。这不是吹的,是血统优势。
02 | 先看代码对比,心里有个数
假设你有个 CSV 文件,100 万行销售数据。
Pandas 版本:
import pandas as pd
df = pd.read_csv('sales.csv')
df['date'] = pd.to_datetime(df['date'])
df['month'] = df['date'].dt.to_period('M')
result = df.groupby('month')['amount'].sum()
print(result)
Polars 版本:
import polars as pl
df = pl.read_csv('sales.csv')
result = (
df
.with_columns(pl.col('date').str.to_date())
.with_columns(pl.col('date').dt.month().alias('month'))
.group_by('month')
.agg(pl.col('amount').sum())
)
print(result)
看出区别没?
pandas 要分好几步赋值,中间还产生临时列。Polars 一个括号链下来,干净利落,管道式操作,读起来跟说话一样。
03 | 安装?3 秒钟搞定
pip install polars
对,就这么一行。
如果你是 conda 用户:
conda install polars -c conda-forge
装完验证一下:
import polars as pl
print(pl.__version__)
# 输出:1.22.0(举例)
没报错就说明装好了,可以开始浪了。
04 | 两个核心概念:Series 和 DataFrame
Polars 的数据结构跟 pandas 差不多,学过 pandas 的鱼油可以直接上手。
4.1 Series:就是一维数组
import polars as pl
s = pl.Series("姓名", ["张三", "李四", "王五"])
print(s)
输出:
shape: (3,)
Series: '姓名' [str]
[
"张三"
"李四"
"王五"
]
就是一组数据,类似于 Excel 里的一列。
4.2 DataFrame:就是表格
import polars as pl
df = pl.DataFrame({
"姓名": ["张三", "李四", "王五"],
"年龄": [25, 30, 35],
"城市": ["北京", "上海", "深圳"]
})
print(df)
输出:
shape: (3, 3)
┌──────┬──────┬──────┐
│ 姓名 ┆ 年龄 ┆ 城市 │
│ str ┆ i64 ┆ str │
╞══════╪══════╪══════╡
│ 张三 ┆ 25 ┆ 北京 │
│ 李四 ┆ 30 ┆ 上海 │
│ 王五 ┆ 35 ┆ 深圳 │
└──────┴──────┴──────┘
这个表格打印出来比 pandas 好看太多了,我第一次用的时候差点感动哭 😭
05 | 读取数据:这些方法要记住
5.1 读 CSV(最常用)
# 最基本的
df = pl.read_csv("data.csv")
# 指定分隔符
df = pl.read_csv("data.csv", separator=";")
# 跳过前几行
df = pl.read_csv("data.csv", skip_rows=2)
# 只读取需要的列
df = pl.read_csv("data.csv", columns=["name", "age", "city"])
# 自动解析日期
df = pl.read_csv("data.csv", try_parse_dates=True)
5.2 读 Parquet(比 CSV 快很多)
Parquet 是一种列式存储格式,大数据场景下比 CSV 快几倍到几十倍:
df = pl.read_parquet("data.parquet")
5.3 读 JSON
df = pl.read_json("data.json")
5.4 懒加载模式(大文件必杀技)
这是 Polars 的杀手锏功能!
# scan_csv 不会立即执行,只是"规划"操作
lf = pl.scan_csv("big_data.csv")
# 查看执行计划(不会真正跑)
print(lf.explain())
# 调用 collect 才真正执行
df = lf.collect()
scan_csv 会自动做 谓词下推(predicate pushdown),意思是大文件它只读取你真正需要的数据,不会傻乎乎全读进内存。
数据量上了千万行的鱼油们,这一点能救你一命。
06 | 基础查询:filter、select、with_columns
6.1 看数据
df.head() # 看前5行
df.tail() # 看后5行
df.glimpse() # 快速瞥一眼
df.describe() # 统计摘要
6.2 筛选行:filter
# 筛选年龄大于25的
df.filter(pl.col("年龄") > 25)
# AND条件:年龄大于25 且 城市是北京
df.filter((pl.col("年龄") > 25) & (pl.col("城市") == "北京"))
# OR条件
df.filter((pl.col("年龄") < 30) | (pl.col("城市") == "深圳"))
注意:& 和 | 要加括号,否则会报错。Python 的运算符优先级会坑你。
6.3 选择列:select
# 选择单列
df.select("姓名")
# 选择多列
df.select(["姓名", "城市"])
# 用表达式选(更灵活)
df.select(pl.col("姓名"), pl.col("年龄") * 2)
6.4 新增/修改列:with_columns
# 新增一列:年龄+1
df.with_columns((pl.col("年龄") + 1).alias("年龄+1"))
# 修改列:姓名全变大写
df.with_columns(pl.col("姓名").str.to_uppercase().alias("姓名大写"))
# 一次做多个变换
df.with_columns(
pl.col("年龄").alias("原年龄"),
(pl.col("年龄") + 10).alias("年龄加10"),
pl.col("城市").str.lengths().alias("城市字符数")
)
07 | 排序:sort
# 按年龄升序
df.sort("年龄")
# 按年龄降序
df.sort("年龄", descending=True)
# 多字段排序:先按城市,再按年龄
df.sort(["城市", "年龄"], descending=[False, True])
08 | 分组聚合:group_by + agg(超级重要)
这个是数据分析的灵魂操作,必须拿下。
# 模拟销售数据
sales_df = pl.DataFrame({
"月份": ["1月", "1月", "2月", "2月", "3月"],
"产品": ["A", "B", "A", "B", "A"],
"销售额": [100, 200, 150, 250, 180]
})
# 按月份分组,求总销售额
result = (
sales_df
.group_by("月份")
.agg(pl.col("销售额").sum().alias("总销售额"))
)
print(result)
输出:
shape: (3, 2)
┌──────┬──────────┐
│ 月份 ┆ 总销售额 │
│ str ┆ i64 │
╞══════╪══════════╡
│ 1月 ┆ 300 │
│ 2月 ┆ 400 │
│ 3月 ┆ 180 │
╞══════╧══════════┡
分组聚合就两句话:group_by 分组,agg 算指标。记住了吧?🐟
09 | 关联表:join
类似 SQL 里的 JOIN,把两张表拼在一起:
# 客户表
customers = pl.DataFrame({
"客户ID": [1, 2, 3],
"客户名": ["张三", "李四", "王五"]
})
# 订单表
orders = pl.DataFrame({
"客户ID": [1, 2, 2],
"订单金额": [100, 200, 300]
})
# 左连接
result = customers.join(orders, on="客户ID", how="left")
print(result)
输出:
shape: (3, 3)
┌────────┬────────┬──────────┐
│ 客户ID ┆ 客户名 ┆ 订单金额 │
│ i64 ┆ str ┆ i64 │
╞════════╪════════╪══════════╡
│ 1 ┆ 张三 ┆ 100 │
│ 2 ┆ 李四 ┆ 200 │
│ 2 ┆ 李四 ┆ 300 │
└────────┴────────┴──────────┘
Polars 的 join 和 SQL 的逻辑一模一样,左连接就是以左边为准,右边没有的填 NULL。
10 | 管道运算符:代码读起来像说话
Polars 支持 |> 管道运算符,代码可读性直接起飞:
# 不用管道:嵌套地狱,眼睛要瞎了
result = sort(with_columns(filter(df, pl.col("年龄") > 25), pl.col("年龄") * 2), "年龄")
# 用管道:跟读句子一样,从左到右
result = (
df
.filter(pl.col("年龄") > 25)
.with_columns((pl.col("年龄") * 2).alias("年龄翻倍"))
.sort("年龄")
)
哪个好读?不用说大家都知道。
11 | 避坑指南
坑 1:别来回转换 pandas 和 Polars
# ❌ 不好:反复横跳,效率没了
df_pandas = df.to_pandas()
df_polars = pl.from_pandas(df_pandas)
# ✅ 好:在Polars里一气呵成
df.filter(...).with_columns(...).sort(...)
坑 2:字符串操作要用 .str
# ✅ 正确
df.with_columns(pl.col("name").str.to_uppercase())
# ❌ 错误
df.with_columns(pl.col("name").to_uppercase())
Polars 的字符串操作要加 .str,否则它不认识你是想搞字符串操作还是列操作。
坑 3:大文件用 LazyFrame
# 小文件:直接读
df = pl.read_csv("small.csv")
# 大文件:scan + collect
df = pl.scan_csv("big.csv").filter(pl.col("age") > 20).collect()
12 | 实战练习 🐟
练习题:
有个销售记录 CSV(sales.csv),包含这些字段:
date:日期
product:产品名
category:类别
amount:销售额
quantity:销售数量
用 Polars 完成:
- 读取 CSV
- 筛选
category == "电子产品" 的记录
- 按产品分组,计算总销售额和总销售数量
- 按总销售额降序排序
- 保留销售额前 3 的产品
答案下期公布! 先自己动手做,做出来的鱼油评论区见 💪
13 | 总结
今天学了什么:
- 为什么快:Rust 血统,多线程,列式存储
- 两种数据结构:Series(一维)、DataFrame(二维表格)
- 读取数据:read_csv、read_parquet、scan_csv(懒加载)
- 基础操作:filter、select、with_columns、sort
- 分组聚合:group_by + agg
- 关联表:join
- 管道运算符:让代码优雅
下期预告: 表达式引擎,像写 SQL 一样写 Python —— 敬请期待!
你们平时用什么处理数据?pandas 还是 Polars?或者 Excel?在云栈社区聊聊你的数据处理利器吧!