pandas で primary key 使わずに複数のテーブル(DataFrame)を行方向に結合する際に,追加したいテーブルを逐次 concat する方法しか頭にありませんでした.
しかし,たまたま見かけたコードでは,「list に DataFrame を append したものを,1 回の concat で一気に結合する」という手法が取られていました.
その時は「ああ,それでもできるんだな」程度で考えていましたが,冷静に考えると「concat を複数回実行するのって処理時間的にまずいんじゃないか」と脳裏をよぎったため,今回検証しました.
また,個人的に最近は pandas よりも polars のほうが良く用いるため,pandas に加えて polars についても併せて検証しました.
結論
- 複数の DataFrame を結合する際は,concat 操作が少ないほうが処理が速い
- shape=(1,000,000, 4) の 20 個の DataFrame を行方向に結合する際に,1 つずつ DataFrame を 20 回 concat するよりも,20 個の DataFrame を一つの list の要素として定義したものを 1 回で concat したほうが,中央値基準で 10 倍速い
- pandas よりも polars のほうが速い
検証
論よりコードということで,以下検証です.
import gc
import sys
from time import perf_counter
import numpy as np
import pandas as pd
import polars as pl
from tqdm import tqdm
concat の速度比較で用いるライブラリの pandas / polars の他に,テーブルを定義するときに用いる numpy,及び,今回は処理に時間がかかるため「あれ,もしかして動いてない?」と心配にならないように進捗バーを表示させるため tqdm を外部ライブラリでインポートしています.
def get_process_time_intervals(
concat_mode: str = "many_times",
lib_name: str = "pandas",
num_try: int = 100,
num_concat: int = 20,
data_range: int = 1_000_000,
) -> list[float]:
assert concat_mode in ["many_times", "only_once"], ValueError(
f"concat_mode={concat_mode} は 'many_times' もしくは 'only_once' のみ使用可能です"
)
assert lib_name in ["pandas", "polars"], ValueError(
f"lib_name={lib_name} は 'pandas' もしくは 'polars' のみ使用可能です"
)
x = np.arange(0, data_range, 1, dtype=np.float64)
if lib_name == "pandas":
df_tmp = pd.DataFrame(
data={"col_0": x, "col_1": x, "col_2": x, "col_3": x, "col_4": x}
)
elif lib_name == "polars":
df_tmp = pl.DataFrame(
data={"col_0": x, "col_1": x, "col_2": x, "col_3": x, "col_4": x}
)
list_time: list[float] = []
for _ in tqdm(range(num_try)):
t_start = perf_counter()
if concat_mode == "many_times":
if lib_name == "pandas":
df = pd.DataFrame()
for _ in range(num_concat):
df = pd.concat(objs=[df, df_tmp], axis=0)
# df.reset_index(drop=True, inplace=True)
elif lib_name == "polars":
df = pl.DataFrame()
for _ in range(num_concat):
df = pl.concat(items=[df, df_tmp], how="vertical")
elif concat_mode == "only_once":
list_dfs: list = []
for _ in range(num_concat):
list_dfs.append(df_tmp)
if lib_name == "pandas":
df = pd.concat(objs=list_dfs, axis=0)
# df.reset_index(drop=True, inplace=True)
elif lib_name == "polars":
df = pl.concat(items=list_dfs, how="vertical")
t_elapsed = perf_counter() - t_start
assert df.__len__() == num_concat * data_range, ValueError
list_time.append(t_elapsed)
del df
gc.collect()
return list_time
“num_try” 回試行させて,各試行における処理時間(秒数)を list で返す関数です.
結合方法は “concat_mode”,使用するライブラリ名称は “lib_name”,1 回の試行で結合させる DataFrame の個数は “num_concat”,その DataFrame はテキトーに作成するものでそれの行数を “data_range” で制御します.
df_time = pd.DataFrame(
data={
"pandas_concat_list_dfs_only_once": get_process_time_intervals(
concat_mode="only_once", lib_name="pandas"
),
"pandas_concat_df_many_times": get_process_time_intervals(
concat_mode="many_times", lib_name="pandas"
),
"polars_concat_list_dfs_only_once": get_process_time_intervals(
concat_mode="only_once", lib_name="polars"
),
"polars_concat_df_many_times": get_process_time_intervals(
concat_mode="many_times", lib_name="polars"
),
}
)
各条件における試行回数分の処理時間について,”df_time” にてまとめています.
カラムはそれぞれ,
- “pandas_concat_list_dfs_only_once” ・・・・ pandas を用いて list に入った複数の DataFrame を 1 回で concat した場合
- “pandas_concat_df_many_times” ・・・・ pandas を用いて複数の DataFrame をその個数分逐次 concat した場合
- “polars_concat_list_dfs_only_once” ・・・・ polars を用いて list に入った複数の DataFrame を 1 回で concat した場合
- “polars_concat_df_many_times” ・・・・ polars を用いて複数の DataFrame をその個数分逐次 concat した場合
を表しています.
以下に,100 回分の結果を df.describe() メソッドで統計量として表示しています(.ipynb ファイルを vscode で実行).
df_desc = df_time.describe().T
df_desc.drop(columns=["count"], inplace=True)
df_desc["median"] = df_time.median()
df_desc.sort_values(by="median", inplace=True)
df_desc.style.bar(color="blue", align="zero")
結果としては,
- “polars_concat_list_dfs_only_once” ・・・・ polars を用いて list に入った複数の DataFrame を 1 回で concat した場合
- “pandas_concat_list_dfs_only_once” ・・・・ pandas を用いて list に入った複数の DataFrame を 1 回で concat した場合
- “polars_concat_df_many_times” ・・・・ polars を用いて複数の DataFrame をその個数分逐次 concat した場合
- “pandas_concat_df_many_times” ・・・・ pandas を用いて複数の DataFrame をその個数分逐次 concat した場合
の順で実行速度が速いことが示されました.
着目すべき点としては,
- concat は少ないほうが良く,concat 20 回行うよりも,20 個 DataFrame が入った list を 1 回で concat したほうが,中央値基準で 10 倍速い ・・・・ 大きい DataFrame の concat のコストが高いから?
- pandas よりも polars 使うほうが速い ・・・・ rust 実装だから? index が無いから?
というところです.
今後,concat は極力少なくする書き方を意識し,これからも polars を積極的に使っていこうと思います.
コメント