疎なデータ構造#
pandas は、疎なデータを効率的に格納するためのデータ構造を提供します。これらは必ずしも一般的な「ほとんどが 0」という意味で疎であるとは限りません。むしろ、これらのオブジェクトは、特定の値(`NaN`/欠損値、ただし 0 を含む任意の値を選択可能)に一致するデータが省略される形で「圧縮」されていると見なすことができます。圧縮された値は、実際には配列に格納されません。
In [1]: arr = np.random.randn(10)
In [2]: arr[2:-2] = np.nan
In [3]: ts = pd.Series(pd.arrays.SparseArray(arr))
In [4]: ts
Out[4]:
0 0.469112
1 -0.282863
2 NaN
3 NaN
4 NaN
5 NaN
6 NaN
7 NaN
8 -0.861849
9 -2.104569
dtype: Sparse[float64, nan]
dtype である `Sparse[float64, nan]` に注目してください。`nan` は、配列内の `nan` である要素は実際には格納されず、`nan` でない要素のみが格納されることを意味します。これらの `nan` でない要素は、`float64` の dtype を持ちます。
疎なオブジェクトは、メモリ効率のために存在します。ほとんどが NA である大きな DataFrame があると仮定します。
In [5]: df = pd.DataFrame(np.random.randn(10000, 4))
In [6]: df.iloc[:9998] = np.nan
In [7]: sdf = df.astype(pd.SparseDtype("float", np.nan))
In [8]: sdf.head()
Out[8]:
0 1 2 3
0 NaN NaN NaN NaN
1 NaN NaN NaN NaN
2 NaN NaN NaN NaN
3 NaN NaN NaN NaN
4 NaN NaN NaN NaN
In [9]: sdf.dtypes
Out[9]:
0 Sparse[float64, nan]
1 Sparse[float64, nan]
2 Sparse[float64, nan]
3 Sparse[float64, nan]
dtype: object
In [10]: sdf.sparse.density
Out[10]: 0.0002
ご覧のとおり、密度(「圧縮」されていない値の割合)は非常に低いです。この疎なオブジェクトは、ディスク上(pickle 形式)および Python インタープリタ内で、はるかに少ないメモリを占有します。
In [11]: 'dense : {:0.2f} bytes'.format(df.memory_usage().sum() / 1e3)
Out[11]: 'dense : 320.13 bytes'
In [12]: 'sparse: {:0.2f} bytes'.format(sdf.memory_usage().sum() / 1e3)
Out[12]: 'sparse: 0.22 bytes'
機能的には、それらの動作は密な対応物とほぼ同じであるはずです。
SparseArray#
arrays.SparseArray は、疎な値の配列を格納するための ExtensionArray です(拡張配列の詳細については、dtypes を参照してください)。これは、`fill_value` とは異なる値のみを格納する 1 次元の ndarray 風オブジェクトです。
In [13]: arr = np.random.randn(10)
In [14]: arr[2:5] = np.nan
In [15]: arr[7:8] = np.nan
In [16]: sparr = pd.arrays.SparseArray(arr)
In [17]: sparr
Out[17]:
[-1.9556635297215477, -1.6588664275960427, nan, nan, nan, 1.1589328886422277, 0.14529711373305043, nan, 0.6060271905134522, 1.3342113401317768]
Fill: nan
IntIndex
Indices: array([0, 1, 5, 6, 8, 9], dtype=int32)
疎な配列は、`numpy.asarray()` を使用して通常の(密な)ndarray に変換できます。
In [18]: np.asarray(sparr)
Out[18]:
array([-1.9557, -1.6589, nan, nan, nan, 1.1589, 0.1453,
nan, 0.606 , 1.3342])
SparseDtype#
`SparseArray.dtype` プロパティには、2 つの情報が格納されます。
疎でない値の dtype
スカラーの fill 値
In [19]: sparr.dtype
Out[19]: Sparse[float64, nan]
SparseDtype は、dtype のみを渡すことで構築できます。
In [20]: pd.SparseDtype(np.dtype('datetime64[ns]'))
Out[20]: Sparse[datetime64[ns], numpy.datetime64('NaT')]
この場合、デフォルトの fill 値が使用されます(NumPy の dtype の場合、これは多くの場合、その dtype の「欠損」値です)。このデフォルトを上書きするには、明示的な fill 値を代わりに渡すことができます。
In [21]: pd.SparseDtype(np.dtype('datetime64[ns]'),
....: fill_value=pd.Timestamp('2017-01-01'))
....:
Out[21]: Sparse[datetime64[ns], Timestamp('2017-01-01 00:00:00')]
最後に、文字列エイリアス `'Sparse[dtype]'` は、多くの場所で疎な dtype を指定するために使用できます。
In [22]: pd.array([1, 0, 0, 2], dtype='Sparse[int]')
Out[22]:
[1, 0, 0, 2]
Fill: 0
IntIndex
Indices: array([0, 3], dtype=int32)
Sparse アクセサ#
pandas は、文字列データ用の .str、カテゴリカルデータ用の .cat、datetime 風データ用の .dt と同様に、.sparse アクセサを提供します。この名前空間は、疎なデータに特有の属性とメソッドを提供します。
In [23]: s = pd.Series([0, 0, 1, 2], dtype="Sparse[int]")
In [24]: s.sparse.density
Out[24]: 0.5
In [25]: s.sparse.fill_value
Out[25]: 0
このアクセサは、SparseDtype を持つデータと、scipy COO 行列から疎なデータを持つ Series を作成するための Series クラス自体でのみ利用可能です。
DataFrame にも .sparse アクセサが追加されました。詳細については、Sparse アクセサ を参照してください。
疎な計算#
NumPy の ufunc を arrays.SparseArray に適用すると、結果として arrays.SparseArray が得られます。
In [26]: arr = pd.arrays.SparseArray([1., np.nan, np.nan, -2., np.nan])
In [27]: np.abs(arr)
Out[27]:
[1.0, nan, nan, 2.0, nan]
Fill: nan
IntIndex
Indices: array([0, 3], dtype=int32)
ufunc は `fill_value` にも適用されます。これは、正しい密な結果を得るために必要です。
In [28]: arr = pd.arrays.SparseArray([1., -1, -1, -2., -1], fill_value=-1)
In [29]: np.abs(arr)
Out[29]:
[1, 1, 1, 2.0, 1]
Fill: 1
IntIndex
Indices: array([3], dtype=int32)
In [30]: np.abs(arr).to_dense()
Out[30]: array([1., 1., 1., 2., 1.])
変換
データを疎な形式から密な形式に変換するには、.sparse アクセサを使用します。
In [31]: sdf.sparse.to_dense()
Out[31]:
0 1 2 3
0 NaN NaN NaN NaN
1 NaN NaN NaN NaN
2 NaN NaN NaN NaN
3 NaN NaN NaN NaN
4 NaN NaN NaN NaN
... ... ... ... ...
9995 NaN NaN NaN NaN
9996 NaN NaN NaN NaN
9997 NaN NaN NaN NaN
9998 0.509184 -0.774928 -1.369894 -0.382141
9999 0.280249 -1.648493 1.490865 -0.890819
[10000 rows x 4 columns]
密な形式から疎な形式に変換するには、DataFrame.astype() を SparseDtype と一緒に使用します。
In [32]: dense = pd.DataFrame({"A": [1, 0, 0, 1]})
In [33]: dtype = pd.SparseDtype(int, fill_value=0)
In [34]: dense.astype(dtype)
Out[34]:
A
0 1
1 0
2 0
3 1
scipy.sparse との連携#
疎な行列から疎な値を持つ DataFrame を作成するには、DataFrame.sparse.from_spmatrix() を使用します。
In [35]: from scipy.sparse import csr_matrix
In [36]: arr = np.random.random(size=(1000, 5))
In [37]: arr[arr < .9] = 0
In [38]: sp_arr = csr_matrix(arr)
In [39]: sp_arr
Out[39]:
<Compressed Sparse Row sparse matrix of dtype 'float64'
with 517 stored elements and shape (1000, 5)>
In [40]: sdf = pd.DataFrame.sparse.from_spmatrix(sp_arr)
In [41]: sdf.head()
Out[41]:
0 1 2 3 4
0 0.95638 0 0 0 0
1 0 0 0 0 0
2 0 0 0 0 0
3 0 0 0 0 0
4 0.999552 0 0 0.956153 0
In [42]: sdf.dtypes
Out[42]:
0 Sparse[float64, 0]
1 Sparse[float64, 0]
2 Sparse[float64, 0]
3 Sparse[float64, 0]
4 Sparse[float64, 0]
dtype: object
すべての疎な形式がサポートされていますが、COOrdinate 形式ではない行列は、必要に応じてデータをコピーして変換されます。COO 形式の疎な SciPy 行列に戻すには、DataFrame.sparse.to_coo() メソッドを使用できます。
In [43]: sdf.sparse.to_coo()
Out[43]:
<COOrdinate sparse matrix of dtype 'float64'
with 517 stored elements and shape (1000, 5)>
Series.sparse.to_coo() は、MultiIndex でインデックス付けされた疎な値を持つ Series を scipy.sparse.coo_matrix に変換するために実装されています。
このメソッドは、2 レベル以上の MultiIndex を必要とします。
In [44]: s = pd.Series([3.0, np.nan, 1.0, 3.0, np.nan, np.nan])
In [45]: s.index = pd.MultiIndex.from_tuples(
....: [
....: (1, 2, "a", 0),
....: (1, 2, "a", 1),
....: (1, 1, "b", 0),
....: (1, 1, "b", 1),
....: (2, 1, "b", 0),
....: (2, 1, "b", 1),
....: ],
....: names=["A", "B", "C", "D"],
....: )
....:
In [46]: ss = s.astype('Sparse')
In [47]: ss
Out[47]:
A B C D
1 2 a 0 3.0
1 NaN
1 b 0 1.0
1 3.0
2 1 b 0 NaN
1 NaN
dtype: Sparse[float64, nan]
以下の例では、最初の MultiIndex レベルと 2 番目の MultiIndex レベルが行のラベルを定義し、3 番目と 4 番目のレベルが列のラベルを定義するように指定することで、Series を 2 次元の配列の疎な表現に変換します。また、最終的な疎な表現で列ラベルと行ラベルをソートするように指定します。
In [48]: A, rows, columns = ss.sparse.to_coo(
....: row_levels=["A", "B"], column_levels=["C", "D"], sort_labels=True
....: )
....:
In [49]: A
Out[49]:
<COOrdinate sparse matrix of dtype 'float64'
with 3 stored elements and shape (3, 4)>
In [50]: A.todense()
Out[50]:
matrix([[0., 0., 1., 3.],
[3., 0., 0., 0.],
[0., 0., 0., 0.]])
In [51]: rows
Out[51]: [(1, 1), (1, 2), (2, 1)]
In [52]: columns
Out[52]: [('a', 0), ('a', 1), ('b', 0), ('b', 1)]
異なる行および列ラベルを指定し(ソートしない)、異なる疎な行列が生成されます。
In [53]: A, rows, columns = ss.sparse.to_coo(
....: row_levels=["A", "B", "C"], column_levels=["D"], sort_labels=False
....: )
....:
In [54]: A
Out[54]:
<COOrdinate sparse matrix of dtype 'float64'
with 3 stored elements and shape (3, 2)>
In [55]: A.todense()
Out[55]:
matrix([[3., 0.],
[1., 3.],
[0., 0.]])
In [56]: rows
Out[56]: [(1, 2, 'a'), (1, 1, 'b'), (2, 1, 'b')]
In [57]: columns
Out[57]: [(0,), (1,)]
便利なメソッド Series.sparse.from_coo() は、scipy.sparse.coo_matrix から疎な値を持つ Series を作成するために実装されています。
In [58]: from scipy import sparse
In [59]: A = sparse.coo_matrix(([3.0, 1.0, 2.0], ([1, 0, 0], [0, 2, 3])), shape=(3, 4))
In [60]: A
Out[60]:
<COOrdinate sparse matrix of dtype 'float64'
with 3 stored elements and shape (3, 4)>
In [61]: A.todense()
Out[61]:
matrix([[0., 0., 1., 2.],
[3., 0., 0., 0.],
[0., 0., 0., 0.]])
デフォルトの動作(`dense_index=False` の場合)は、単に非 NULL エントリのみを含む Series を返します。
In [62]: ss = pd.Series.sparse.from_coo(A)
In [63]: ss
Out[63]:
0 2 1.0
3 2.0
1 0 3.0
dtype: Sparse[float64, nan]
`dense_index=True` を指定すると、行列の行と列の座標のデカルト積であるインデックスが生成されます。疎な行列が十分に大きい(かつ疎である)場合、これは(`dense_index=False` と比較して)かなりのメモリを消費することに注意してください。
In [64]: ss_dense = pd.Series.sparse.from_coo(A, dense_index=True)
In [65]: ss_dense
Out[65]:
1 0 3.0
2 NaN
3 NaN
0 0 NaN
2 1.0
3 2.0
0 NaN
2 1.0
3 2.0
dtype: Sparse[float64, nan]