非零项不一致:pandas的稀疏数组与密集数组
原文
我的理解是,稀疏矩阵和密集矩阵存储的是相同的信息,只是内部格式不同。我有一个大部分值为False的稀疏矩阵:
(Pdb) x
[False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, ...]
Fill: False
IntIndex
Indices: array([ 73004, 73005, 73007, ..., 2629273, 2629274, 2629275],
dtype=int32)
检查nonzero的值是否与上面的表示一致:
(Pdb) x.nonzero()
(array([ 73004, 73005, 73007, ..., 2629273, 2629274, 2629275],
dtype=int32),)
这里是我看到有些意外的地方:
(Pdb) x[73004]
False
更多出乎意料的结果:
(Pdb) y = x.to_dense()
(Pdb) y.nonzero()
(array([ 456646, 658112, 692257, ..., 2608127, 2608154, 2608292]),)
显然,稀疏数组和密集数组并不仅在内部方式上不同,因为经过一次往返后你不会得到相同的结果:
(Pdb) z = pd.arrays.SparseArray(x.to_dense())
(Pdb) z
[False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, ...]
Fill: False
IntIndex
Indices: array([ 456646, 658112, 692257, ..., 2608127, 2608154, 2608292],
dtype=int32)
我在这里到底漏掉了什么?
编辑
下面的实现可能并非最简,但它用于重现一个类似上文x的稀疏数组:
import pandas as pd
sa = pd.arrays.SparseArray([1,-1,2,1,-2,4], fill_value=-2, dtype=float)
as_int = pd.DataFrame(sa)[0].apply(int)
x = ((as_int > 0) & (as_int & 1) > 0).values
我看到的是:
(Pdb) print(f"{pd.__version__}: {x.nonzero()}")
1.1.5: (array([0, 1, 2, 3, 5], dtype=int32),)
我在新版本的pandas中无法重现。在2.2.3中,产生x的那一行会导致以下警告:
FutureWarning: Allowing arbitrary scalar fill_value in SparseDtype is deprecated. In a future version, the fill_value must be a valid value for the SparseDtype.subtype.
>>> print(f"{pd.__version__}: {x.nonzero()}")
2.2.3: (array([0, 3], dtype=int32),)
在3.0.2上,产生x的那一行会报错:
ValueError: fill_value must be a valid value for the SparseDtype.subtype
解决方案
显然,稀疏数组和密集数组并不仅在内部格式上有差异,因为经过一次往返后得到的结果并不相同:
稀疏数组和密集数组之间有一个相当重要的区别:稀疏数组对零值可以有多种表示方式,而密集数组只有一种表示。
例如,在这段代码中,a和b拥有不同的内部表示,但语义相同。
import pandas as pd
from pandas._libs.sparse import IntIndex
sparse_index = IntIndex(3, np.array([0, 1, 2]))
a = pd.arrays.SparseArray([0, 1, 2], sparse_index=sparse_index)
sparse_index = IntIndex(3, np.array([1, 2]))
b = pd.arrays.SparseArray([1, 2], sparse_index=sparse_index)
print(a)
print(b)
输出:
[0, 1, 2]
Fill: 0
IntIndex
Indices: array([0, 1, 2], dtype=int32)
[0, 1, 2]
Fill: 0
IntIndex
Indices: array([1, 2], dtype=int32)
从阅读 SparseArray 这段代码,我不太清楚SparseArray是否允许在sp\_values数组中包含fill\_value值。 __init__ 会过滤掉冗余值,除非提供sparse\_index对象。在这种情况下,它避免重复过滤冗余值的工作,在某些情况下这可能会非常慢。
我对Pandas的稀疏实现并没有太多经验——我只对SciPy的稀疏实现有过贡献。就SciPy而言,在数组中允许出现“显式零”是完全允许的。对我来说,这里的问题是否在于你可以得到一个在sp\_values中包含fill\_value的 SparseArray,还是在于 SparseArray.nonzero() 包含的代码假设sp\_values不会有冗余值。
尝试使用SparseArray时,似乎并不难找到让sp\_values中出现fill\_value的方法,即使不滥用前面那个示例中的私有API。
例如,导致fill\_value的 binops可能会产生冗余值:
import numpy as np
import pandas as pd
a = pd.arrays.SparseArray([True, True, True, False])
b = pd.arrays.SparseArray([False, False, False, True])
print(a)
print(b)
result = a & b
print(result)
输出:
[True, True, True, False]
Fill: False
IntIndex
Indices: array([0, 1, 2], dtype=int32)
[False, False, False, True]
Fill: False
IntIndex
Indices: array([3], dtype=int32)
[False, False, False, False]
Fill: False
IntIndex
Indices: array([0, 1, 2, 3], dtype=int32)
在这个例子中,所有的False值都被显式存储。
另一方面,我确实找到了一个过去的Pandas问题,提示这种状态下的SparseArray不是一个有效的值。这意味着你在Pandas中用来获取这个SparseArray的任何方法,某种程度上都存在一个错误。
如果你能找出导致包含sp\_value条目等于fill\_value的 SparseArray的 Pandas操作序列,我建议你联系Pandas开发者。