Pandas風ライブラリのパフォーマンス比較 Part 2

# はじめに

[SMN a.i lab. Advent Calendar 2024](https://qiita.com/advent-calendar/2024/smn-ailab)12日目です。
[Pandas風ライブラリのパフォーマンス比較](https://qiita.com/tomohiro_ubukata/items/68a9032900b29c19a302) で残課題としていたライブラリの一部についても測定したため、Part 2です。
今回は以下のライブラリを比較対象に追加しました。

- Fireducks
- Vaex
- Modin
- Daft
- Ibis

以下のGPU用ライブラリも加えたかったのですが、Colabへのインストールがうまく行かなかったので見送りました。

- NVTabular
- cuStreamz

# Pandas風ライブラリたち

## Pandas

もっともポピュラーな表形式データ処理ライブラリです。

## PyArrow

Apache ArrowのPython用ライブラリです。
それほどPandas風ではありませんが、表形式のデータ型があります。
Arrow自体は列指向のデータフォーマット仕様であり、公式から多くの言語向けにライブラリが配布されています。
PandasからPyArrowの一部の機能を利用することもできます。

## PySpark

Apache SparkのPython用ライブラリです。
それほどPandas風ではありませんが、表形式のデータ型があります。
Spark自体は主に分散処理することを想定したソフトウェアですが、ローカルでも使えます。

## Polars

Pandasの代替として近年注目されているようです。
Pandasとある程度互換性があるようです。

## Dask

Pandasとの互換性の高さを謳っています。
分散処理にも対応しており、Sparkより高速だと主張しています。

## Fireducks

NECが長年に渡って磨き上げてきたスーパーコンピューターのエッセンスを注ぎ込んで開発されたそうです。
Pandasと完全な互換性があると主張しています。

## Vaex

最高のパフォーマンスのために"zero memory copy policy"と遅延評価を用いているそうです。

## Modin

すべてのコアをマルチスレッドで使い切ることで高速化を実現しているそうです。
Pandasと完全な互換性があると主張しています。
エンジンをRay、Dask、MPIから選ぶことができます。
今回はDaskを使いました。

## Daft

SQLとPython DataFrame interfacesの双方を「1級市民」として提供し、DuckDBのパフォーマンス、PolarsのPythonic UX、Sparkの拡張性を統合しているそうです。
分散処理もできます。

## Ibis

どちらかというとPyArrowやPySparkに近く、分散処理向けのようです。
各種DBMSを含む20以上ものバックエンドを選んで同じAPIで操作することができます。
今回はバックエンドとしてPolarsを使いました。

## cuDF

NVIDIA GPU用のライブラリです。
"cuDF pandas Accelerator Mode" を利用するとPandasと完全な互換性があると主張しています。

# Notebook

CPU
https://colab.research.google.com/drive/1E80y6CNsWKEHyCOk7M6ClNnKa8PWFt-I
GPU
https://colab.research.google.com/drive/1kQcmFGgO7i0KM5rYhRy5waRQgYOTfibw

# CPU

## 環境

Google Colaboratoryを使用します。

CPUの情報

```
!lscpu
```

```
Architecture:             x86_64
  CPU op-mode(s):         32-bit, 64-bit
  Address sizes:          46 bits physical, 48 bits virtual
  Byte Order:             Little Endian
CPU(s):                   2
  On-line CPU(s) list:    0,1
Vendor ID:                GenuineIntel
  Model name:             Intel(R) Xeon(R) CPU @ 2.20GHz
    CPU family:           6
    Model:                79
    Thread(s) per core:   2
    Core(s) per socket:   1
    Socket(s):            1
    Stepping:             0
    BogoMIPS:             4399.99
    Flags:                fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 cl
                          flush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc re
                          p_good nopl xtopology nonstop_tsc cpuid tsc_known_freq pni pclmulqdq ssse3
                           fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand
                           hypervisor lahf_lm abm 3dnowprefetch invpcid_single ssbd ibrs ibpb stibp 
                          fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm rdseed adx sm
                          ap xsaveopt arat md_clear arch_capabilities
Virtualization features:  
  Hypervisor vendor:      KVM
  Virtualization type:    full
Caches (sum of all):      
  L1d:                    32 KiB (1 instance)
  L1i:                    32 KiB (1 instance)
  L2:                     256 KiB (1 instance)
  L3:                     55 MiB (1 instance)
NUMA:                     
  NUMA node(s):           1
  NUMA node0 CPU(s):      0,1
Vulnerabilities:          
  Gather data sampling:   Not affected
  Itlb multihit:          Not affected
  L1tf:                   Mitigation; PTE Inversion
  Mds:                    Vulnerable; SMT Host state unknown
  Meltdown:               Vulnerable
  Mmio stale data:        Vulnerable
  Reg file data sampling: Not affected
  Retbleed:               Vulnerable
  Spec rstack overflow:   Not affected
  Spec store bypass:      Vulnerable
  Spectre v1:             Vulnerable: __user pointer sanitization and usercopy barriers only; no swa
                          pgs barriers
  Spectre v2:             Vulnerable; IBPB: disabled; STIBP: disabled; PBRSB-eIBRS: Not affected; BH
                          I: Vulnerable (Syscall hardening enabled)
  Srbds:                  Not affected
  Tsx async abort:        Vulnerable
```

ホストメモリの情報

```
!free -h
```

```
               total        used        free      shared  buff/cache   available
Mem:            12Gi       644Mi       8.1Gi       1.0Mi       3.9Gi        11Gi
Swap:             0B          0B          0B
```

## ライブラリのインストールとimport

```
!pip install pyarrow pyspark polars dask fireducks vaex modin[all] getdaft ibis-framework
```

```
import pandas as pd
import numpy as np

import pyarrow as pa
import pyarrow.compute as pc

import polars as pl

import dask.dataframe as dd

import fireducks.pandas as fd

import vaex

import modin.pandas as mpd

import daft

import ibis

from pyspark.sql import SparkSession
from pyspark.sql import functions as F
```

## データ準備

100列 * 50万行の表形式のデータをランダムに32ビット浮動小数点数で埋め、カテゴリ列A, B, C, Dを追加します。
あまり大きすぎるとMemoryErrorになるので気を付けてください。

```
np.random.seed(42)
n_rows = 500000
n_cols = 100

# 100列 * 50万行のデータをランダムに作成
# NVIDIA GPUはたぶん32ビット浮動小数点数の方が性能がいいため32ビットで作成
data = np.random.randn(n_rows, n_cols).astype(np.float32)
df = pd.DataFrame(data, columns=[f'col_{i}' for i in range(n_cols)])

# ある列を基準にグループ化するために、適当なカテゴリ列を追加
df['group'] = np.random.choice(['A', 'B', 'C', 'D'], size=n_rows)
```

## 問題

group列でグループ化し平均を取るという処理で比較しました。
Jupyterのtimeitマジックコマンドで10ループ * 7回実行して実行時間の平均を比較します。
また、今回はコピーの速度は主眼ではありませんが極端に差がついたのでついでに測りました。

## Pandas

Pandas

```
%%time
# 時間比較用にコピー
df_copied = df.copy()
```

```
CPU times: user 595 ms, sys: 40.9 ms, total: 636 ms
Wall time: 879 ms
```

```
%%timeit -r 7 -n 10
# group列でグループ化し平均を取る
grouped_mean = df.groupby('group').mean()
```

```
421 ms ± 185 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
```

## PyArrow

```
%%time
arrow_table = pa.Table.from_pandas(df)
```

```
CPU times: user 2.56 s, sys: 205 ms, total: 2.77 s
Wall time: 1.65 s
```

```
%%timeit -r 7 -n 10
# group列でグループ化し平均を取る
grouped_mean_pa = arrow_table.group_by('group').aggregate(
  [
    (f'col_{i}', "mean") for i in range(n_cols)
  ]
)
```

```
119 ms ± 31.7 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
```

3倍以上早くなり、標準偏差も小さくなりました。

## Polars

```
%%time
df_pl = pl.from_pandas(df)
```

```
CPU times: user 1.06 s, sys: 232 ms, total: 1.29 s
Wall time: 829 ms
```

```
%%timeit -r 7 -n 10
# group列でグループ化し平均を取る
grouped_mean_pl = df_pl.group_by('group').mean()
```

```
84.5 ms ± 18.1 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
```

Pandasの4倍くらいです。

## Dask

```
%%time
df_dask = dd.from_pandas(df, npartitions=4)
```

```
CPU times: user 1.08 s, sys: 71.7 ms, total: 1.15 s
Wall time: 1.17 s
```

```
%%timeit -r 7 -n 10
# group列でグループ化し平均を取る
grouped_mean_dask= df_dask.groupby('group').mean()
```

```
66.8 ms ± 3.87 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
```

Pandasの6倍くらいです。

## Fireducks

```
%%time
df_fd = fd.DataFrame(df)
```

```
CPU times: user 3.44 ms, sys: 0 ns, total: 3.44 ms
Wall time: 3.82 ms
```

```
%%timeit -r 7 -n 10
grouped_mean_fd = df_fd.groupby('group').mean()
```

```
165 µs ± 78.8 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
```

ここで圧倒的な結果が出ました。
Pandasの2550倍くらいです。

## Vaex

```
%%time
df_vaex = vaex.from_pandas(df)
```

```
CPU times: user 743 ms, sys: 47 ms, total: 790 ms
Wall time: 726 ms
```

```
%%timeit -r 7 -n 10
grouped_mean_vaex = df_vaex.groupby('group', agg='mean')
```

```
2.54 s ± 76.8 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
```

なんか遅いですね。

## Modin

```
!export MODIN_ENGINE=dask
```

```
%%time
df_modin = mpd.DataFrame(df)
```

```
CPU times: user 1.35 s, sys: 612 ms, total: 1.96 s
Wall time: 8.84 s
```

```
%%timeit -r 7 -n 10
grouped_mean_modin = df_modin.groupby('group').mean()
```

```
28.9 ms ± 6.28 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
```

Pandasの10倍くらいです。

## Daft

```
%%time
df_daft = daft.from_pandas(df)
```

```
CPU times: user 2.4 s, sys: 172 ms, total: 2.57 s
Wall time: 2.87 s
```

```
%%timeit -r 7 -n 10
grouped_mean_daft = df_daft.groupby('group').mean()
```

```
10.2 ms ± 1.06 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
```

Pandasの40倍くらいです。

## Ibis

```
# バックエンドをPolarsに設定
ibis.set_backend("polars")
client = ibis.pandas.connect({'df': df})
```

```
%%time
table = client.table('df')
```

```
CPU times: user 180 ms, sys: 34.8 ms, total: 215 ms
Wall time: 290 ms
```

```
grouped_mean_ibis = table.group_by('group').aggregate([table[col].mean().name(f'{col}_mean') for col in df.columns if col != 'group'])
```

```
%%timeit -r 7 -n 10
grouped_mean_ibis.execute()
```

```
17.8 s ± 2.42 s per loop (mean ± std. dev. of 7 runs, 10 loops each)
```

なんか遅いですね。

## PySpark

```
# Sparkセッションを作成
spark = SparkSession.builder.appName("tabular_data_processing_benchmark").getOrCreate()
```

```
%%time
df_spark = spark.createDataFrame(df)
```

```
CPU times: user 5min 57s, sys: 6.09 s, total: 6min 3s
Wall time: 6min 19s
```

今回はコピーは主眼ではありませんが、PySparkはなぜかコピーが異様に遅いです。

```
%%timeit -r 7 -n 10
# group列でグループ化し平均を取る
grouped_mean_spark = df_spark.groupBy("group").mean()
```

```
The slowest run took 5.57 times longer than the fastest. This could mean that an intermediate result is being cached.
122 ms ± 94.8 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
```

PyArrowと同じくらいです。

# GPU

## 環境

Google ColaboratoryでランタイムのタイプをT4 GPUに設定して使用します。

GPUの情報

```
!nvidia-smi
```

```
Mon Nov 11 05:09:59 2024       
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 535.104.05             Driver Version: 535.104.05   CUDA Version: 12.2     |
|-----------------------------------------+----------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |         Memory-Usage | GPU-Util  Compute M. |
|                                         |                      |               MIG M. |
|=========================================+======================+======================|
|   0  Tesla T4                       Off | 00000000:00:04.0 Off |                    0 |
| N/A   43C    P8               9W /  70W |      0MiB / 15360MiB |      0%      Default |
|                                         |                      |                  N/A |
+-----------------------------------------+----------------------+----------------------+
                                                                                         
+---------------------------------------------------------------------------------------+
| Processes:                                                                            |
|  GPU   GI   CI        PID   Type   Process name                            GPU Memory |
|        ID   ID                                                             Usage      |
|=======================================================================================|
|  No running processes found                                                           |
+---------------------------------------------------------------------------------------+
```

CUDA Toolkitのバージョン

```
!nvcc --version
```

## ライブラリのインストールとimport

```
!pip install --extra-index-url=https://pypi.nvidia.com cudf-cu12
```

```
import pandas as pd
import numpy as np

import cudf
```


## データ準備

CPUと同じです。

## 問題

CPUと同じです。

## cuDF

```
%%time
df_cudf = cudf.from_pandas(df)
```

```
CPU times: user 600 ms, sys: 224 ms, total: 824 ms
Wall time: 1.03 s
```

```
%%timeit -r 7 -n 10
# group列でグループ化し平均を取る
grouped_mean_cudf = df_cudf.groupby('group').mean()
```

```
71.9 ms ± 5.22 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
```

Pandasの5倍くらいでした。

# 結果

|  | 平均 |標準偏差
| ---- | ---- | ---- |
| Pandas | 421 ms | 185 ms |
| PyArrow | 119 ms | 31.7 ms |
| Polars | 84.5 ms | 18.1 ms |
| Dask | 66.8 ms | 3.87 ms |
| Fireducks | 165 µs | 78.8 µs |
| Vaex | 2.54 s | 76.8 ms |
| Modin | 28.9 ms | 6.28 ms |
| Daft | 10.2 ms | 1.06 ms |
| Ibis | 17.8 s | 2.42 s |
| PySpark | 122 ms | 94.8 ms |
| cuDF | 71.9 ms | 5.22 ms |

# まとめ

今回、FireDucksが圧倒的な性能で首位でした。
Pandasとの互換性も高いようなので、Pandasを高速化したい方は検討してみてはどうでしょうか。
VaexとIbisについては全然性能が出ませんでした。
Ibisはどちらかというと分散処理向けなのでローカルだと性能が出ないのかもしれませんが、Vaexはそうは見えないため原因はよくわかりません。
環境やデータのサイズ、問題によって結果は変わると思うので、実際に導入を検討する場合は自分の求める条件に合わせて測定してみてください。