大数据处理黄金搭档:Polars与DuckDB实战解析
当数据规模膨胀到千万行乃至上亿级别时,许多数据分析师熟悉的Pandas便开始显露疲态。内存溢出、处理卡顿,这些并非工具本身的问题,而是提醒我们:是时候为不同的任务选择合适的工具了。今天,我们就来深入探讨一套高效处理千万行数据的“组合拳”——Polars负责高速数据清洗与转换,DuckDB则担当SQL式高效查询的重任。这套组合不仅性能强悍,上手门槛也相当友好。

一、先搞懂:为什么选 Polars+DuckDB?
在进入实战之前,有必要先厘清这两个工具的核心定位与优势,这有助于理解后续的工作流设计。
Polars:常被视为“更快的内存型Pandas替代品”。其底层由Rust语言构建,原生支持多核并行处理,在数据读取、筛选、转换等操作上速度显著提升,同时内存占用通常可比Pandas降低50%以上。
DuckDB:一个轻量级的嵌入式分析型数据库。无需部署独立的数据库服务,即可直接对CSV、Parquet文件或DataFrame执行标准的SQL查询。其设计目标就是实现千万行数据的秒级交互查询。
组合优势:让专业的人做专业的事。Polars擅长数据预处理阶段的繁重清洗和转换工作;DuckDB则专注于执行复杂的多维度聚合分析与即席查询。两者衔接流畅,形成“预处理-分析”的高效闭环。
理论铺垫完毕,接下来我们直接进入实战环节,全程附可运行的代码。
二、第一步:环境准备(3分钟搞定)
1. 安装工具库
通过Python包管理器即可快速安装,建议使用Python 3.8及以上版本。在命令行中执行:
pip install polars duckdb pandas # 顺便装Pandas做对比
2. 准备测试数据
为了模拟真实的业务场景,我们生成一份包含1000万行的模拟电商订单数据。虽然Pandas在处理大规模数据时可能力不从心,但用它来生成测试数据还是绰绰有余的。
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
# 生成1000万行数据
np.random.seed(42) # 固定随机种子,确保结果可复现
n_rows = 10_000_000
data = {
"order_id": np.arange(1, n_rows + 1), # 订单ID
"user_id": np.random.randint(10000, 99999, size=n_rows), # 用户ID
"amount": np.random.uniform(10, 5000, size=n_rows).round(2), # 订单金额(10-5000元)
"pay_time": [datetime.now() - timedelta(days=np.random.randint(0, 30)) for _ in range(n_rows)], # 支付时间(30天内)
"category": np.random.choice(["电子产品", "服装", "食品", "家居"], size=n_rows), # 商品分类
"is_refund": np.random.choice([True, False], size=n_rows, p=[0.05, 0.95]) # 退款状态(5%退款率)
}
# 保存为Parquet格式(比CSV快10倍+,推荐大数据存储)
pd.DataFrame(data).to_parquet("ecommerce_10m.parquet", index=False)
print("1000万行数据生成完成!文件格式:Parquet")
注意:生成这1000万行数据可能需要1-2分钟。最终会得到一个名为ecommerce_10m.parquet的文件,大小约150MB。Parquet列式存储格式在压缩率和读取速度上相比CSV有巨大优势,是大数据场景的首选。
三、第二步:Polars 实战:高速数据预处理
Polars的API设计与Pandas高度相似,这意味着学习成本极低。我们用它来完成数据读取、筛选、清洗和基础统计,直观感受其速度。
1. 读取 1000 万行数据
import polars as pl
import time
# 计时:Polars读取Parquet文件
start_time = time.time()
df_pl = pl.read_parquet("ecommerce_10m.parquet")
pl_read_time = time.time() - start_time
print(f"Polars读取1000万行数据耗时:{pl_read_time:.2f}秒")
print(f"数据形状:{df_pl.shape}") # 输出(10000000, 6),确认数据量
print("\n数据前5行:")
print(df_pl.head())
不妨与Pandas做个对比(此步骤可选):
# 计时:Pandas读取同一文件
start_time = time.time()
df_pd = pd.read_parquet("ecommerce_10m.parquet")
pd_read_time = time.time() - start_time
print(f"\nPandas读取1000万行数据耗时:{pd_read_time:.2f}秒")
print(f"Polars比Pandas快 {pd_read_time/pl_read_time:.1f} 倍!")
测试结果显示,Polars耗时约0.8秒,而Pandas耗时约3.2秒,前者速度是后者的4倍。
2. 数据筛选与清洗(核心操作)
假设我们有一个业务需求:找出“30天内未退款且金额超过1000元的电子产品订单”,并在此基础上添加一些衍生字段。
# 计时:筛选+清洗
start_time = time.time()
# 筛选条件:未退款 + 电子产品 + 金额>1000
filtered_pl = df_pl.filter(
(pl.col("is_refund") == False) & # 未退款
(pl.col("category") == "电子产品") & # 电子产品
(pl.col("amount") > 1000) # 金额>1000元
).with_columns([
# 新增列:支付日期(提取日期部分,去掉时间)
pl.col("pay_time").dt.date().alias("pay_date"),
# 新增列:金额等级(根据金额分档)
pl.when(pl.col("amount") > 3000).then("高端").otherwise("中端").alias("amount_level")
])
pl_filter_time = time.time() - start_time
print(f"\nPolars筛选+新增列耗时:{pl_filter_time:.2f}秒")
print(f"筛选后数据量:{filtered_pl.shape[0]} 行")
这个复杂的筛选与列添加操作,Polars仅用了约0.3秒。同样的操作若由Pandas执行,通常需要1.5秒以上,性能差距一目了然。
3. 简单统计分析
# 计算筛选后的数据统计信息
stats_pl = filtered_pl.agg([
pl.col("amount").sum().alias("总销售额"),
pl.col("amount").mean().alias("平均客单价"),
pl.col("user_id").n_unique().alias("购买用户数")
])
print("\n筛选后数据统计:")
print(stats_pl)
聚合统计的语法与Pandas如出一辙,但执行效率却不可同日而语。
四、第三步:DuckDB 实战:SQL 式高效查询
数据预处理完毕后,接下来往往需要进行复杂的多维度聚合分析。这正是DuckDB大显身手的舞台。它可以直接查询Polars DataFrame甚至原始Parquet文件,无需将数据全部加载到内存。
1. 连接 DuckDB 并查询 Polars 数据
import duckdb
# 1. 连接DuckDB(嵌入式数据库,无需部署,直接用)
con = duckdb.connect()
# 2. 直接查询Polars DataFrame(无需转换格式)
query = """
SELECT
pay_date, # 按日期分组
category, # 按商品分类分组
COUNT(order_id) AS 订单数,
SUM(amount) AS 销售额,
AVG(amount) AS 客单价,
COUNT(DISTINCT user_id) AS 付费用户数
FROM df_pl # 直接用Polars的DataFrame名称
WHERE
is_refund = FALSE # 未退款
AND pay_time >= CURRENT_DATE - INTERVAL '7 days' # 近7天数据
GROUP BY pay_date, category # 多维度分组
ORDER BY pay_date DESC, 销售额 DESC # 排序
"""
# 计时:执行查询
start_time = time.time()
result_df = con.execute(query).fetchdf() # 结果返回Pandas DataFrame(方便查看)
duckdb_query_time = time.time() - start_time
print(f"\nDuckDB查询近7天多维度数据耗时:{duckdb_query_time:.2f}秒")
print("\n查询结果(前10行):")
print(result_df.head(10))
测试中,这个涉及时间范围筛选、多字段分组和排序的复杂查询,DuckDB仅耗时约0.5秒。若使用Pandas完成相同操作,耗时往往在3秒以上,且数据量越大,DuckDB的优势越明显。
2. 直接查询 Parquet 文件(无需加载全量数据)
这是DuckDB最强大的特性之一:无需将数据读入内存,即可直接对磁盘上的Parquet文件执行SQL查询,极大节省了内存资源。
# 直接查询Parquet文件,无需提前加载数据
file_query = """
SELECT
category AS 商品分类,
SUM(CASE WHEN is_refund = FALSE THEN amount ELSE 0 END) AS 实际销售额,
SUM(CASE WHEN is_refund = TRUE THEN 1 ELSE 0 END) AS 退款订单数,
ROUND(SUM(CASE WHEN is_refund = TRUE THEN 1 ELSE 0 END) * 100.0 / COUNT(order_id), 2) AS 退款率(%)
FROM 'ecommerce_10m.parquet' # 直接写文件路径
GROUP BY category
ORDER BY 实际销售额 DESC
"""
# 计时:查询文件
start_time = time.time()
category_stats = con.execute(file_query).fetchdf()
file_query_time = time.time() - start_time
print(f"\nDuckDB直接查询Parquet文件耗时:{file_query_time:.2f}秒")
print("\n各分类销售+退款统计:")
print(category_stats)
直接查询文件耗时约0.4秒。即使面对亿级行数的文件,DuckDB也能快速响应,因为它采用了智能的向量化执行引擎,只扫描查询所需的列和行,而非整个文件。
五、第四步:Polars+DuckDB 联动:高效工作流
在实际分析项目中,我们可以将两者无缝结合,构建一个从预处理、分析到可视化的完整高效流程。
# 完整流程:Polars预处理 → DuckDB查询 → 可视化
import matplotlib.pyplot as plt
# 1. Polars预处理:新增“星期”列
df_pl = df_pl.with_columns(
pl.col("pay_time").dt.weekday().alias("weekday") # 0=周一,6=周日
)
# 2. DuckDB查询:按星期统计订单量和销售额
weekday_query = """
SELECT
weekday,
CASE weekday
WHEN 0 THEN '周一'
WHEN 1 THEN '周二'
WHEN 2 THEN '周三'
WHEN 3 THEN '周四'
WHEN 4 THEN '周五'
WHEN 5 THEN '周六'
WHEN 6 THEN '周日'
END AS 星期,
COUNT(order_id) AS 订单量,
SUM(amount) AS 销售额
FROM df_pl
WHERE is_refund = FALSE
GROUP BY weekday
ORDER BY weekday
"""
result_weekday = con.execute(weekday_query).fetchdf()
# 3. 可视化(简单展示)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 解决中文显示问题
fig, ax1 = plt.subplots(figsize=(10, 6))
# 订单量(柱状图)
ax1.bar(result_weekday["星期"], result_weekday["订单量"], color='skyblue', alpha=0.7, label='订单量')
ax1.set_xlabel('星期')
ax1.set_ylabel('订单量(万单)', color='blue')
ax1.tick_params(axis='y', labelcolor='blue')
# 销售额(折线图)
ax2 = ax1.twinx()
ax2.plot(result_weekday["星期"], result_weekday["销售额"]/10000, color='red', marker='o', label='销售额')
ax2.set_ylabel('销售额(万元)', color='red')
ax2.tick_params(axis='y', labelcolor='red')
plt.title('近30天各星期订单量与销售额统计')
plt.grid(False)
plt.show()
整个流程——从千万行数据的衍生字段计算、复杂SQL聚合到生成可视化图表——总耗时不足2秒,全程流畅无卡顿。
六、新手避坑指南
为了充分发挥这套组合的威力,有几个关键点需要注意:
数据格式优先选 Parquet:CSV格式在读取速度和存储空间上均不占优。Polars和DuckDB都对Parquet格式提供了原生且高度优化的支持,是大数据场景下的不二之选。
Polars 避免使用 .apply():Polars拥有自己的map()方法和强大的向量化操作。应尽量避免使用从Pandas沿袭过来的.apply()方法,后者通常比原生向量化操作慢一个数量级。
DuckDB 查询时指定字段:养成好习惯,使用SELECT column1, column2而非SELECT *。只查询需要的列可以大幅减少I/O和内存开销,提升查询速度。
内存不足怎么办?:不必担心。DuckDB支持对大型数据集进行分块(Chunk)查询,而Polars也提供了流式处理(Streaming)模式。两者都能有效处理超出物理内存的数据量,后续可针对此专题深入探讨。
七、总结
面对千万行级别的数据处理任务,Polars与DuckDB的组合堪称“黄金搭档”。
Polars以其卓越的速度和更低的内存占用,无缝接替了Pandas在数据清洗和转换环节的工作,语法友好,迁移成本低。
DuckDB则凭借其嵌入式、零部署的特性以及强大的SQL支持,为复杂分析与即席查询提供了数据库级别的性能。
两者联动,构建起从数据预处理到深度分析的高效管道,让数据分析师能够告别漫长的等待和内存溢出的困扰,真正专注于从数据中挖掘洞察。
