1. 从“选择”到“转换”:Polars数据处理的核心两步
如果你从Pandas转向Polars,或者刚开始用Polars处理数据,最先要搞明白的不是它的语法有多快,而是怎么把数据“拿过来”和“变过去”。这说的就是行列选择和数据转换。很多新手卡住的地方,往往是以为Polars和Pandas用法一样,结果在选列、过滤行或者转换类型时报错。
Polars的核心优势在于惰性执行和并行计算,但前提是你的操作得在它的“语法体系”里。行列选择是定位数据的“坐标”,而转换则是改变数据的“形态”,比如把字符串日期变成时间戳、把分类文本转成数值。这两步做对了,后续的分析、聚合、输出才能顺畅。
我建议先别急着处理复杂逻辑,从最基础的select和with_columns开始,把单张表玩熟。你会发现,很多在Pandas里需要绕一下的操作,在Polars里有更直接、更高效的做法。这篇文章就围绕这两个核心动作,拆解具体的用法、常见的坑,以及怎么组合起来解决实际问题。
2. 行列选择:用对方法,效率翻倍
行列选择是数据操作的起点。Polars提供了多种方式,但每种都有其适用的场景和性能考量。用错了方法,在数据量大的时候,速度差异会非常明显。
2.1 列选择:select是主力,[]是快捷方式
选择列最常用的方法是select。它接受列名、表达式,或者一个列名列表。
import polars as pl # 假设我们有一个DataFrame `df` df = pl.DataFrame({ “id”: [1, 2, 3], “name”: [“Alice”, “Bob”, “Charlie”], “value”: [100.5, 200.3, 150.0], “category”: [“A”, “B”, “A”] }) # 1. 选择单列或多列 df_selected = df.select(“name”, “value”) # 选择 name 和 value 列 df_selected = df.select([“name”, “value”]) # 用列表形式也一样 # 2. 使用 pl.col 对象进行更灵活的选择 df_selected = df.select(pl.col(“name”), pl.col(“value”) * 2) # 选择name列,并计算value*2作为一个新列pl.col是构建表达式的核心,它允许你在选择的同时进行计算。这是Polars非常强大的地方,你不需要先选择列,再赋值,可以一步到位。
除了select,你也可以用类似Pandas的[]索引,但这通常只用于非常简单的列名选取,且返回的是一个Series(单列)或新的DataFrame(多列)。对于复杂的表达式,[]就无能为力了。
# 使用 [] 选择单列(返回 Series) name_series = df[“name”] # 使用 [] 选择多列(返回 DataFrame) sub_df = df[[“name”, “value”]]我的经验是:只要是涉及列的操作,无论是单纯选择还是附带计算,优先使用select配合pl.col。[]只在你临时需要查看某一列,或者进行非常简单的切片时用。因为select能更好地融入Polars的表达式API和惰性执行框架。
2.2 行选择:filter是核心,切片要小心
选择行主要靠filter方法,它接受一个布尔表达式。
# 选择 value 大于 150 的行 df_filtered = df.filter(pl.col(“value”) > 150) # 组合条件选择 df_filtered = df.filter((pl.col(“value”) > 150) & (pl.col(“category”) == “A”))这里的关键是,条件表达式必须用pl.col来构建。直接写df[“value”] > 150在某些简单情况下可能行得通,但在复杂查询或惰性模式下容易出错,不推荐。
对于按位置选择行(切片),Polars提供了slice方法。但要注意,在惰性LazyFrame上直接按整数位置切片是低效的,因为它通常需要先计算或排序。在DataFrame上可以,但要明白其含义。
# 选择前2行 df_head = df.slice(0, 2) # 跳过前1行,取之后的所有行 df_tail = df.slice(1, -1) # -1 表示剩余全部一个重要的避坑点:不要试图用df[0:2]这样的Python列表切片语法来选择行,这在Polars中通常不是你想要的结果,或者会直接报错。行选择,逻辑条件用filter,位置切片用slice,界限要清晰。
2.3 组合选择:行列同时筛选
实际工作中,我们经常需要同时筛选行和列。Polars鼓励链式调用,顺序很重要。
# 先筛选行,再选择列(更高效) result = df.filter(pl.col(“category”) == “A”).select(“id”, “name”, “value”) # 也可以先选择列,再筛选行。但在惰性执行下,优化器会尝试重排操作,最终效率可能差不多。 # 对于人类阅读来说,“先过滤行,减少数据量,再选列”的逻辑更直观。在惰性执行(LazyFrame)中,Polars的查询优化器会自动对操作进行重新排序(如谓词下推、投影下推)以获得最佳性能。所以你可以按照最自然的逻辑去写,Polars会在后台帮你优化。
3. 数据转换:用with_columns和cast改变数据形态
选出了需要的数据,下一步就是转换。转换的目的通常有两个:1) 改变数据类型;2) 基于现有列计算新列。
3.1 类型转换:cast方法
这是最直接的“转换”。比如把字符串转成整数、浮点数转成整数、或者转成日期类型。
# 假设 value 列是字符串,我们想转成浮点数 df_converted = df.with_columns(pl.col(“value”).cast(pl.Float64)) # 一次转换多列类型 df_converted = df.with_columns( pl.col(“id”).cast(pl.Int32), pl.col(“value”).cast(pl.Int64), # 浮点转整数会截断小数 ) # 更安全的转换:使用 `strict=False` 避免因转换失败而报错,无法转换的会变成 null df_safe = df.with_columns(pl.col(“some_column”).cast(pl.Int64, strict=False))类型转换的常见坑:
- 精度丢失:
Float64转Int64会直接去掉小数部分,不是四舍五入。需要四舍五入的话,要先round再cast。 - 转换失败:比如把
“hello”转成整数会失败。在批处理中,一个失败可能导致整个任务停止。务必使用strict=False或提前清洗数据。 - 日期时间:字符串转日期时间要用
pl.col(“date_str”).str.strptime(pl.Date, “%Y-%m-%d”),而不是简单的cast。cast通常用于同大类下的类型转换(如不同整数之间)。
3.2 计算新列与列变换:with_columns的舞台
with_columns是Polars中添加新列或替换现有列的主要方法。它可以在保留原有列的同时,添加由表达式计算出的新列。
# 1. 添加新列 df_new = df.with_columns( (pl.col(“value”) * 1.1).alias(“value_with_tax”), # 计算含税价,新列名为 value_with_tax pl.lit(“processed”).alias(“status”) # 添加一个常量列 ) # 2. 替换(覆盖)现有列 df_replaced = df.with_columns( (pl.col(“value”) * 2).alias(“value”) # 列名与现有列相同,则覆盖 ) # 3. 复杂的表达式转换 df_complex = df.with_columns( pl.when(pl.col(“value”) > 150) .then(pl.lit(“High”)) .otherwise(pl.lit(“Low”)) .alias(“value_level”), # 条件赋值,类似 SQL CASE WHEN pl.col(“name”).str.to_uppercase().alias(“name_upper”) # 字符串操作 )with_columns的核心优势:
- 并行计算:每个表达式独立计算,Polars会自动并行化。
- 表达式API:可以与
pl.col、字符串函数、时间函数等无缝结合。 - 惰性友好:在
LazyFrame上使用,可以极大优化整个查询计划。
与Pandas的对比:在Pandas里,你可能会写df[‘new_col’] = df[‘old_col’] * 2。在Polars里,对应的就是df.with_columns((pl.col(‘old_col’) * 2).alias(‘new_col’))。虽然写法不同,但Polars的方式更符合其函数式、表达式的设计哲学,也更容易被优化。
3.3 使用select进行“转换式选择”
有时候,转换的目的就是为了选出新的结果集,这时可以直接用select。
# 选择原有列的同时,直接计算并选择新列 result = df.select( “id”, “name”, (pl.col(“value”) * 1.1).alias(“value_taxed”), pl.col(“category”).cast(pl.Categorical).alias(“category_cat”) )这相当于先with_columns添加新列,再select选择它们。对于一次性转换并输出的场景,直接用select更简洁。
4. 实战串联:一个完整的数据清洗与转换流程
现在我们把行列选择和转换组合起来,看一个更接近真实场景的例子:处理一份销售数据。
假设我们有一个原始的销售记录DataFramesales_df,存在以下问题:
amount列是字符串类型,且有货币符号(如“$100.5”)。sale_date列是“YYYYMMDD”格式的字符串。- 我们只需要2023年以后的数据。
- 我们想计算一个“折扣后金额”列(假设打9折),并只保留相关列输出。
import polars as pl # 模拟原始数据 sales_df = pl.DataFrame({ “order_id”: [“A001”, “A002”, “A003”, “A004”], “amount”: [“$100.50”, “$200.00”, “$150.75”, “$80.30”], “sale_date”: [“20221215”, “20230120”, “20230510”, “20240105”], “customer”: [“Alice”, “Bob”, “Alice”, “Charlie”] }) print(“原始数据:”) print(sales_df) print(sales_df.schema) # 查看数据类型 # 第一步:类型转换与清洗 # 1. 清洗 amount 列:去掉`$`,转成浮点数 # 2. 转换 sale_date 列:字符串转日期 df_cleaned = sales_df.with_columns( pl.col(“amount”).str.replace_all(“\$”, “”).cast(pl.Float64).alias(“amount”), pl.col(“sale_date”).str.strptime(pl.Date, “%Y%m%d”).alias(“sale_date”) ) print(“\n清洗转换后:”) print(df_cleaned) print(df_cleaned.schema) # 第二步:行筛选(过滤) # 只保留2023年及以后的销售记录 df_filtered = df_cleaned.filter(pl.col(“sale_date”) >= pl.Date(2023, 1, 1)) print(“\n过滤后(2023年以后):”) print(df_filtered) # 第三步:列转换与选择 # 1. 计算折扣后金额(9折) # 2. 选择最终需要的列 final_df = df_filtered.with_columns( (pl.col(“amount”) * 0.9).alias(“amount_after_discount”) ).select( “order_id”, “customer”, “sale_date”, “amount”, “amount_after_discount” ) print(“\n最终结果:”) print(final_df)这个流程的关键点:
- 顺序:通常是
清洗/转换类型->过滤行->计算新列/选择列。先处理脏数据和非标类型,能避免后续过滤和计算出错。 - 链式调用:上述步骤可以写成一行链式调用:
sales_df.with_columns(...).filter(...).with_columns(...).select(...)。Polars的惰性执行会优化整个链条。 alias的使用:在with_columns中,如果新列名与旧列名相同(如amount),会覆盖旧列。如果想保留旧列,需要指定不同的alias。
5. 性能考量与进阶技巧
当你熟悉基础操作后,这些进阶技巧能帮你写出更高效、更优雅的Polars代码。
5.1 惰性执行(LazyFrame)下的选择与转换
所有上述操作在LazyFrame上同样适用,而且强烈建议在数据处理管道中使用惰性模式。
# 创建 LazyFrame lazy_sales = sales_df.lazy() # 构建完整的惰性查询计划 query = (lazy_sales .with_columns( pl.col(“amount”).str.replace_all(“\$”, “”).cast(pl.Float64).alias(“amount”), pl.col(“sale_date”).str.strptime(pl.Date, “%Y%m%d”) ) .filter(pl.col(“sale_date”) >= pl.Date(2023, 1, 1)) .with_columns((pl.col(“amount”) * 0.9).alias(“amount_after_discount”)) .select(“order_id”, “customer”, “sale_date”, “amount”, “amount_after_discount”) ) # 查看优化后的查询计划(可选,用于调试) print(query.explain()) # 真正执行计算,得到结果 DataFrame final_df_lazy = query.collect()惰性执行的优势在于:
- 查询优化:Polars会合并操作、下推谓词和投影,减少中间数据移动。
- 流水线并行:整个计划可以更高效地利用多核CPU。
- 避免物化中间结果:除非调用
collect、fetch或sink,否则数据不会真正加载到内存中计算。
5.2 多列批量操作
当需要对很多列进行相同操作时(比如将所有数值列转为Float32),手动写每一列很麻烦。Polars提供了便捷的方式。
# 选择所有数值列进行转换 df = df.with_columns(pl.col(pl.NUMERIC_DTYPES).cast(pl.Float32)) # 选择所有列名以“_temp”结尾的列并删除 df = df.select(pl.all().exclude(pl.col(“^.*_temp$”))) # 使用正则表达式排除 # 对多列应用同一个字符串操作 df = df.with_columns(pl.col([“col1”, “col2”, “col3”]).str.strip_chars())5.3 处理转换中的错误与空值
数据转换时,脏数据或边界情况可能导致错误。Polars的表达式API提供了稳健的处理方式。
# 1. 使用 `strict=False` 在 cast 时容忍错误,产生 null df = df.with_columns(pl.col(“dirty_int”).cast(pl.Int64, strict=False)) # 2. 使用 `fill_null` 为转换产生的 null 提供默认值 df = df.with_columns( pl.col(“dirty_int”).cast(pl.Int64, strict=False).fill_null(-1) ) # 3. 使用 `replace` 在转换前清洗数据 df = df.with_columns( pl.col(“amount_str”).str.replace(“[^0-9.]”, “”, literal=False).cast(pl.Float64) ) # 这个正则表达式移除了所有非数字和小数点的字符5.4 选择与转换的性能陷阱
- 避免在
select或with_columns中重复计算相同表达式。如果多个新列依赖同一个复杂计算,先计算并保存到一个临时列,或者使用alias后在其他表达式中引用pl.col(“temp_col”)。 - 谨慎使用
apply函数。apply会将操作切换到Python的慢速循环中,失去Polars的向量化性能优势。绝大多数标准操作(字符串处理、数学运算、日期计算)都有内置的表达式方法,应优先使用。 filter条件尽量简单。复杂的filter条件可能阻碍查询优化。如果条件太复杂,考虑分步过滤,或者先用with_columns计算出一个布尔列,再基于此列过滤。
6. 总结:从知道到熟练
Polars的行列选择与转换,核心是掌握select、filter、with_columns和cast这几个方法,并理解其背后的表达式哲学(pl.col)。
一个高效的Polars数据处理流程通常是这样的:
- 读入数据为
LazyFrame(如果数据量大)。 - 清洗与类型转换:使用
with_columns配合str.*方法、cast和strptime,将数据整理成干净、类型正确的状态。 - 行过滤:使用
filter基于业务逻辑筛选数据,尽早减少数据量。 - 特征工程/列转换:使用
with_columns计算衍生列、聚合指标等。 - 列选择:使用
select精准选择最终需要的列,减少内存占用和I/O。 - 执行与输出:调用
collect()(惰性模式)或直接得到结果,然后写入文件或进行下一步分析。
刚开始你可能会觉得Polars的表达式写法有点绕,不如Pandas的赋值直观。但一旦习惯,你会发现这种声明式的风格让复杂的数据管道更清晰、更易维护,并且能实实在在地享受到性能提升。最关键的是,先确保单条数据转换的逻辑正确,再通过链式调用组合成完整管道,这是用好Polars最踏实的一条路径。