ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Polars数据操作指南:从Pandas思维迁移到高效行列选择与转换

Polars数据操作指南:从Pandas思维迁移到高效行列选择与转换 如果你正在处理大规模数据并且对Pandas的性能瓶颈感到头疼那么Polars很可能已经出现在你的视野里。它凭借Rust底层和多核并行能力在速度上对Pandas实现了“降维打击”。然而很多开发者在从Pandas迁移到Polars时遇到的第一个“水土不服”不是性能而是如何像以前一样灵活地选择、筛选和转换数据。你可能会发现熟悉的.iloc、.loc在Polars里要么用法不同要么根本不存在。一个简单的“选择A列大于10的行并计算B列平均值”的操作在Pandas里信手拈来在Polars里却需要重新思考表达方式。这并非Polars设计得不好恰恰相反它提供了一套更严谨、更高效且利于并行计算的API。问题在于我们习惯了Pandas的“命令式”思维而Polars更倾向于“声明式”和“延迟执行”的范式。这篇文章要解决的正是这个核心痛点。我们将深入Polars数据操作的心脏地带——行列选择与数据转换。我不会只罗列API而是会带你理解Polars设计这套API背后的逻辑为什么它要放弃.ilocselect和filter的真正威力在哪里那些看似复杂的when、then、otherwise链式调用如何写出既高效又易读的代码更重要的是我会通过大量对比Pandas与Polars的代码示例让你直观地看到思维需要如何转换以及转换后带来的性能收益。无论你是正在评估Polars还是已经使用但感觉不够顺手这篇文章都将帮你打通任督二脉。你会发现一旦掌握了Polars的“语法”你不仅能获得数倍到数十倍的速度提升还能写出更清晰、更易于维护的数据处理流水线。1. 思维转换从“我要怎么做”到“我要什么结果”在深入代码之前我们必须先完成一次思维模式的转换。这是用好Polars最关键的一步。Pandas是“命令式”的它像在指挥一个机器人一步一步地告诉它做什么。“先取这一列再根据条件过滤这些行然后对那一列求和。” 代码顺序基本反映了执行顺序。.iloc按整数位置选择和.loc按标签选择是这种思维的典型代表它们直接操作内存中的数据视图。Polars是“声明式”的它更像是在描述你想要的结果。“给我一个数据集其中包含A列和B列并且A列的值大于10最后按C列分组。” Polars的查询优化器Query Optimizer会分析整个描述生成一个最优的执行计划可能重新排序操作、合并步骤并充分利用多核并行。它的核心操作select选择列和filter过滤行都是延迟执行的只有在最终需要结果如.collect()或.fetch()时才会真正计算。这种区别带来的直接影响是性能声明式延迟执行查询优化让Polars能进行大规模优化。代码风格Polars代码通常是链式调用Method Chaining更函数式更紧凑。学习曲线对于习惯了Pandas“所见即所得”的用户初期会觉得有些抽象不够直观。理解这一点后我们再去看Polars的行列选择API就不会觉得它“反人类”而是能体会到其设计之美。2. 核心概念理解Polars的数据结构在操作之前先快速回顾Polars的两个核心数据结构这关系到所有选择操作的基础。DataFrame: 二维表格与Pandas的DataFrame概念一致是列Series的集合。Series: 一维数组带有数据类型DataType如Int64Utf8Boolean等。Expr表达式: 这是Polars的灵魂。Expr代表一个尚未执行的计算。当你写pl.col(“A”)、pl.col(“A”) 10时你得到的不是一个结果而是一个Expr对象。select、filter、with_columns等方法都接受Expr作为参数。查询优化器正是在Expr组成的表达式树上进行优化的。一个关键认知在Polars中几乎所有的数据转换操作都是通过构建和组合Expr来完成的。行列选择本质上是构建特定的列选择表达式和行过滤表达式。3. 环境准备搭建你的Polars playground在开始实操前确保你的环境已经就绪。Polars的安装非常简单。# 使用pip安装Polars推荐安装完整版以支持所有功能 pip install polars[all] # 或者最小化安装 pip install polars为了进行对比和验证我们通常也会安装Pandas。pip install pandas接下来创建一个Python脚本或Jupyter Notebook导入必要的库并生成一个示例数据集用于后续所有操作。import polars as pl import pandas as pd import numpy as np # 创建一个示例Polars DataFrame df_pl pl.DataFrame({ “id”: range(1, 11), # 1到10的整数 “name”: [f“Name_{i}” for i in range(1, 11)], “department”: [“Tech”, “HR”, “Tech”, “Finance”, “HR”, “Tech”, “Finance”, “Tech”, “HR”, “Finance”], “salary”: [55000, 42000, 71000, 48000, 39000, 80000, 52000, 68000, 45000, 90000], “bonus”: [5000, 3000, 8000, 4000, 2500, 10000, 4500, 7500, 3500, 11000], “join_year”: [2019, 2020, 2018, 2021, 2020, 2017, 2022, 2019, 2021, 2016] }) print(“原始Polars DataFrame:”) print(df_pl) print(f“Shape: {df_pl.shape}”)运行后你会看到一个10行6列的表格包含ID、姓名、部门、薪资、奖金和入职年份。我们将用它来演示所有行列选择与转换操作。4. 列的选择Selecting Columnsselect的多种姿势在Pandas中选择列可以用df[[‘A‘ ‘B‘]]或df.loc[ [‘A‘ ‘B‘]]。在Polars中主要使用df.select()方法它接受一个或多个Expr。4.1 基础选择按列名选择这是最直接的方式。# 选择单列 - 返回一个DataFrame单列 selected_single df_pl.select(“name”) print(“选择单列 ‘name‘:”) print(selected_single) print(type(selected_single)) # polars.dataframe.frame.DataFrame # 选择多列 - 注意是逗号分隔的多个参数或者一个列表 selected_multi df_pl.select(“name” “department” “salary”) print(“\n选择多列 ‘name‘ ‘department‘ ‘salary‘:”) print(selected_multi)关键点df.select(“name”)返回的仍然是一个DataFrame而不是Series。如果你想获取一个Series需要使用df[“name”]或df.get_column(“name”)。4.2 使用pl.col()表达式进行高级选择pl.col()是构建列表达式的起点它提供了极大的灵活性。# 使用 pl.col() 选择列 selected_by_col df_pl.select(pl.col(“name”), pl.col(“salary”)) print(“使用 pl.col() 选择列:”) print(selected_by_col) # 选择所有列通常用于在链式调用中重新排序列 select_all df_pl.select(pl.all()) print(“\n选择所有列pl.all():”) print(select_all) # 使用通配符选择列非常实用 # 假设我们有很多以 ‘temp_‘ 开头的列 df_with_prefix pl.DataFrame({“temp_1”: [1 2] “temp_2”: [3 4] “data”: [5 6]}) selected_wildcard df_with_prefix.select(pl.col(“temp_*”)) print(“\n使用通配符选择列:”) print(selected_wildcard)4.3 按数据类型选择列在数据清洗时我们常常需要统一处理某一类型的所有列比如将所有浮点数转换为整数。# 选择所有数值类型的列 numeric_cols df_pl.select(pl.col(pl.NUMERIC_DTYPES)) print(“选择所有数值类型列:”) print(numeric_cols) # 选择所有整数类型的列 int_cols df_pl.select(pl.col(pl.INTEGER_DTYPES)) print(“\n选择所有整数类型列:”) print(int_cols)4.4 排除某些列有时我们想选择“除了某几列之外的所有列”。# 选择除了 ‘id‘ 和 ‘join_year‘ 之外的所有列 excluded df_pl.select(pl.exclude(“id” “join_year”)) print(“选择排除 ‘id‘ 和 ‘join_year‘ 的列:”) print(excluded)4.5 与Pandas的对比# Pandas 等效操作 df_pd df_pl.to_pandas() # 选择多列 pd_selected df_pd[[“name” “salary”]] pl_selected df_pl.select(“name” “salary”) print(“Pandas 方式:”) print(pd_selected.head()) print(“\nPolars 方式:”) print(pl_selected.head())你会发现Pandas的df[[列列表]]是索引操作而Polars的df.select(列列表)是方法调用。在链式操作中Polars的方式更加连贯。5. 行的过滤Filtering Rowsfilter与条件表达式行过滤是数据分析中最常见的操作之一。Polars使用df.filter()方法它接受一个结果为布尔型的Expr。5.1 基础条件过滤# 选择 salary 大于 60000 的行 high_salary df_pl.filter(pl.col(“salary”) 60000) print(“薪资大于60000的员工:”) print(high_salary) # 选择部门为 ‘Tech‘ 的行 tech_employees df_pl.filter(pl.col(“department”) “Tech”) print(“\n部门为 ‘Tech‘ 的员工:”) print(tech_employees)5.2 多条件组合使用(与)|(或)~(非) 来组合条件。非常重要每个条件必须用括号括起来因为运算符优先级问题。# 选择部门为 ‘Tech‘ 且薪资大于 60000 的员工 tech_high df_pl.filter((pl.col(“department”) “Tech”) (pl.col(“salary”) 60000)) print(“Tech部门且薪资60000的员工:”) print(tech_high) # 选择部门为 ‘Tech‘ 或 ‘Finance‘ 的员工 tech_or_finance df_pl.filter((pl.col(“department”) “Tech”) | (pl.col(“department”) “Finance”)) print(“\n部门为 Tech 或 Finance 的员工:”) print(tech_or_finance) # 选择部门不是 ‘HR‘ 的员工 not_hr df_pl.filter(pl.col(“department”) ! “HR”) # 或者使用 ~ not_hr_alt df_pl.filter(~(pl.col(“department”) “HR”)) print(“\n部门不是 HR 的员工 (两种写法):”) print(not_hr)5.3 使用is_in进行集合成员判断当需要匹配多个值时is_in()比多个|更清晰高效。# 选择部门在 [‘Tech‘ ‘Finance‘] 中的员工 target_depts [“Tech” “Finance”] employees_in_depts df_pl.filter(pl.col(“department”).is_in(target_depts)) print(“部门在 [‘Tech‘ ‘Finance‘] 中的员工:”) print(employees_in_depts)5.4 字符串匹配过滤对于文本数据Polars提供了强大的字符串表达式通过pl.col(“str_column”).str访问。# 选择名字以 ‘Name_1‘ 开头的员工比如 Name_1 Name_10等这里演示方法 # 我们的数据是Name_1到Name_10我们找包含‘1‘的 name_contains_1 df_pl.filter(pl.col(“name”).str.contains(“1”)) print(“名字中包含字符 ‘1‘ 的员工:”) print(name_contains_1) # 选择名字以 ‘Name_2‘ 结尾的员工 name_ends_with_2 df_pl.filter(pl.col(“name”).str.ends_with(“_2”)) print(“\n名字以 ‘_2‘ 结尾的员工:”) print(name_ends_with_2)5.5 与Pandas的对比# Pandas 等效操作 # 单条件 pd_filtered df_pd[df_pd[“salary”] 60000] # 多条件 pd_filtered_multi df_pd[(df_pd[“department”] “Tech”) (df_pd[“salary”] 60000)] print(“Pandas 多条件过滤:”) print(pd_filtered_multi) print(“\nPolars 多条件过滤:”) print(tech_high)Pandas使用布尔索引而Polars使用filter方法。Polars的表达式在复杂条件下更易读尤其是结合链式调用时。6. 行列同时选择select与filter的链式组合在实际工作中我们几乎总是需要同时进行行列选择。Polars的链式调用让这变得非常优雅。# 选择 Tech 部门员工的 name 和 salary 列 result ( df_pl .filter(pl.col(“department”) “Tech”) .select(“name” “salary”) ) print(“Tech部门员工的姓名和薪资:”) print(result) # 更复杂的例子选择2019年及以后入职且薪资高于部门平均薪资的员工这里需要用到窗口函数后续会讲 # 我们先做一个简单的选择薪资大于70000或奖金大于8000的员工的ID、姓名和部门 complex_result ( df_pl .filter((pl.col(“salary”) 70000) | (pl.col(“bonus”) 8000)) .select(“id” “name” “department” “salary” “bonus”) .sort(“salary” descendingTrue) # 按薪资降序排序 ) print(“\n薪资70000或奖金8000的员工按薪资降序:”) print(complex_result)这种df.filter(...).select(...).sort(...)的链式写法是Polars的典型风格它清晰地描述了数据处理流水线并且由于延迟执行整个流水线会被优化后执行。7. 高效的数据转换Transformationwith_columns与表达式威力选择数据后下一步往往是对数据进行转换计算新列、修改现有列。这是Polars表达式系统大放异彩的地方。核心方法是df.with_columns()。重要with_columns会返回一个包含新列或修改列的新DataFrame不会原地修改原DataFramePolars默认是不可变的这有利于并发和安全。7.1 创建新列# 计算总报酬 (salary bonus) df_with_total df_pl.with_columns( (pl.col(“salary”) pl.col(“bonus”)).alias(“total_compensation”) ) print(“添加总报酬列:”) print(df_with_total) # 同时创建多个新列 df_with_multiple df_pl.with_columns([ (pl.col(“salary”) * 1.1).alias(“salary_after_raise”), (pl.col(“join_year”).cast(pl.Utf8)).alias(“join_year_str”) # 转换数据类型 ]) print(“\n同时添加多个新列:”) print(df_with_multiple.select(“name” “salary” “salary_after_raise” “join_year” “join_year_str”))7.2 条件转换类似SQL的CASE WHEN或Pandas的np.where/Series.wherePolars提供了极其强大的pl.when().then().otherwise()语法它是声明式的并且可以被优化。# 根据薪资水平打标签 df_with_label df_pl.with_columns( pl.when(pl.col(“salary”) 70000) .then(pl.lit(“High”)) .when((pl.col(“salary”) 50000) (pl.col(“salary”) 70000)) .then(pl.lit(“Medium”)) .otherwise(pl.lit(“Low”)) .alias(“salary_level”) ) print(“添加工资等级标签:”) print(df_with_label.select(“name” “salary” “salary_level”))pl.lit()用于创建一个字面量常量列。when().then().otherwise()链必须完整以覆盖所有情况。7.3 使用函数进行复杂转换你可以对列应用任何函数但为了保持高性能并行化应优先使用Polars内建的表达式函数。如果必须用Python函数使用map_elements原apply但要注意性能损耗。# 使用内建表达式函数计算名字的长度 df_with_len df_pl.with_columns( pl.col(“name”).str.len_chars().alias(“name_length”) # .str.len_chars() 计算字符数.str.lengths()计算字节数 ) print(“计算名字长度使用内建函数:”) print(df_with_len.select(“name” “name_length”)) # 使用自定义Python函数谨慎使用可能成为性能瓶颈 def year_to_period(year): if year 2018: return “Early” elif year 2020: return “Mid” else: return “Recent” df_with_period df_pl.with_columns( pl.col(“join_year”).map_elements(year_to_period return_dtypepl.Utf8).alias(“join_period”) ) print(“\n使用自定义函数添加入职时期:”) print(df_with_period.select(“name” “join_year” “join_period”))7.4 与Pandas转换的对比# Pandas 等效操作 df_pd[“total_compensation”] df_pd[“salary”] df_pd[“bonus”] # 条件赋值 df_pd[“salary_level”] np.where(df_pd[“salary”] 70000 “High” np.where(df_pd[“salary”] 50000 “Medium” “Low”)) print(“Pandas 转换结果:”) print(df_pd[[“name” “salary” “total_compensation” “salary_level”]].head())Pandas的转换通常是命令式的、立即执行的。Polars的with_columns是声明式的并且是延迟执行的可以与其他操作一起被优化。8. 选择与转换的综合实战一个完整的数据处理管道让我们把这些知识串联起来解决一个实际的数据清洗和特征工程问题。任务我们有一份员工数据需要只分析“Tech”和“Finance”部门的员工。计算他们的总报酬薪资奖金和报酬与部门平均薪资的比率。根据总报酬创建等级“A“ 80000 “B“ 60000 “C“ 60000。最终只保留“姓名”、“部门”、“总报酬”、“报酬比率”和“等级”这几列并按总报酬降序排列。# 使用 Polars 链式操作一气呵成 processed_df ( df_pl # 1. 过滤行 .filter(pl.col(“department”).is_in([“Tech” “Finance”])) # 2. 计算总报酬 .with_columns( (pl.col(“salary”) pl.col(“bonus”)).alias(“total_comp”) ) # 3. 计算部门平均薪资使用窗口函数 .with_columns( pl.col(“salary”).mean().over(“department”).alias(“dept_avg_salary”) ) # 4. 计算报酬比率 .with_columns( (pl.col(“total_comp”) / pl.col(“dept_avg_salary”)).alias(“comp_ratio”) ) # 5. 创建等级 .with_columns( pl.when(pl.col(“total_comp”) 80000) .then(pl.lit(“A”)) .when(pl.col(“total_comp”) 60000) .then(pl.lit(“B”)) .otherwise(pl.lit(“C”)) .alias(“grade”) ) # 6. 选择最终列 .select(“name” “department” “total_comp” “comp_ratio” “grade”) # 7. 排序 .sort(“total_comp” descendingTrue) ) print(“综合数据处理管道结果:”) print(processed_df)这段代码清晰地展示了Polars声明式编程的魅力。整个数据处理逻辑像一条流水线从上到下阅读非常符合人类对数据转换步骤的思考顺序。更重要的是Polars的查询优化器会分析这整条流水线可能重新安排计算顺序比如将多个with_columns合并以最优方式执行。9. 性能对比与最佳实践理解了“如何做”之后我们来看看“为什么”要这么做以及如何做得更好。9.1 延迟执行与查询优化Polars的LazyFrameAPI是性能的关键。上面的例子我们用的是DataFrame即时执行。对于大规模数据应始终优先使用LazyFrame。# 使用 LazyFrame 重写上面的管道 lazy_processed ( df_pl.lazy() # 转换为 LazyFrame .filter(pl.col(“department”).is_in([“Tech” “Finance”])) .with_columns( (pl.col(“salary”) pl.col(“bonus”)).alias(“total_comp”) ) .with_columns( pl.col(“salary”).mean().over(“department”).alias(“dept_avg_salary”) ) .with_columns( (pl.col(“total_comp”) / pl.col(“dept_avg_salary”)).alias(“comp_ratio”) ) .with_columns( pl.when(pl.col(“total_comp”) 80000) .then(pl.lit(“A”)) .when(pl.col(“total_comp”) 60000) .then(pl.lit(“B”)) .otherwise(pl.lit(“C”)) .alias(“grade”) ) .select(“name” “department” “total_comp” “comp_ratio” “grade”) .sort(“total_comp” descendingTrue) ) # 此时没有任何计算发生只是构建了查询计划 print(“Lazy查询计划:”) print(lazy_processed.explain()) # 触发计算 final_result lazy_processed.collect() print(“\n最终计算结果 (通过 LazyFrame):”) print(final_result)explain()方法可以打印查询计划让你看到Polars是如何优化你的操作的。对于复杂查询性能提升可能非常显著。9.2 最佳实践总结拥抱表达式Expr尽量使用pl.col()构建的表达式和Polars内建函数strdtlist等命名空间下的函数进行计算。避免在map_elements中使用Python函数除非万不得已。优先使用Lazy API对于从文件读取或复杂转换使用.lazy()和.collect()。这允许Polars进行全局优化如谓词下推、投影下推。链式调用将filterselectwith_columnsgroup_by等操作写成链式调用。这不仅是代码风格也更利于优化。减少.clone()和中间变量Polars的延迟执行和优化器会处理中间步骤通常不需要手动创建很多中间DataFrame。过度使用.clone()会阻碍优化。理解不可变性Polars操作默认返回新的DataFrame。这避免了副作用使代码更安全、更易于推理。选择正确的数据类型使用cast确保数据类型正确例如将字符串数字转换为整数。正确的数据类型能大幅提升性能并减少内存占用。10. 常见问题与排查思路问题现象可能原因排查方式解决方案filter条件不生效返回空DataFrame1. 条件逻辑错误如/优先级问题未加括号。2. 数据类型不匹配如字符串与数字比较。3. 列名拼写错误或大小写问题。1. 打印条件表达式pl.col(“A”) 10看是否合理。2. 使用df.schema检查列数据类型。3. 使用df.columns检查列名。select时提示列不存在列名错误或列在上游操作中已被剔除。检查当前DataFrame的列print(df.columns)。在链式操作中确保要选择的列在之前的步骤中仍然存在。修正列名或在链式操作中提前保留需要的列。with_columns创建的新列全是null1. 表达式计算错误如除零。2. 使用了不匹配的pl.lit值。3. 条件表达式when().then().otherwise()未覆盖所有情况。1. 逐步执行先检查用于计算的原始列数据。2. 检查when条件是否有逻辑漏洞。1. 处理可能的异常值如使用fill_nan。2. 确保when().then().otherwise()链完整。使用自定义函数map_elements速度极慢map_elements原apply会将数据逐行传到Python解释器无法利用Rust的并行优化是性能杀手。考虑是否能用Polars内建表达式重写逻辑。使用profile功能定位瓶颈。尽可能避免。寻找对应的Polars内建表达式如字符串处理用.str日期处理用.dt数学运算用算术表达式。从Pandas转换后操作报错Pandas DataFrame可能包含Polars不支持的数据类型如object混合类型或缺失值表示NaNvsnull。使用df_pl pl.from_pandas(df_pd)转换后立即检查df_pl.schema。在Pandas端做好数据清洗或使用Polars转换后使用cast和fill_null统一数据类型和处理空值。链式操作很长难以调试代码可读性降低错误难以定位。将长链拆分成多个步骤用临时变量存储中间结果逐步检查。在开发阶段可以适当拆分。对于稳定且复杂的流水线考虑将其封装为函数并在关键步骤添加print或记录中间状态。掌握Polars的行列选择与转换核心在于从“操作数据”的思维转向“描述数据需求”的思维。它提供的select、filter、with_columns以及强大的表达式系统不仅是一套新的API更是一种高效处理数据的新范式。开始时的不适应换来的是在处理海量数据时巨大的性能优势和更清晰的代码结构。建议你将文中的示例代码在自己的环境中逐一运行、修改和实验这是建立肌肉记忆最快的方式。当你下次面对一个需要复杂过滤、转换和计算的任务时不妨先思考“用Polars的表达式该怎么描述” 你会发现很多曾经需要多步循环或复杂Pandas操作的任务现在可以用一行清晰、高效的Polars链式调用轻松解决。
返回列表