本記事では以下の内容を扱っています.
scipy.sparse
とpandas.SparseDtype
の演算速度・メモリ使用量比較
バージョン情報
バージョンの情報は以下のとおりです.
- Python 3.10.5
- NumPy 1.23.1
- pandas 1.4.3
- SciPy 1.8.1
概要
scipy.sparse
は疎行列を効率的に扱うためのクラスでSciPy
が提供しています.
またpandas
においても疎行列を扱えるpandas.SparseDtype
があります.
本記事ではこれらの演算速度やメモリ使用量を比較していきます.
演算速度とメモリ使用量の比較
本記事で使用するライブラリと関数は以下です.
# 本記事中で使用するライブラリ
import numpy as np
import pandas as pd
from scipy import sparse as sp
# dfとscipy.sparseのメモリ使用量を算出する関数(単位を付与する関数を含む)
def format_bytes(size):
power = 2**10
n = 0
power_labels = ['B', 'KB', 'MB', 'GB', 'TB']
while size > power and n <= len(power_labels):
size /= power
n += 1
return f'{size :.3f} {power_labels[n]}'
def get_size_of_df(df):
return format_bytes(df.memory_usage().sum())
def get_size_sparse_matrix(sp_matrix):
return format_bytes((sp_matrix.data.nbytes + sp_matrix.indices.nbytes + sp_matrix.indptr.nbytes))
以下のコードで演算速度,メモリ使用量を確認します.
行列内の0
割合を変えた1000x1000行列で演算速度とメモリ使用量を見ています.
出力結果は以下のとおりです.
(scipy.sparse
とpandas.SparseDtype
の結果がそれぞれ出ています)
- メモリ使用量と非
0
割合(density1) - 行列積速度
density_list = [0.0001, 0.001, 0.01, 0.1, 0.3, 0.5, 0.7, 0.9, 1.0]
# 疎行列の0でない成分の割合を変えてそれぞれ計算
for density in density_list:
scipy_sp = sp.rand(1000, 1000, density=density, format='csr', random_state=42)
pandas_sp = pd.DataFrame.sparse.from_spmatrix(scipy_sp)
print(f'scipy_sp: {get_size_sparse_matrix(scipy_sp)}, density={density}')
%timeit scipy_sp * scipy_sp
print(f'pandas_sp: {get_size_of_df(pandas_sp)}, density={density}')
%timeit pandas_sp @ pandas_sp
print('')
scipy_sp: 5.082 KB, density=0.0001 39.1 µs ± 80.7 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each) pandas_sp: 1.297 KB, density=0.0001 37.5 ms ± 1.45 ms per loop (mean ± std. dev. of 7 runs, 10 loops each) scipy_sp: 15.629 KB, density=0.001 44.1 µs ± 38.3 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each) pandas_sp: 11.844 KB, density=0.001 36.3 ms ± 1.66 ms per loop (mean ± std. dev. of 7 runs, 10 loops each) scipy_sp: 121.098 KB, density=0.01 560 µs ± 3.8 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each) pandas_sp: 117.312 KB, density=0.01 39.6 ms ± 3.46 ms per loop (mean ± std. dev. of 7 runs, 10 loops each) scipy_sp: 1.148 MB, density=0.1 24.6 ms ± 30.8 µs per loop (mean ± std. dev. of 7 runs, 10 loops each) pandas_sp: 1.145 MB, density=0.1 39.9 ms ± 1.74 ms per loop (mean ± std. dev. of 7 runs, 10 loops each) scipy_sp: 3.437 MB, density=0.3 103 ms ± 112 µs per loop (mean ± std. dev. of 7 runs, 10 loops each) pandas_sp: 3.433 MB, density=0.3 40.1 ms ± 1.75 ms per loop (mean ± std. dev. of 7 runs, 10 loops each) scipy_sp: 5.726 MB, density=0.5 257 ms ± 285 µs per loop (mean ± std. dev. of 7 runs, 1 loop each) pandas_sp: 5.722 MB, density=0.5 43.1 ms ± 2.12 ms per loop (mean ± std. dev. of 7 runs, 10 loops each) scipy_sp: 8.015 MB, density=0.7 487 ms ± 697 µs per loop (mean ± std. dev. of 7 runs, 1 loop each) pandas_sp: 8.011 MB, density=0.7 42.6 ms ± 2.47 ms per loop (mean ± std. dev. of 7 runs, 10 loops each) scipy_sp: 10.304 MB, density=0.9 787 ms ± 1.13 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) pandas_sp: 10.300 MB, density=0.9 49.7 ms ± 1.59 ms per loop (mean ± std. dev. of 7 runs, 10 loops each) scipy_sp: 11.448 MB, density=1.0 966 ms ± 1.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) pandas_sp: 11.444 MB, density=1.0 29.8 ms ± 1.1 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
for文では順に以下の演算をしています.
0
でない成分の割合が異なるscipy.sparse
を作成(CSR format)- 作成した
scipy.sparse
をpandasのpandas.SparseDtype
に変換 - それぞれの行列積にかかる時間を計測2
- メモリ使用量を算出する関数でそれぞれのメモリ使用量を確認し,densityと共に出力
演算速度の結果
0
割合が多い(=疎な行列の)場合はscipy.sparse
の方が高速.0
割合が少ない(=密な行列の)場合はpandas.SparseDtype
の方が高速.pandas.SparseDtype
は0
割合と速度に明確な関係がないが0
成分がないと演算速度が向上する.
メモリ使用量の結果
0
割合が少ない場合のメモリ効率はpandas.SparseDtype
の方がよい.0
成分が多い場合は両者に大きな差はない.
なお,密行列のDataFrame(データ型float64
)を作成し,演算速度とメモリ使用量を確認すると,以下のようになります.
csr = sp.rand(1000, 1000, density=1.0, format='csr', random_state=42)
normal_df = pd.DataFrame.sparse.from_spmatrix(csr).sparse.to_dense()
%timeit normal_df @ normal_df
print(get_size_of_df(normal_df))
20 ms ± 1.2 ms per loop (mean ± std. dev. of 7 runs, 10 loops each) 7.630 MB
密行列の演算速度・メモリ使用量は通常のDataFrameの方がよいですね.(当たり前ですが)
まとめ
疎行列を扱う際にはscipy.sparse
を使うと高速かつメモリ効率も向上するため,基本的にはscipy.sparse
を使うとよいでしょう.
一方でメモリ観点ではpandas.SparseDtype
の方がよい場合もあります.疎行列を扱う際にDataFrameをそのまま使用するケースはあまりないと思いますが,必要性が出てきた時の選択肢として知っておくと吉ですね.
疎行列と密行列を合わせて演算する場合
実際にデータを扱う際には密行列と疎行列を組み合わせることが多いと思います.
そのため,ここでは簡単な実験で演算速度とメモリ使用量を確認してみます.
準備
以下で前準備として密行列(1000x5),疎行列(1000x995)を作成します.
(後ほど行列を結合させて1000x1000行列を2パターン作成するためです)
# 乱数シードの固定
np.random.seed(42)
# DataFrameの作成
df = pd.DataFrame({
'col_1': np.ones(1000, dtype=np.int8),
'col_2': np.random.rand(1000),
'col_3': np.ones(1000, dtype=np.float32),
'col_4': np.random.rand(1000),
'col_5': np.random.rand(1000)
})
df.head(3)
col_1 | col_2 | col_3 | col_4 | col_5 | |
---|---|---|---|---|---|
0 | 1 | 0.374540 | 1.0 | 0.185133 | 0.261706 |
1 | 1 | 0.950714 | 1.0 | 0.541901 | 0.246979 |
2 | 1 | 0.731994 | 1.0 | 0.872946 | 0.906255 |
sparse_csr = sp.rand(1000, 995, density=0.005, format='csr', random_state=42)
sparse_csr
<1000x995 sparse matrix of type '<class 'numpy.float64'>' with 4975 stored elements in Compressed Sparse Row format>
route1: scipy.sparseで揃えた行列
route1ではscipy.sparse
で1000x1000行列を作成します.
# ルート1:両方をscipy.sparseに揃えて結合
# dfをsparse matrixに変換
df_sparse = sp.csr_matrix(df)
# 変換したdfとsparse_csrを結合
route_1 = sp.hstack((df_sparse, sparse_csr), format='csr')
route_1
<1000x1000 sparse matrix of type '<class 'numpy.float64'>' with 9975 stored elements in Compressed Sparse Row format>
route2: pandas.SparseDtypeで揃えた行列
route2ではpandas.SparseDtype
で1000x1000行列を作成します.
# ルート2:scipy.sparse を pandas.SparseDtype に変換
# sparse_csr を pandas sparse に変換
sparse_pds = pd.DataFrame.sparse.from_spmatrix(sparse_csr)
route_2 = pd.concat([df, sparse_pds], axis=1)
route_2.shape
(1000, 1000)
結果
演算速度とメモリ使用量はそれぞれ以下のとおりになりました.
print(f'route_1: {get_size_sparse_matrix(route_1)}')
%timeit route_1 * route_1
print('')
print(f'route_2: {get_size_of_df(route_2)}')
# 行列積の計算のため,index, columnsを揃える
route_2.index = route_2.columns
%timeit route_2 @ route_2
route_1: 120.805 KB 361 µs ± 560 ns per loop (mean ± std. dev. of 7 runs, 1,000 loops each) route_2: 86.746 KB 40 ms ± 1.66 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
演算速度はscipy.sparse
,メモリ使用量はpandas.SparseDtype
の方がいいですね.
今回は簡易的な実験ですので,実際のデータ型やデータ量によって異なることがあると思います.
しかし「まとめ」でも記載したとおり通常はscipy.sparse
を使う方がよさそうです.
ひとこと
scipy.sparse
とpandas.SparseDtype
で色々と実験してみましたが,より巨大でデータ型が混在する行列だと結果が異なってくるかもしれません.
何らかの参考になれば幸いです.