本記事では以下の内容を扱っています.
- sparse matrix(疎行列)と dense matrix(密行列)の概要
- sparse matrix をpandasとSciPyで生成・変換する方法
- sparse matrix のメモリ使用量比較
バージョン情報
バージョンの情報は以下のとおりです.
- Python 3.10.5
- NumPy 1.23.1
- pandas 1.4.3
- SciPy 1.8.1
sparse matrixとdense matrixの概要
sparse matrixは行列の成分がほとんど0
である行列のことをいいます.
sparse matrixを日本語で言うと「疎行列」です.
反対に成分の多くが0
でない行列はdense matrixといい,日本語は「密行列」です.
以下で疎行列と密行列を見てみましょう.
ライブラリのimport
はじめに本記事で使用するライブラリをimportしておきます.
# 本記事中で使用するライブラリ
import numpy as np
import pandas as pd
from scipy import sparse as sp
疎行列(sparse matrix)
以下のコードでDataFrameを作ります.
3×1000の行列のうち,ほとんどが0
で稀に1
(=データ)がある疎行列です.
np.random.seed(42) # 乱数シードの固定
# 乱数を生成して3×1000のdfを作成
sparse = np.random.binomial(n=1, p=0.1, size=3*1000)
sdf = pd.DataFrame(sparse.reshape(3, 1000))
sdf
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ... | 990 | 991 | 992 | 993 | 994 | 995 | 996 | 997 | 998 | 999 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
2 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
3 rows × 1000 columns
参考
今回は例示のために小さい行列で見ていますが,sparse matrixの列数は非常に多いことが普通です.
密行列(dense matrix)
続いて密行列を作成します.
3×1000の行列のうち,ほとんどが1
で稀に0
があるデータです.
ちなみに密行列の数値に決まりはありません.1
np.random.seed(42) # 乱数シードの固定
# 乱数を生成して3×1000のdfを作成
dense = np.random.binomial(n=1, p=0.9, size=3*1000)
ddf = pd.DataFrame(dense.reshape(3, 1000))
ddf
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ... | 990 | 991 | 992 | 993 | 994 | 995 | 996 | 997 | 998 | 999 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ... | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 0 | 1 |
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ... | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 1 | 1 |
2 | 1 | 1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ... | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
3 rows × 1000 columns
疎行列を効率的に扱う
疎行列は成分のほとんどが0
です.
そのため1
の場所だけを記憶しておくとメモリ効率がよくなります.
上記のような方法でデータの保持するにはscipy.sparse
やpandasのsparse型を使用します.
sparse matrixをpandasとSciPyで生成・変換する方法
SciPy
先ほど作成した各DataFrameについて,SciPyのcoo_matrix()
メソッドを使用してscipy.sparse
に変換してみます.
# sparse(sdf_sp) と dense(ddf_sp) を作成
sdf_sp = sp.coo_matrix(sdf.values)
ddf_sp = sp.coo_matrix(ddf.values)
# 例としてsdf_spを確認
sdf_sp
<3x1000 sparse matrix of type '<class 'numpy.int64'>' with 298 stored elements in COOrdinate format>
文字列が出力されていますが,これはscipy.sparse
の情報です.
オブジェクトのデータ型はscipy.sparse.coo.coo_matrix
です.
出力された情報の見方が知りたい方はこちらをクリック
出力結果の見方は以下のとおりです.
3x1000 sparse matrix of type '<class 'numpy.int64'>'
行列の大きさとデータ型を表しています.今回指定したDataFrameは3×1000でしたので一致していますね.なお,データ型はwith 298 stored elements in COOrdinate format
298 stored elements部分は保持している
numpy.int64
です.
1
の数です.今回のDataFrameには1
が298個あるということです.その後のCOOrdinate format
はsparse matrixの種類です.(sparse matrixにおけるデータの持ち方には色々な種類がありますが,今回はCOOという種類を使っています)
pandas
次にpandasのメソッドを使用してsparse matrixを生成します.
以下の2パターンを見てみましょう.
- DataFrameから
astype
メソッドでsparse型に変換する scipy.sparse
をpandasのsparse型に変換する
DataFrameからastypeメソッドでsparse型に変換する
DataFrameに対してastype
メソッドを使用しpandas.SparseDtype
を指定すればOKです.
# sparse(pd_sdf) と dense(pd_ddf) を作成
pd_sdf = sdf.astype(pd.SparseDtype("int64", 0))
pd_ddf = ddf.astype(pd.SparseDtype("int64", 0))
# 例としてpd_sdfを確認
pd_sdf
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ... | 990 | 991 | 992 | 993 | 994 | 995 | 996 | 997 | 998 | 999 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
2 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
3 rows × 1000 columns
オブジェクトのデータ型はpandas.core.frame.DataFrame
型です.2
dtypes
メソッドで各カラムのデータ型を見ると,Sparse[int64, 0]
となっており,データがSparse型で保持されていることがわかります.
print(type(pd_sdf), '\n')
print(pd_sdf.dtypes)
<class 'pandas.core.frame.DataFrame'> 0 Sparse[int64, 0] 1 Sparse[int64, 0] 2 Sparse[int64, 0] 3 Sparse[int64, 0] 4 Sparse[int64, 0] ... 995 Sparse[int64, 0] 996 Sparse[int64, 0] 997 Sparse[int64, 0] 998 Sparse[int64, 0] 999 Sparse[int64, 0] Length: 1000, dtype: object
scipy.sparseをpandasのsparse型に変換する
scipy.sparse
をpandasのsparse型に変換します.
pd.DataFrame.sparse.from_spmatrix
というメソッドで変換することができます.
pd_sdf2 = pd.DataFrame.sparse.from_spmatrix(sdf_sp)
pd_sdf2
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ... | 990 | 991 | 992 | 993 | 994 | 995 | 996 | 997 | 998 | 999 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
2 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
3 rows × 1000 columns
各カラムのデータ型はscipy.sparse
の型に依存します.
今回はscipy.sparse
の型がnumpy.int64
だったためSparse[int64, 0]
になっています.
print(type(pd_sdf2), '\n')
print(pd_sdf2.dtypes)
<class 'pandas.core.frame.DataFrame'> 0 Sparse[int64, 0] 1 Sparse[int64, 0] 2 Sparse[int64, 0] 3 Sparse[int64, 0] 4 Sparse[int64, 0] ... 995 Sparse[int64, 0] 996 Sparse[int64, 0] 997 Sparse[int64, 0] 998 Sparse[int64, 0] 999 Sparse[int64, 0] Length: 1000, dtype: object
sparse matrix のメモリ使用量
DataFrameとscipy.sparse
の各行列について,メモリ使用量を確認してみましょう.
以下の関数を使用してDataFrameとsparse matrixのメモリ使用量を見ることができます.
# DataFrame
def get_size_of_df(df):
return df.memory_usage().sum()
# COO matrix
def get_size_of_coo(coo):
return coo.data.nbytes + coo.row.nbytes + coo.col.nbytes
DataFrameの疎行列・密行列
DataFrameで作成した疎行列と密行列のメモリ使用量を比較すると,両者のメモリ使用量は同じです.
通常のDataFrameで扱うとどちらもメモリ使用量が同じになるので非効率です.
print(f'DataFrame 疎行列:{get_size_of_df(sdf)}')
print(f'DataFrame 密行列:{get_size_of_df(ddf)}')
DataFrame 疎行列:24128 DataFrame 密行列:24128
scipy.sparseの疎行列・密行列
続いてscipy.sparse
に変換した疎行列と密行列のメモリ使用量を比較します.
print(f'scipy.sparse 疎行列:{get_size_of_coo(sdf_sp)}')
print(f'scipy.sparse 密行列:{get_size_of_coo(ddf_sp)}')
scipy.sparse 疎行列:4768 scipy.sparse 密行列:43232
疎行列についてはメモリ使用量が非常に少なくなっています.
必要なデータだけを持っておくことでうまくメモリ使用量を節約できていますね.
一方で密行列をscipy.sparse
に変換すると逆にメモリの使用量が増加してしまいました.
密行列をscipy.sparse
に変換するメリットはないようです.
pandasの疎行列・密行列
最後にpandasのsparse型を見ていきましょう.
print(f'pandas sparse 疎行列:{get_size_of_df(pd_sdf)}')
print(f'pandas sparse 密行列:{get_size_of_df(pd_ddf)}')
pandas sparse 疎行列:3704 pandas sparse 密行列:32552
pandasのsparse型もscipy.sparse
と同じ傾向3になりました.
またscipy.sparse
とpandasのsparse型を見るとメモリ使用量に関してはpandas sparseの方がより少ないことがわかります.
まとめ
疎行列と密行列を合わせて扱う際にはデータ型やsparse matrix化の方法を検討するとメモリ効率をよくすることができますね.
- 疎行列はSciPyやpandasなどで効率的に扱うことができる.
- 密行列をsparse型に変換するとメモリ使用量が増加する.