Tacit-Knowledge-of-Advanced-Polars / zh

  • Polars高阶技巧:写出优雅的数据处理代码

    • Polars标志
  • 花式列选择——表达式扩展

  • 表达式扩展是一种声明式选择具有特定属性列的方法:
    • 选择所有列:pl.all() 或 `pl.col('')`
      使用正则选择列名:`pl.col("^price_of_gen.$")`
      * 按类型选择列:pl.col(pl.Int32)
      * 排除特定列:pl.all().exclude(pattern)
  • 当这些技巧组合使用时会产生神奇效果。以下是表达意图清晰、代码简洁的示例:
    • ```python
      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)
      )
      ```
  • 表达式组合构建复杂表达式

    • 类似于函数抽象与组合,我们可以对表达式应用相同原则:
    • ```python
      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 ) )
      ```
  • 在计算上下文中混合异构列

    • ```python
      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,每行一个操作:
    • ```python
      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()获取当前名称。
    • ```python
      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])
      ```
  • 利用LazyFrame及其强大的(自动)优化

    • 上述特性已足够让我偏爱Polars胜过其他DataFrame库。但最精彩的部分还在后面——惰性计算。在优雅的API之下,惰性特性展现了Polars出色的实现。
    • 就像表达式只是列操作的"计划"一样,LazyFrame表示DataFrame操作的计划。通过在LazyFrame上执行操作并延迟到最后一步才实际计算,Polars内部的查询执行引擎能够看到操作的全貌,从而实现多种优化。这些优化通常将计算效率推向物理极限,最小化IO、寻址、加法和乘法操作。可以说,操作100万行的Polars DataFrame感觉就像操作10行的CSV一样快。
    • 大多数(95%+)方法在pl.DataFramepl.LazyFrame之间共享相同的签名,这意味着在惰性模式和探索性调试之间切换非常容易:
      • ```python
        # 以下代码有问题
        (
        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()
        )
        ```
        这使得从非惰性代码开始,并逐步替换/升级为惰性版本变得容易。为了使代码更简洁和可组合,可以编写一些辅助函数:
        ```python
        # 确保函数在惰性模式下运行的@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也会是个不错的选择。
  • 安全防御性编程

  • 防范意外/非预期结果:
    ```python
    # 比.replace()更好:防范意外的NULL
    col('to_replace').replace_strict(mapping_dict)
    # 确保它是你认为的样子
    df1.join(df2, on='uid', how='left', validate='1:m') # 验证关系
    ```
    (译文说明:技术术语保持英文原词,代码块保留原格式,采用技术博客常见的简洁表达方式,复杂概念添加中文注释,整体符合中文技术文档的阅读习惯)
  • tags: polars

A digital garden, perpetually growing.