pandas / polars における concat の速度比較



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")

結果としては,

  1. “polars_concat_list_dfs_only_once” ・・・・ polars を用いて list に入った複数の DataFrame を 1 回で concat した場合
  2. “pandas_concat_list_dfs_only_once” ・・・・ pandas を用いて list に入った複数の DataFrame を 1 回で concat した場合
  3. “polars_concat_df_many_times” ・・・・ polars を用いて複数の DataFrame をその個数分逐次 concat した場合
  4. “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 を積極的に使っていこうと思います.

コメント

タイトルとURLをコピーしました