【Python】pandas, SciPyで疎行列を扱う方法とメモリ使用量比較

本記事では以下の内容を扱っています.

  • 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です.

出力された情報の見方が知りたい方はこちらをクリック

出力結果の見方は以下のとおりです.

  1. 3x1000 sparse matrix of type '<class 'numpy.int64'>'
  2. 行列の大きさとデータ型を表しています.今回指定したDataFrameは3×1000でしたので一致していますね.なお,データ型はnumpy.int64です.
  3. with 298 stored elements in COOrdinate format
  4. 298 stored elements部分は保持している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型に変換するとメモリ使用量が増加する.

関連記事

SciPyとpandasの疎行列・密行列の演算速度について検討した記事は以下です.


  1. 密行列は01だけで構成されるものではないと言う意味です.なお,疎行列は通常01のみです. 

  2. 以下コードprint(type(pd_sdf), '\n')'\n'は見栄えのために改行を入れているだけです. 

  3. 疎行列をsparse型に変換するとメモリ使用量が非常に少なくなり,密行列をsparse型に変換すると逆にメモリ使用量が増加しています.