Spec-Zone.ru › pandas 2

Разреженные структуры данных

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, dtype — float64.

Разреженные объекты предназначены для экономии памяти. Предположим, у вас есть большой DataFrame, состоящий преимущественно из NA:

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

Как видите, плотность (% значений, которые не были «сжаты») чрезвычайно низкая. Этот разреженный объект занимает гораздо меньше места на диске (в сериализованном виде) и в интерпретаторе 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). Это одномерный объект, подобный ndarray, который хранит только значения, отличные от fill_value:

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)

Разреженный массив можно преобразовать в обычный (плотный) ndarray с помощью numpy.asarray()

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 хранит две части информации

  1. dtype неразреженных значений

  2. Скалярное значение заполнения

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')]

В этом случае будет использоваться значение заполнения по умолчанию (для типов NumPy это часто значение «отсутствует» для данного dtype). Чтобы переопределить значение по умолчанию, можно указать явное значение заполнения

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 предоставляет аксессор .sparse, аналогичный .str для строковых данных, .cat для категориальных данных и .dt для данных, связанных с датой и временем. Это пространство имён предоставляет атрибуты и методы, предназначенные для работы с разреженными данными.

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, а также непосредственно для класса Series, чтобы создавать Series с разреженными данными из матрицы scipy COO.

Для DataFrame также добавлен аксессор .sparse. Подробнее см. в разделе Аксессор sparse.

Вычисления с разреженными данными

К arrays.SparseArray можно применять ufunc NumPy; в результате получится 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.sparse.from_spmatrix(), чтобы создать DataFrame с разреженными значениями из разреженной матрицы.

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 будут преобразованы с копированием данных по мере необходимости. Чтобы преобразовать данные обратно в разреженную матрицу SciPy в формате COO, можно использовать метод 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() преобразует Series с разреженными значениями, индексированный объектом MultiIndex, в scipy.sparse.coo_matrix.

Для работы метода требуется 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]

В примере ниже мы преобразуем Series в разреженное представление двумерного массива, указав, что первый и второй уровни MultiIndex определяют метки строк, а третий и четвёртый — метки столбцов. Также мы указываем, что метки столбцов и строк в итоговом разреженном представлении должны быть отсортированы.

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 с разреженными значениями из scipy.sparse.coo_matrix реализован удобный метод Series.sparse.from_coo().

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) просто возвращает 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]

© 2008–2011, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team
© 2011–2025, Open source contributors
Licensed under the 3-clause BSD License.
https://pandas.pydata.org/pandas-docs/version/2.3.0/user_guide/sparse.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API