-
Polars高阶技巧:写出优雅的数据处理代码
-
花式列选择——表达式扩展
-
表达式扩展是一种声明式选择具有特定属性列的方法:
-
- 选择所有列:
pl.all()或pl.col('*') - 使用正则选择列名:
pl.col("^price_of_gen.*$") - 按类型选择列:
pl.col(pl.Int32) - 排除特定列:
pl.all().exclude(pattern)
- 选择所有列:
-
-
当这些技巧组合使用时会产生神奇效果。以下是表达意图清晰、代码简洁的示例:
-
import polars as pl; col = pl.col # 计算value_1到value_n相对于value_0的变化 df_chg = df.select( col('^value\_.+$').exclude('value_0') - col('value_0') ) # 仅浮点类型列支持`is_not_nan`方法 df_no_nan = df.filter( pl.all_horizontal( col(pl.Float32).is_not_nan()) ) # 双精度转单精度: df_casted = df.with_columns( col(pl.Float64).cast(pl.Float32) # 或:pl.selectors.float().cast(pl.Float64) )
-
-
表达式组合构建复杂表达式
- 类似于函数抽象与组合,我们可以对表达式应用相同原则:
-
def clip_greater_than_3(e: pl.Expr) -> pl.Expr: return e.clip(-3, 3) def calculate_max(e: pl.Expr) -> pl.Expr: return e.max() observations = pl.col("height", "weight", "age") outlier_processor, aggregator = clip_greater_than_3, calculate_max processed_observations = aggregator( outlier_processor(observations) ) # processed_observations仍是表达式! print( df.select( processed_observations ) )
-
在计算上下文中混合异构列
-
extra_requested_exprs = [col('birthday'), col('color').replace({'red': 'Red'})] df.select( 'date', 'uid', col('^next\_.+\_day\_growth$').clip(0, 10), *extra_requested_exprs )
-
-
管道式操作提升可读性
- 将每个操作重构为
.select、.with_columns或.pipe,每行一个操作: -
def analyze_battery_usage_rate(df: pl.DataFrame) -> pl.DataFrame: return ( df # 预处理 .select('app', 'launch_time', 'close_time', 'battery_used') .filter( col('close_time').is_not_null(), col('lauch_time') > yesterday_evening ) # 按组分析电量下降率 .group_by( col('app').starts_with('sys').alias('is_system_app') ) .agg( launch_order=col('launch_time').rank(), decline_rate=( col('battery_used') / (col('close_time') - col('launch_time')) ) ) # 添加衍生列 .with_columns( (col('decline_rate') > col('decline_rate').quantile(.7)).alias('burner') ) # 使用.pipe运行普通函数 .pipe( send_df_to_sink, output_path='/here_is_cool.parquet' ) )
- 将每个操作重构为
-
列名操作
- 在DataFrame层面使用
df.rename(映射或函数)。在表达式层面,使用Expr.name.suffix/prefix添加前后缀,使用Expr.meta.output_name()获取当前名称。 -
df_jan = pl.DataFrame({ 'day': [1, 2], 'low': [11, 22], }) df_feb = pl.DataFrame({ 'day': [1, 2], 'value': [101, 202], }) df_feb.join( df_jan.select( 'day', col('value').name.prefix('jan_')), # 通过添加前缀重命名 on='day' ) def get_statistics_exprs(value: str) -> list[pl.Expr]: return [col(value).count().alias('total'), col(value).mean().alias('avg_value')] # plot_stats是只接受字符串列名的API示例 def plot_stats(df: pl.DataFrame, x: str, y_cols: list[str]): return display(df.to_pandas(), x, y_cols) df = pl.DataFrame({'group': ['A', 'A', 'B'], 'sample_value': [1, 3, 5]}) exprs = get_statistics_exprs('sample_value') df_w_stats = ( df .group_by('group') .agg( *exprs ) ) # 使用pl.Expr.meta.output_name(): plot_stats(df_w_stats, 'group', [e.meta.output_name() for e in exprs])
- 在DataFrame层面使用
-
利用LazyFrame及其强大的(自动)优化
- 上述特性已足够让我偏爱Polars胜过其他DataFrame库。但最精彩的部分还在后面——惰性计算。在优雅的API之下,惰性特性展现了Polars出色的实现。
- 就像表达式只是列操作的”计划”一样,
LazyFrame表示DataFrame操作的计划。通过在LazyFrame上执行操作并延迟到最后一步才实际计算,Polars内部的查询执行引擎能够看到操作的全貌,从而实现多种优化。这些优化通常将计算效率推向物理极限,最小化IO、寻址、加法和乘法操作。可以说,操作100万行的Polars DataFrame感觉就像操作10行的CSV一样快。 - 大多数(95%+)方法在
pl.DataFrame和pl.LazyFrame之间共享相同的签名,这意味着在惰性模式和探索性调试之间切换非常容易:-
# 以下代码有问题 ( df.lazy() .with_columns( *exprs ) .group_by( 'group' ).agg ( *agg_exprs ) .collect() ) # 回到即时模式逐步检查: ( df#.lazy() # 注释掉.lazy()其他API仍同样工作 .with_columns( *exprs ) # 现在关注这行结果 #.group_by( 'group' ).agg ( *agg_exprs ) #.collect() )这使得从非惰性代码开始,并逐步替换/升级为惰性版本变得容易。为了使代码更简洁和可组合,可以编写一些辅助函数:
# 确保函数在惰性模式下运行的@lazify装饰器: Df2DfFunc = Callable[[DataFrame], DataFrame] def lazify(func: Df2DfFunc) -> Df2DfFunc: def wrapped(df, *args, **kwargs): return func(df.lazy(), *args, **kwargs).collect() return wrapped # 更好的是,确保函数同时接受Lazy和DataFrame LazyOrNot = DataFrame | LazyFrame def good_func(df: LazyOrNot) -> LazyOrNot: return df.with_columns( *expr ) # 有时到处是Lazy/非Lazy帧但你只想显示 def no_lazy(df: LazyOrNot) -> DataFrame: return df.collect() if isinstance(df, LazyFrame) else df print( df .pipe( this_func_might_return_lazyframe ) .pipe( another_func_might_return_lazyframe ) .pipe( no_lazy ) .shape # shape只在非惰性DataFrame上工作 )惰性特性使其像Pandas一样易用,又像Apache Spark一样高效。
-
-
使用Parquet
-
Apache Arrow格式已成为当今数据相关工具中数据存储/通信的标准。Parquet和Polars都使用Apache Arrow格式。这使得Polars读写Parquet文件极其快速。
如果你在进行大数据量分析,Parquet是你的好朋友。如果不确定,Parquet也会是个不错的选择。
-
安全防御性编程
-
防范意外/非预期结果:
# 比.replace()更好:防范意外的NULL col('to_replace').replace_strict(mapping_dict) # 确保它是你认为的样子 df1.join(df2, on='uid', how='left', validate='1:m') # 验证关系(译文说明:技术术语保持英文原词,代码块保留原格式,采用技术博客常见的简洁表达方式,复杂概念添加中文注释,整体符合中文技术文档的阅读习惯)
-
tags: polars
