Tacit-Knowledge-of-Advanced-Polars / zh
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及其强大的(自动)优化
- 就像表达式只是列操作的"计划"一样,
LazyFrame表示DataFrame操作的计划。通过在LazyFrame上执行操作并延迟到最后一步才实际计算,Polars内部的查询执行引擎能够看到操作的全貌,从而实现多种优化。这些优化通常将计算效率推向物理极限,最小化IO、寻址、加法和乘法操作。可以说,操作100万行的Polars DataFrame感觉就像操作10行的CSV一样快。 - 大多数(95%+)方法在
pl.DataFrame和pl.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
