Python Polars 速查表(基于 O'Reilly 官方书籍浓缩)
文章摘要
这是 Posit(原 RStudio)开源资源站上新增的一份 Polars 速查表,作者 Jeroen Janssens 和 Thijs Nieuwdorp 正是 O’Reilly 出版的《Python Polars: The Definitive Guide》的两位作者。他们在 HN 自述里说,花了几周把近 500 页的书「有损压缩」成两页速查表,除 PDF 之外还提供了无障碍的 HTML 版本,全站内容也是开源的(托管在 posit-dev/open-source-website 仓库)。Polars 是 Ritchie Vink 在 2020 年首次发布的库,安装用 uv pip install "polars[all]",pl.show_versions() 可以确认依赖版本。速查表通篇约定:df 是 DataFrame,lf 是 LazyFrame,o 是要与 df 合并的第二个 DataFrame,e 表示任意表达式。
数据结构与惰性 API。 三种结构:Series(一维、同类型序列)、DataFrame(二维、多个等长 Series)、LazyFrame(外形像 DataFrame 但不持有数据,是生成 DataFrame 的蓝图)。与 pandas 不同,Polars 的 DataFrame 没有行索引,API 偏向不可变和方法链,需要索引时用 df.with_row_index("id") 显式加一列。eager API 立即执行,lazy API 先构建优化后的查询计划,优化器会自动做谓词下推(尽早过滤)和投影下推(丢掉从未用到的列)。.lazy() 与 .collect() 在两种表示间往返;lf.collect(engine="streaming") 用流式引擎做核外处理,从而处理比内存更大的数据集;lf.explain() 打印优化后的计划,lf.show_graph() 画成图,lf.profile() 返回每个节点的耗时。
数据类型。 Polars 实现了 Apache Arrow 内存规范的大部分。数值类有 Decimal(128 位,带 precision 和 scale)、Float32/64、Int8 到 Int128、UInt8 到 UInt64;时间类有 Date(Unix epoch 起的天数)、Datetime(微秒)、Duration、Time;嵌套类有 Array(定长)、List(变长)、Struct(带名字的多字段);字符串类有 String(UTF-8 变长)、Categorical(字符串字典)、Enum(固定字符串字典);其他有 Boolean、Binary、Null。检视类型用 df.schema / df.dtypes / df.glimpse()(宽表时每列一行,比直接打印可读)/ df.describe() / df.estimated_size("mb")。转换用 .cast(),默认严格,strict=False 时溢出值变成 null 而不报错。
读写 I/O 的四个家族。 read_*() 读进 DataFrame,scan_*() 建 LazyFrame 并延迟到 collect 时才真正读,write_*() 写到磁盘或云存储,sink_*() 流式写出而不全量驻留内存。不是每种格式都支持全部四种:CSV、Delta Lake、IPC/Feather、NDJSON、Parquet 四种全支持;Iceberg 支持 scan/write/sink 但没有 read;Avro、Clipboard、Database、Excel/ODS、JSON 只有 read 和 write;PyArrow Dataset 只有 scan。常用关键字参数有 schema_overrides、n_rows、row_index_name、storage_options、compression。扫云上文件可以直接给带通配符的 URI 加 storage_options 传凭据和 region;lf.sink_parquet(pl.PartitionBy("out/", key="x")) 可以按键值流式写成分区 Parquet 数据集。
转换数据。 选列:df.select("a","b")、df.select(pl.col("x") * 2)、用关键字参数给结果命名 df.select(doubled=pl.col("x") * 2)、正则选列(模式必须以 ^ 开头 $ 结尾)、pl.all();更灵活的是列选择器 import polars.selectors as cs,cs.numeric()、cs.starts_with("val")、cs.string()、cs.contains()、cs.first(),并且可以用 |、&、-、^、~ 这些集合运算符组合;丢列用 df.drop("a","y", strict=False)。造列:with_columns(new=...) 加到最右侧,同名表达式则替换原列,pl.lit(1) 加常量列,with_row_index(name="id", offset=1)。过滤行:df.filter("valid") 直接传布尔列名,多个表达式默认按 AND 组合,也可显式用 &(每个比较各自加括号)、| 表示 OR,还支持 df.filter(valid=True, x=5) 这种关键字约束简写;drop_nulls()、unique(subset=["x"], keep="first")。切片抽样:head()/tail() 默认 5、slice(2, 5)、gather_every(2)、sample(10, with_replacement=True)、sample(fraction=0.2)。排序:sort 支持多列、nulls_last=True、descending 传布尔列表逐列指定方向、按表达式排序,以及比「全排再切」更省的 top_k(5, by="score") / bottom_k。变形:unpivot(宽变长)、pivot(长变宽,若 on 与 index 组合不唯一需给 aggregate_function)、explode(列表列展开成行)、unnest(struct 字段展开成列)、transpose(include_header=True)、partition_by("group")。
汇总聚合。 「Split. Apply. Combine.」——group_by 得到 GroupBy 对象后可用现成汇总 len() / head(2) / mean(),自定义用 map_groups(...),完全控制用 agg(...)(不带聚合方法的表达式会把值收集成列表,关键字参数命名结果);窗口表达式 over() 可以把聚合作为新列加回原 DataFrame 而不折叠行数;时间维度上有 group_by_dynamic("timestamp", every="1h", group_by="store") 做定长窗口、rolling(index_column="date", period="7d", group_by="store") 做逐行滑动窗口、upsample(...) 补齐规整时间序列缺失的行;横向聚合有 pl.sum_horizontal(cs.numeric())、pl.any_horizontal(cs.boolean())。
连接与拼接。 df.join(o, on="key") 默认 inner,how 可选 left、full(配 coalesce=True 合并键列)、semi(保留有匹配的 df 行)、anti(保留没匹配的)、cross(笛卡尔积,无需键);键名不同用 left_on/right_on;join_asof(o, on="ts", by="i") 做最近邻匹配,是对齐两条时间序列的常规做法;join_where(o, pl.col("a") >= pl.col("b")) 支持不等值/任意谓词连接;validate 接受 "m:m"、"m:1"、"1:m"、"1:1"。拼接用 pl.concat,how 可为默认的垂直、horizontal、diagonal(取列的并集、空缺填 null)、vertical_relaxed(放宽类型强转而不报错);df.update(o, on="id", how="left") 用另一个 DataFrame 的非空值更新。
表达式系统。 速查表给了一个「表达式」的正式定义:表达式是一棵描述如何构造一个或多个 Series 的操作树——Series 是同类型数组(可以是列也可以独立存在),操作树可以是单个、线性或有分支的,「描述」意味着它是被动的配方、需要函数来执行,「构造」意味着输出可能是内部的而非新列,「一个或多个」意味着单个表达式可以产出多个 Series。起点只有三种:某列(pl.col("name"))、所有列(pl.col("*") 等价于 pl.all())、字面量(pl.lit("ok")),另有 pl.arange(0, 5)(stop 不含)和 pl.date_range / pl.date_ranges(单数产生一个范围,复数产生每行一个范围的列)。算术、比较、布尔运算每个运算符都有等价方法(add/sub/mul/truediv/floordiv/pow/mod/dot;lt/le/eq/ge/gt/ne;and_/or_/not_/xor,因为 and/or/not 是 Python 关键字所以带下划线),方便保持方法链不断。注意 Python 里的链式比较不可用,必须写成 (pl.col("x") > 0) & (pl.col("x") < 10)。条件表达式用 when().then()...otherwise(),条件按顺序求值、先匹配者胜,所以最具体的条件要放前面。
表达式方法按命名空间归类。 数学与取整:abs/sign/exp/cbrt/sqrt/log/log10/log1p/三角与双曲及其反函数/degrees/radians/ceil/floor/round/clip/cut/qcut。缺失值——Polars 明确区分 null(缺失)与 NaN(0/0 这类未定义运算得到的浮点数),两者由不同方法处理:fill_nan/fill_null/is_finite/is_infinite/is_nan/is_null/drop_nans/drop_nulls/flatten/reshape/explode/implode。位移、累计与滑动:backward_fill/forward_fill/interpolate/shift/cum_count/cum_sum/cum_max/cum_min/diff/pct_change/ewm_mean/ewm_std/ewm_var/rolling_max/rolling_min/rolling_mean/rolling_median/rolling_std/rolling_var/rolling_map。排序排名:sort/sort_by/arg_sort/shuffle/reverse/rank/is_duplicated/is_unique/is_first_distinct/is_last_distinct。统计:max/min/mean/nan_max/nan_min/median/std/var/entropy/kurtosis/skew/product/quantile/sum/arg_max/arg_min/first/last/get/mode。计数与唯一值这里有个容易踩的区别:len() 计所有行含 null,count() 只计非空值,null_count() 计 null;另有 n_unique/approx_n_unique/arg_unique/unique/unique_counts/value_counts/head/tail/limit/bottom_k/top_k/gather/gather_every/sample/slice/arg_true/replace/search_sorted。类型专属命名空间:arr(定长数组)与 list(变长,list.len/get/sort/join("-")/contains)、cat(Categorical 从数据推断类别并按字典序排,Enum 预先固定并按声明顺序排)、dt(month/replace/strftime/convert_time_zone("UTC")/total_seconds)、str(UTF-8,长度和切片按字符不按字节:contains/split/to_uppercase/to_datetime/extract/strip_chars)、struct(field/rename_fields/with_fields)、bin(base64_decode/hex_encode)、name(prefix/to_lowercase 控制输出列名)、meta(写插件时用的内省:output_name/is_regex/has_multiple_outputs)。
呈现与可视化。 排版用 Great Tables:GT(df) 起手,链 tab_stub/cols_label/tab_header/fmt_number/fmt_nanoplot/data_color。绘图内置方法基于 Altair,走 plot 命名空间(df.plot.scatter(x=..., y=..., color=...));Plotnine、Plotly、hvPlot、Seaborn、Matplotlib 都能直接吃 Polars DataFrame,实在不行先 df.to_pandas()。最后一节是 Polars Cloud:用 pc.ComputeContext(workspace=..., cpus=4, memory=16, cluster_size=32) 描述算力,然后 lf.remote(ctx).execute().await_result() 把 LazyFrame 跑在自己环境里的集群上。
HN 评论精华
这条帖子 197 分、41 条评论。讨论几乎没停留在速查表本身,而是迅速跑成了三场语言/工具之争:R 的 tidyverse 与 data.table 对 pandas/Polars 的人机工程学优势、「不如直接用 DuckDB 写 SQL」派的反攻,以及对 pl.col("...") 这十个字符仪式感的集体吐槽(并给出了几种真正好用的缩写法)。
- jeroenjanssens(作者本人)开帖说明:把近 500 页的书压成两页是「高度有损的压缩,但希望有用」,欢迎大家指出漏掉的常用操作或对分类方式的意见。有人问有没有 Markdown 版「用于某些原因」,作者回答 opensource.posit.co 整站开源,可以直接去仓库拿。
- clircle 承认数据科学世界已经转向 Python,但一直觉得 R 的 data.table 有最顺手的 DataFrame 开发体验。wsowens 说尽管自己大部分过程式代码写 Python,数据分析仍偏爱 R,dplyr + ggplot + tidyverse 的人机工程学很难被超越,用 pandas 和 matplotlib/seaborn 的几次尝试都很挫败——但从这份速查表看,Polars 似乎解决了 pandas 的一些摩擦,值得一试。220hertz 回应说摩擦更少、明显更快,他有位统计学家朋友最近就从 R 跳过来了。
- holub008 给出了最技术性的判断:R 相对其他语言/库的根本优势是「表达式」——能直接引用列名、同时又能和 base R 的向量化操作互通,这种能力「不公平」;当然这个超能力对初学者同样迷惑、在生产代码里也容易出事。latent-person 给了 tidyverse 的示例来展示这种简洁:管道接
group_by(country)、filter(amount <= median(amount) * 10)、summarize(total = sum(amount - discount)),不懂 R 也能读懂。 - qsort 提出反方论点:如果工作偏统计和纯建模,R 完胜;但多数项目都有「不干净」的部分——从多个来源采集数据、用各种服务的连接器、S3 桶之类,处理这堆烂摊子是 Python 的强项,他宁愿用 Python 维护复杂数据管线。epihelix 补充说数据科学是个大教堂,他自己的分子生物学/基因组学/表观遗传学/生物信息学和伴侣的生态学领域仍然深度扎根 R。dec0dedab0de 回忆 pandas 刚出来时(Wes McKinney 还来他们的 Python 用户组讲过)社区共识就是「数据到手之后 R 更好,其他一切 Python 更好」,当年有人做过在 Python 里开 R 并共享内存的实验项目;现在 cheesecakegood 指出
ryp库已经做到这件事,数据以 Arrow 格式存活因此不用落盘,dajt 则说自己用 rpy2,科学家用 R 写计算引擎,他写 Python 的仪表盘和数据管线,批处理里嵌入 R 解释器以免反复起进程。 - 「不如用 DuckDB」这条线由 mrtimo 挑起:他已经从 python/polars/pandas 转到 DuckDB 且没有回头。被问「连内存里的快速分析也用?」时,krapht 给了一句很有传播力的对比:「用 duckdb 起个 venv:5 分钟;到处都用 SQL、永远不用记 DataFrame 语法:无价。」viccis 的理由更实际:他每次在 Python 里用 Polars 的流式接口,它仍然会把一切物化到内存,那是大约半年前的事,「流式接口是需要大量手把手照看的漏抽象」,比如当时还不能从 S3 做 NDJSON 流式扫描(看起来已被 PR #26563 修掉)。nojito 则站在反面,直接甩出两个链接对比——SQL 是糟糕的数据转换 API,DuckDB 的 PIVOT 有一堆限制,而 Polars 的对应 API 没有。rtpg 追问 SQL 路线的可视化怎么办,因为他觉得 pandas 里事后分组再画图相当舒服。
- 吐槽
pl.col("...")的这一支最有实用价值。paulfharrison 说他就是过不去「每次引用一列都要十个字符的仪式」这道坎。thijsn(另一位作者)透露有人正是因为这个而改用import polars.col as c,然后写c("colname");mmplxx 和 laGrenouille 补充还能更短,直接c.colname,再配上在with_columns/agg里用var=代替 assign,laGrenouille 说这两点「彻底改变了我对 polars 的看法」,过去一年一直拿它当主力。vovavili 的写法是from polars import col, lit。mmplxx 给的极简版是c = pl.col然后c.foo + c.bar,paulfharrison 回应这已经接近 R 的简洁度而没有 R 的黑魔法,并建议把它加进速查表。 - ForceBru 为这十个字符做了最好的辩护:正因为把列包成对象,才能把列当成值来做数学运算,这非常直观;他用了很久 pandas 一直不太顺,试了 Polars 之后「能做以前想都不敢想的事,而且还快」。他把
pl.col理解为延迟求值:先按名字引用、构建想算的表达式,交给 Polars 之后它再去取实际值执行;理想情况下最好能直接对字符串做数学("Amount" * "Price" - "Losses"),但编程语言要么不允许要么会解释成字符串拼接,所以只能包一层。他顺带感叹 Julia 基本没有 Polars 的对等物,DataFrames.jl 他一直学不进去;latent-person 推荐去看 dplyr 的mutate(profit = Amount * Price - Losses)有多直观,以及 Julia 侧用宏提供 tidy 语法的 TidierData.jl。 - 两条一句话神评:brikym 的
echo "Never use pandas, use polars instead" >> AGENTS.md,被 kirubakaran 接了一句「除非你想让这行成为 AGENTS.md 里唯一的内容」;rtpg 最后感慨 pandas 和 Polars 都让他非常想要 Python 有个宏系统,pl.col(...)是个巧妙的技巧,但 pandas 的df[df["foo"] == "bar"]一直很拗口(尤其当你敢给 DataFrame 起个长名字时),他觉得总有一天会有人做出一个「非常」好用的 API。