はじめに
Pandasはもう古い?2026年のPythonデータ分析ライブラリを整理してみたが、思ったより伸びてしまい、ちゃんとベンチくらいやらないと申し訳ないなと思いもう一つ記事を書いておきます。
Pandas、Polars、duckDB、NumPyで簡単にベンチマークテストをしました。
速度変わる条件いろいろあるようなので、あくまで簡易的なベンチです。
あと、numpy好きなので無理やりベンチとして入れました。
本ベンチマークは in-memory DataFrame を対象とした比較なので、
CSV / Parquet などのファイル読み込み性能は含めていないです。
PCについて
Polars、duckDBは、マルチコア対応のようなので、コア数だけ触れておきます。
10コアのPCでwindows環境のローカルで実験。
比較プログラム
特定の条件で、group byを使って集計する処理をベンチマークとしました。
変数の数をいじりづらくしてしまっていますがご容赦ください。
pythonの場合、gcなどで結構結果が変わってしまうようで、
10回やった中央値で対応する事にしました。
NumPyは、group byの関数ないので、自作していますが、データ数と$b$のデータ種類の二重ループとなっているため、基本的に遅い仕様となっています。
Pandasなどは、データ数1ループで処理するようなアルゴリズムになっているようです。
import time
import pandas as pd
import polars as pl
import duckdb
import numpy as np
print(f"pandas ver:{pd.__version__}")
print(f"polars ver:{pl.__version__}")
print(f"duckDB ver:{duckdb.__version__}")
print(f"numpy ver:{np.__version__}")
N_list = [500,1_000,1_000_000,10_000_000]
trial_time = 10
np.random.seed(0)
pandas_calc_time_list = []
polars_calc_time_list = []
polars_lazy_calc_time_list = []
duckDB_calc_time_list = []
numpy_calc_time_list = []
for N in N_list:
pandas_calc_time_tmp = []
polars_calc_time_tmp = []
polars_lazy_calc_time_tmp = []
duckDB_calc_time_tmp = []
numpy_calc_time_tmp = []
for i in range(trial_time):
# -----------------------------
# データ生成
# -----------------------------
a = np.random.rand(N)
b = np.random.randint(0, 100, N)
c = np.random.rand(N)
d = np.random.rand(N)
e = np.random.rand(N)
f = np.random.rand(N)
g = np.random.rand(N)
h = np.random.rand(N)
i = np.random.rand(N)
j = np.random.rand(N)
# Pandas
pdf = pd.DataFrame({
"a": a,
"b": b,
"c": c,
"d": d,
"e": e,
"f": f,
"g": g,
"h": h,
"i": i,
"j": j
})
# Polars
pldf = pl.DataFrame({
"a": a,
"b": b,
"c": c,
"d": d,
"e": e,
"f": f,
"g": g,
"h": h,
"i": i,
"j": j
})
# numpy
data = np.array([a,b,c,d,e,f,g,h,i,j]).T
# -----------------------------
# Pandas
# -----------------------------
start = time.time()
res_pd = (
pdf[pdf["a"] > 0.5]
.groupby("b")
.mean()
)
t_pd = time.time() - start
pandas_calc_time_tmp.append(t_pd)
#print("pandas:", t_pd)
# -----------------------------
# Polars
# -----------------------------
start = time.time()
res_pl = (
pldf
.filter(pl.col("a") > 0.5)
.group_by("b")
.mean()
)
t_pl = time.time() - start
polars_calc_time_tmp.append(t_pl)
#print("polars:", t_pl)
# -----------------------------
# Polars lazy
# -----------------------------
start = time.time()
res_pl = (
pldf.lazy()
.filter(pl.col("a") > 0.5)
.group_by("b")
.mean()
.collect()
)
t_pll = time.time() - start
polars_lazy_calc_time_tmp.append(t_pll)
#print("polars lazy:", t_pll)
# -----------------------------
# duckDB
# -----------------------------
start = time.time()
con = duckdb.connect()
result = con.sql("""
SELECT b,avg(a),avg(c),avg(d),avg(e),avg(f),avg(g),avg(h),avg(i),avg(j)
FROM pdf
WHERE a > 0.5
GROUP BY b
""").df()
t_ddb = time.time() - start
duckDB_calc_time_tmp.append(t_ddb)
#print("duckDB:", t_ddb)
# -----------------------------
# NumPy
# -----------------------------
start = time.time()
mask = a > 0.5
data2 = data[mask]
b2 = data2[:,1]
keys = np.unique(b2)
means = np.array([
data2[b2 == k].mean(axis=0)
for k in keys
])
t_np = time.time() - start
numpy_calc_time_tmp.append(t_np)
#print("numpy:", t_np)
pandas_calc_time_list.append(np.median(pandas_calc_time_tmp))
polars_calc_time_list.append(np.median(polars_calc_time_tmp))
polars_lazy_calc_time_list.append(np.median(polars_lazy_calc_time_tmp))
duckDB_calc_time_list.append(np.median(duckDB_calc_time_tmp))
numpy_calc_time_list.append(np.median(numpy_calc_time_tmp))
result_df = pd.DataFrame({'N':N_list,
'pandas':pandas_calc_time_list,
'polars':polars_calc_time_list,
'polars lazy':polars_lazy_calc_time_list,
'duckDB':duckDB_calc_time_list,
'numpy':numpy_calc_time_list})
print(result_df)
結果
pandas ver:2.3.3
polars ver:1.36.1
duckDB ver:1.4.4
numpy ver:2.0.2
N pandas polars polars lazy duckDB numpy
0 500 0.002393 0.001448 0.000816 0.023994 0.002203
1 1000 0.002281 0.001136 0.000740 0.023272 0.002505
2 1000000 0.044741 0.012358 0.010971 0.035462 0.441179
3 10000000 0.516562 0.080414 0.077823 0.110148 4.856230
1. Polarsが最も高速
大規模データでは
Polars が最も高速だった。
10,000,000行では
Polars ≈ Pandasの6倍高速
となった。
PolarsはRustで実装されており、内部でマルチスレッド処理が行われるため、大規模データ処理で強い。
2. Polars lazy は今回ほぼ同等
Polarsの lazy API は
クエリ最適化
を行う仕組みだが、今回の処理は
filter → groupby
という単純なパイプラインのため、
Polars eager ≒ Polars lazy
という結果になった。
3. DuckDB も高速だが若干遅い
DuckDB も高速で、10M行でも 0.11秒で処理できた。
ただし今回は Pandas DataFrame を DuckDB に渡して SQL 実行しているため、
データ変換コストが含まれている可能性がある。
条件によっては DuckDB の方が高速になるケースもある。
4. Pandasは安定だがスケールしにくい
Pandas は安定した速度だが、
データサイズ増加
↓
ほぼ線形に時間増加
するため、大規模データではPolarsに差を付けられた。
5. NumPyは今回の処理では不利
NumPy は配列演算では非常に高速だが、
今回のような
groupby処理
は直接サポートされていないため、
Pythonループを使う実装となり大きく遅くなった。
※ NumPyでもアルゴリズムを工夫すれば高速化できる可能性はあるが、
今回はシンプルな実装とした。
10M行では
NumPy ≈ Polarsの60倍
程度の差が出た。
なお、小規模データ(1000行以下)ではライブラリ間の差はほとんど見られなかった。
このサイズでは処理時間よりも Python のオーバーヘッドの影響が大きいと考えられる。
おわりに
前回の投稿が思ったより伸びたので、Pythonの表計算ライブラリのベンチマークテストをしました。
個人的には、全くサポートされていない使い方なのに、小さなデータだと同じくらいの速度にNumPyがなり、味が出せたと思っています。今回の結果を見ると、大きめのデータを扱う場合は
また、Polars を使ってみるのも良い選択肢だと思いました。
Polars は to_pandas() で簡単に Pandas DataFrame に変換できるので、
必要に応じて Pandas に戻すこともできそうです。
参考になれば幸いです。
おまけ
- 個人で特許を取得して記事を書いています
- 日常の疑問をシミュレーションして考察する記事書いてます