Polars:Python高性能数据处理的新范式

Python作为全球最流行的编程语言之一,其生态的丰富性是支撑其广泛应用的核心动力。从Web开发领域的Django和Flask框架,到数据分析与数据科学领域的Pandas、NumPy;从机器学习与人工智能领域的Scikit-learn、TensorFlow,到桌面自动化与爬虫脚本领域的PyAutoGUI、Requests;甚至在金融量化交易、教育科研等专业领域,Python都凭借简洁的语法和强大的扩展能力成为首选工具。随着数据规模的爆发式增长,传统的数据处理工具在性能和效率上逐渐面临挑战,而Polars的出现,为Python数据处理领域带来了突破性的解决方案。

一、Polars:重新定义数据处理的速度与效率

1. 核心用途与应用场景

Polars是一个基于Rust语言开发的高性能DataFrame库,专为大规模数据处理和分析设计。其核心目标是通过并行处理、向量化操作和内存优化,解决传统Python数据处理库在处理GB级以上数据时的性能瓶颈。目前Polars主要应用于以下场景:

  • 大数据分析:支持高效处理CSV、Parquet、JSON等格式的大规模数据集,处理速度可达Pandas的数倍甚至数十倍。
  • 流式数据处理:通过pl.LazyFrame接口实现延迟计算,适合构建数据管道和流式分析任务。
  • 内存优化场景:基于Apache Arrow内存格式,支持零拷贝操作和高效的内存管理,大幅降低内存占用。
  • 与Python生态集成:无缝兼容Pandas数据结构,可直接转换为NumPy数组或PySpark DataFrame,方便跨框架协作。

2. 工作原理与技术特性

Polars的高性能源于其底层设计的三大核心技术:

  • 多线程并行计算:利用现代CPU的多核特性,自动对数据处理任务进行并行化调度,默认启用所有可用核心。
  • 向量化操作:基于Rust实现的向量化运算引擎,避免Python解释器的循环开销,直接在编译层对整列数据进行操作。
  • Apache Arrow内存模型:采用列式存储格式,数据按列分区存储,支持高效的过滤、投影和聚合操作,同时兼容Arrow生态的其他工具(如Parquet、Feather)。

3. 优缺点对比与License

优势

  • 速度极快:在聚合、过滤、排序等常见操作中,性能显著优于Pandas,尤其适合处理10GB以上数据集。
  • 内存高效:通过零拷贝技术和延迟计算,内存占用通常比Pandas低30%-50%。
  • API友好:语法接近Pandas,支持链式操作和Lazy模式,代码可读性强。
  • 生态扩展:支持与Dask、PySpark集成,实现分布式计算。

局限性

  • 学习曲线:部分高级功能(如LazyFrame)需要理解延迟计算逻辑,对新手有一定门槛。
  • 生态成熟度:虽然核心功能完善,但在某些细分领域(如时间序列分析)的工具链不如Pandas丰富。

License类型:Polars采用宽松的MIT License,允许商业使用、修改和再分发,无需公开修改代码。

二、Polars快速上手:从安装到核心操作

1. 安装与环境配置

方式一:通过PyPI直接安装(推荐)

pip install polars  # 自动安装依赖项(如pyarrow)

方式二:安装额外功能(如Excel支持)

pip install polars[excel]  # 支持读取.xlsx文件
pip install polars[parquet]  # 优化Parquet文件读写性能

验证安装

import polars as pl
print(pl.__version__)  # 输出版本号,如"0.19.7"

2. 核心数据结构:Series与DataFrame

(1)Series:一维数据容器

# 创建Series
s = pl.Series("numbers", [1, 2, 3, None, 5])
print(s)
"""
shape: (5,)
Series: 'numbers' [i64]
[
    1
    2
    3
    null
    5
]
"""

# 数据类型推断与转换
s = s cast pl.Int32  # 显式转换为32位整数
s = s.to_float()    # 转换为浮点数

(2)DataFrame:二维表格数据

# 从字典创建DataFrame
df = pl.DataFrame({
    "姓名": ["Alice", "Bob", "Charlie"],
    "年龄": [25, 30, None],
    "分数": [85.5, 90.0, 78.5]
})
print(df)
"""
shape: (3, 3)
┌─────────┬──────┬──────┐
│ 姓名    ┆ 年龄 ┆ 分数 │
│ ---     ┆ ---  ┆ ---  │
│ str     ┆ i64  ┆ f64  │
╞═════════╪══════╪══════╡
│ Alice   ┆ 25   ┆ 85.5 │
├─────────┼──────┼──────┤
│ Bob     ┆ 30   ┆ 90.0 │
├─────────┼──────┼──────┤
│ Charlie ┆ null ┆ 78.5 │
└─────────┴──────┴──────┘
"""

3. 数据读取与写入

(1)读取CSV文件

# 读取普通CSV
df = pl.read_csv("sales_data.csv")

# 读取大文件时指定分块大小(chunked读取)
stream = pl.scan_csv("large_data.csv")  # 延迟加载,返回LazyFrame
df_chunked = stream.collect(streaming=True)  # 分块处理后合并

(2)写入Parquet文件(高效存储格式)

df.write_parquet("sales_data.parquet")
# 读取Parquet文件
df_parquet = pl.read_parquet("sales_data.parquet")

(3)与Pandas互操作

# Pandas转Polars
import pandas as pd
pd_df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
pl_df = pl.from_pandas(pd_df)

# Polars转Pandas
pd_df_converted = pl_df.to_pandas()

4. 数据清洗与转换

(1)处理缺失值

# 查看缺失值分布
print(df.is_null().sum())
"""
年龄    1
分数    0
dtype: int64
"""

# 删除包含缺失值的行
df_clean = df.drop_nulls()

# 用中位数填充缺失值
median_age = df["年龄"].median()
df_filled = df.fill_null({"年龄": median_age})

(2)数据过滤与筛选

# 筛选年龄大于25且分数大于80的记录
df_filtered = df.filter(
    (pl.col("年龄") > 25) & (pl.col("分数") > 80)
)

# 按条件替换值
df_updated = df.with_columns(
    pl.col("分数").apply(lambda x: x * 1.1 if x > 90 else x).alias("调整后分数")
)

(3)列操作与类型转换

# 添加计算列
df = df.with_columns(
    (pl.col("分数") * 0.8).alias("折后分数"),
    pl.lit("2023-10-01").cast(pl.Date).alias("日期")
)

# 重命名列
df = df.rename({"年龄": "Age", "分数": "Score"})

# 转换数据类型
df = df.with_columns(pl.col("Age").cast(pl.UInt8))  # 无符号8位整数

5. 数据聚合与分组统计

(1)基础聚合函数

# 计算分数的统计指标
stats = df["Score"].describe()
print(stats)
"""
shape: (1, 7)
┌─────────┬───────┐
│ variable ┆ value │
│ ---      ┆ ---   │
│ str      ┆ f64   │
╞═════════╪═══════╡
│ count    ┆ 3.0   │
├─────────┼───────┤
│ mean     ┆ 84.67 │
├─────────┼───────┤
│ std      ┆ 6.36  │
├─────────┼───────┤
│ min      ┆ 78.5  │
├─────────┼───────┤
│ 25%      ┆ 82.75 │
├─────────┼───────┤
│ 50%      ┆ 85.5  │
├─────────┼───────┤
│ 75%      ┆ 87.75 │
└─────────┴───────┘
"""

(2)分组聚合(GroupBy)

# 按年龄段分组统计平均分数
df.groupby_dynamic(
    "Age",
    every="10y",  # 每10年为一组
    include_boundaries=True
).agg(
    pl.col("Score").mean().alias("平均分数"),
    pl.col("姓名").count().alias("人数")
)
"""
输出示例:
shape: (2, 3)
┌────────────┬─────────┬──────┐
│ Age        ┆ 平均分数 ┆ 人数 │
│ ---        ┆ ---     ┆ ---  │
│ date_range ┆ f64     ┆ u32  │
╞════════════╪═════════╪══════╡
│ [25,35)    ┆ 87.75   ┆ 2    │
├────────────┼─────────┼──────┤
│ [35,45)    ┆ 78.5    ┆ 1    │
└────────────┴─────────┴──────┘
"""

(3)窗口函数

# 计算每个学生分数的排名(按班级分组)
df = df.with_columns(
    pl.col("Score").rank("dense", descending=True).over("班级").alias("班级排名")
)

6. 数据合并与重塑

(1)合并(Join)操作

# 左表:学生信息
students = pl.DataFrame({
    "学号": [1001, 1002, 1003],
    "姓名": ["Alice", "Bob", "Charlie"]
})

# 右表:成绩信息
scores = pl.DataFrame({
    "学号": [1001, 1002, 1004],
    "科目": ["数学", "英语", "数学"],
    "分数": [90, 85, 78]
})

# 内连接(Inner Join)
joined_df = students.join(scores, on="学号", how="inner")
print(joined_df)
"""
shape: (2, 4)
┌──────┬─────────┬──────┬──────┐
│ 学号 ┆ 姓名    ┆ 科目 ┆ 分数 │
│ ---  ┆ ---     ┆ ---  ┆ ---  │
│ i32  ┆ str     ┆ str  ┆ i32  │
╞══════╪═════════╪══════╪══════╡
│ 1001 ┆ Alice   ┆ 数学 ┆ 90   │
├──────┼─────────┼──────┼──────┤
│ 1002 ┆ Bob     ┆ 英语 ┆ 85   │
└──────┴─────────┴──────┴──────┘
"""

(2)透视表(Pivot Table)

# 将成绩表转换为科目-学生透视表
pivot_df = scores.pivot(
    index="学号",
    columns="科目",
    values="分数"
)
print(pivot_df)
"""
shape: (3, 3)
┌──────┬──────┬──────┐
│ 学号 ┆ 数学 ┆ 英语 │
│ ---  ┆ ---  ┆ ---  │
│ i32  ┆ i32  ┆ i32  │
╞══════╪══════╪══════╡
│ 1001 ┆ 90   ┆ null │
├──────┼──────┼──────┤
│ 1002 ┆ null ┆ 85   │
├──────┼──────┼──────┤
│ 1004 ┆ 78   ┆ null │
└──────┴──────┴──────┘
"""

7. 延迟计算(LazyFrame):构建高效数据管道

Polars的LazyFrame提供延迟计算机制,将多个数据处理操作编译为优化后的执行计划,避免中间结果的频繁生成,大幅提升复杂流程的处理效率。

示例:复杂数据处理流程

# 定义延迟计算流程
lazy_df = pl.scan_csv("sales_data.csv") \
    .filter(pl.col("销售额") > 1000) \
    .groupby("地区") \
    .agg([
        pl.col("销售额").sum().alias("总销售额"),
        pl.col("订单数").mean().alias("平均订单数")
    ]) \
    .sort("总销售额", descending=True)

# 执行计算并获取结果
final_df = lazy_df.collect()

优势:

  • 优化执行计划:Polars自动对多个操作进行合并和重排序,减少I/O和内存操作。
  • 流式处理支持:通过streaming=True参数支持分块处理超大文件,避免内存溢出。

三、实际案例:电商销售数据深度分析

场景描述

假设我们需要分析某电商平台的销售数据,数据包含以下字段:

  • 订单号:唯一标识每笔订单
  • 用户ID:购买用户的ID
  • 购买时间:订单生成时间
  • 商品类别:商品所属类别(如电子产品、服装、家居)
  • 销售额:订单金额(单位:元)
  • 促销类型:是否参与促销活动(0=未参与,1=参与)

数据预处理

1. 读取数据并查看基本信息

# 读取CSV文件,指定时间列解析格式
df = pl.read_csv(
    "ecommerce_sales.csv",
    dtypes={
        "购买时间": pl.Datetime,
        "销售额": pl.Float32
    }
)

# 查看前5行数据
print(df.head())

# 统计缺失值
print(df.is_null().sum())

2. 清洗数据:处理异常值与格式转换

# 过滤销售额为负数的记录(视为异常值)
df = df.filter(pl.col("销售额") > 0)

# 将促销类型转换为布尔类型
df = df.with_columns(pl.col("促销类型").cast(pl.Boolean))

# 提取日期中的年、月、日
df = df.with_columns([
    pl.col("购买时间").dt.year().alias("年份"),
    pl.col("购买时间").dt.month().alias("月份"),
    pl.col("购买时间").dt.day().alias("日期")
])

核心分析任务

1. 各季度销售额趋势分析

# 按季度分组,计算各季度总销售额
quarterly_sales = df.groupby(
    pl.col("购买时间").dt.quarter().alias("季度")
).agg(
    pl.col("销售额").sum().alias("总销售额"),
    pl.col("订单号").n_unique().alias("订单总数")
).sort("季度")

print(quarterly_sales)
"""
输出示例:
shape: (4, 3)
┌──────┬──────────┬──────────┐
│ 季度 ┆ 总销售额 ┆ 订单总数 │
│ ---  ┆ ---      ┆ ---      │
│ u32  ┆ f32      ┆ u32      │
╞══════╪══════════╪══════════╡
│ 1    ┆ 125000.0 ┆ 850      │
├──────┼──────────┼──────────┤
│ 2    ┆ 150000.0 ┆ 980      │
├──────┼──────────┼──────────┤
│ 3    ┆ 145000.0 ┆ 920      │
├──────┼──────────┼──────────┤
│ 4    ┆ 180000.0 ┆ 1100     │
└──────┴──────────┴──────────┘
"""

2. 促销活动对销售额的影响

# 按促销类型分组,计算平均销售额和订单量
promotion_analysis = df.groupby("促销类型").agg([
    pl.col("销售额").mean().alias("平均销售额"),
    pl.col("订单号").count().alias("订单量")
])

print(promotion_analysis)
"""
输出示例:
shape: (2, 3)
┌──────────┬──────────┬───────┐
│ 促销类型 ┆ 平均销售额 ┆ 订单量 │
│ ---      ┆ ---      ┆ ---   │
│ bool     ┆ f32      ┆ u32   │
╞══════════╪══════════╪═══════╡
│ false    ┆ 150.0    ┆ 2000  │
├──────────┼──────────┼───────┤
│ true     ┆ 220.0    ┆ 1500  │
└──────────┴──────────┴───────┘
"""

3. 最受欢迎的商品类别Top 5

# 按商品类别统计订单数,取前5名
top_categories = df.groupby("商品类别").agg(
    pl.col("订单号").count().alias("订单数")
).sort("订单数", descending=True).head(5)

print(top_categories)

数据可视化(基于Matplotlib)

import matplotlib.pyplot as plt

# 绘制季度销售额柱状图
plt.bar(quarterly_sales["季度"], quarterly_sales["总销售额"])
plt.title("各季度销售额趋势")
plt.xlabel("季度")
plt.ylabel("总销售额(元)")
plt.xticks(quarterly_sales["季度"])
plt.show()

四、资源获取与生态扩展

1. 官方资源链接

  • PyPI地址:https://pypi.org/project/polars/
  • GitHub仓库:https://github.com/pola-rs/polars
  • 官方文档:https://pola-rs.github.io/polars/py-polars/html/

2. 生态工具推荐

  • Polars+Dask:用于分布式数据处理,通过dask-polars库实现大规模数据集的并行计算。
  • Polars+PySpark:通过polars-spark桥接工具,实现与Spark DataFrame的无缝转换。
  • 可视化库:配合Matplotlib、Seaborn或Plotly,直接使用Polars DataFrame生成图表。

五、总结:Polars的价值与未来

Polars的出现标志着Python数据处理进入高性能时代。对于数据分析师和科学家而言,它不仅提供了比Pandas更高效的底层实现,还通过简洁的API降低了学习成本。无论是处理日常的中小规模数据集,还是应对GB级以上的大数据挑战,Polars都能在保持代码可读性的同时显著提升执行效率。随着开源社区的快速迭代(截至2023年,Polars已成为GitHub星标数超25k的热门项目),其生态短板正在迅速补齐,未来有望成为Python数据处理领域的主流工具之一。

实践建议:从简单的数据分析任务开始尝试Polars,例如替换Pandas完成日常的数据清洗和统计,逐步体会向量化操作和延迟计算的优势。对于大规模数据场景,建议优先使用LazyFrame构建处理流程,并结合Parquet等列式存储格式进一步提升性能。

# 最后用一个简单示例回顾Polars的核心用法
# 计算iris数据集的统计指标
import polars as pl

# 读取数据集(假设iris.csv存在)
iris = pl.read_csv("iris.csv")

# 按品种分组,计算花瓣长度的均值和标准差
result = iris.groupby("variety").agg([
    pl.col("petal_length").mean().alias("平均花瓣长度"),
    pl.col("petal_length").std().alias("花瓣长度标准差")
])

print(result)

关注我,每天分享一个实用的Python自动化工具。