Редко встречающиеся структуры данных
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]
Обратите внимание на тип данных, Sparse[float64, nan]. nan означает, что элементы в массиве, которые являются nan, фактически не хранятся, сохраняются только элементы, отличные от nan. Эти элементы, отличные от nan, имеют тип данных 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 для хранения массива разреженных значений (см. Типы данных для получения дополнительной информации об расширенных массивах). Это одномерный объект, похожий на 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 хранит две части информации:
Тип данных для значений, не являющихся разреженными.
Скалярное значение заполнения.
In [19]: sparr.dtype
Out[19]: Sparse[float64, nan]
Можно создать SparseDtype, передав только тип данных.
In [20]: pd.SparseDtype(np.dtype('datetime64[ns]'))
Out[20]: Sparse[datetime64[ns], numpy.datetime64('NaT')]
В этом случае будет использоваться значение заполнения по умолчанию (для типов данных NumPy это часто «пропущенное» значение для данного типа данных). Чтобы переопределить это значение по умолчанию, вместо этого можно указать явное значение заполнения.
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]' можно использовать для указания разреженного типа данных во многих местах.
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)
Аксессор для разреженных данных
pandas предоставляет аксессор .sparse, аналогичный аксессорам .str для строковых данных, .cat для категориальных данных и .dt для данных типа datetime. Этот пространство имен предоставляет атрибуты и методы, специфичные для разреженных данных.
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.
Добавлен в версии 0.25.0.
Аксессор .sparse также добавлен для DataFrame. Подробнее см. Аксессор для разреженных данных.
Вычисления с разреженными данными
Вы можете применять NumPy ufuncs к 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.])
Миграция
Примечание
SparseSeries и SparseDataFrame были удалены в pandas 1.0.0. Это руководство по миграции предназначено для помощи в переходе с предыдущих версий.
В более ранних версиях pandas классы SparseSeries и SparseDataFrame (документированные ниже) были предпочтительным способом работы с разреженными данными. С появлением расширенных массивов эти подклассы больше не нужны. Их цель лучше достигается с помощью обычного Series или DataFrame со значениями разреженных данных.
Примечание
Использование Series или DataFrame со значениями разреженных данных, а не SparseSeries или SparseDataFrame, не приводит к снижению производительности или памяти.
Этот раздел содержит руководство по миграции вашего кода в новый стиль. Напоминаем, что вы можете использовать модуль Python warnings для управления предупреждениями. Но мы рекомендуем изменить свой код, а не игнорировать предупреждение.
Создание
Из похожих на массив данных используйте обычные конструкторы Series или DataFrame со значениями arrays.SparseArray.
# Previous way
>>> pd.SparseDataFrame({"A": [0, 1]})
# New way
In [31]: pd.DataFrame({"A": pd.arrays.SparseArray([0, 1])})
Out[31]:
A
0 0
1 1
Из матрицы SciPy с разреженными данными используйте DataFrame.sparse.from_spmatrix().
# Previous way
>>> from scipy import sparse
>>> mat = sparse.eye(3)
>>> df = pd.SparseDataFrame(mat, columns=['A', 'B', 'C'])
# New way
In [32]: from scipy import sparse
In [33]: mat = sparse.eye(3)
In [34]: df = pd.DataFrame.sparse.from_spmatrix(mat, columns=['A', 'B', 'C'])
In [35]: df.dtypes
Out[35]:
A Sparse[float64, 0]
B Sparse[float64, 0]
C Sparse[float64, 0]
dtype: object
Преобразование
Из разреженных данных в плотные используйте аксессоры .sparse
In [36]: df.sparse.to_dense()
Out[36]:
A B C
0 1.0 0.0 0.0
1 0.0 1.0 0.0
2 0.0 0.0 1.0
In [37]: df.sparse.to_coo()
Out[37]:
<3x3 sparse matrix of type '<class 'numpy.float64'>'
with 3 stored elements in COOrdinate format>
Из плотных данных в разреженные используйте DataFrame.astype() с SparseDtype.
In [38]: dense = pd.DataFrame({"A": [1, 0, 0, 1]})
In [39]: dtype = pd.SparseDtype(int, fill_value=0)
In [40]: dense.astype(dtype)
Out[40]:
A
0 1
1 0
2 0
3 1
Свойства разреженных данных
Свойства, специфичные для разреженных данных, например, density, доступны через аксессор .sparse
In [41]: df.sparse.density
Out[41]: 0.3333333333333333
Общие различия
В SparseDataFrame, все столбцы были разреженными. DataFrame может содержать смесь разреженных и плотных столбцов. В результате присваивание новых столбцов с разреженными значениями DataFrame не будет автоматически преобразовывать входные данные в разреженный вид.
# Previous Way
>>> df = pd.SparseDataFrame({"A": [0, 1]})
>>> df['B'] = [0, 0] # implicitly becomes Sparse
>>> df['B'].dtype
Sparse[int64, nan]
Вместо этого вам нужно убедиться, что присваиваемые значения являются разреженными.
In [42]: df = pd.DataFrame({"A": pd.arrays.SparseArray([0, 1])})
In [43]: df['B'] = [0, 0] # remains dense
In [44]: df['B'].dtype
Out[44]: dtype('int64')
In [45]: df['B'] = pd.arrays.SparseArray([0, 0])
In [46]: df['B'].dtype
Out[46]: Sparse[int64, 0]
Атрибуты SparseDataFrame.default_kind и SparseDataFrame.default_fill_value не имеют замены.
Взаимодействие с scipy.sparse
Используйте DataFrame.sparse.from_spmatrix(), чтобы создать DataFrame со значениями разреженной матрицы.
Новое в версии 0.25.0.
In [47]: from scipy.sparse import csr_matrix
In [48]: arr = np.random.random(size=(1000, 5))
In [49]: arr[arr < .9] = 0
In [50]: sp_arr = csr_matrix(arr)
In [51]: sp_arr
Out[51]:
<1000x5 sparse matrix of type '<class 'numpy.float64'>'
with 517 stored elements in Compressed Sparse Row format>
In [52]: sdf = pd.DataFrame.sparse.from_spmatrix(sp_arr)
In [53]: sdf.head()
Out[53]:
0 1 2 3 4
0 0.956380 0.0 0.0 0.000000 0.0
1 0.000000 0.0 0.0 0.000000 0.0
2 0.000000 0.0 0.0 0.000000 0.0
3 0.000000 0.0 0.0 0.000000 0.0
4 0.999552 0.0 0.0 0.956153 0.0
In [54]: sdf.dtypes
Out[54]:
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 [55]: sdf.sparse.to_coo()
Out[55]:
<1000x5 sparse matrix of type '<class 'numpy.float64'>'
with 517 stored elements in COOrdinate format>
Series.sparse.to_coo() реализован для преобразования Series со значениями разреженной матрицы, индексированными MultiIndex, в scipy.sparse.coo_matrix.
Метод требует MultiIndex с двумя или более уровнями.
In [56]: s = pd.Series([3.0, np.nan, 1.0, 3.0, np.nan, np.nan])
In [57]: 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 [58]: ss = s.astype('Sparse')
In [59]: ss
Out[59]:
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 [60]: A, rows, columns = ss.sparse.to_coo(
....: row_levels=["A", "B"], column_levels=["C", "D"], sort_labels=True
....: )
....:
In [61]: A
Out[61]:
<3x4 sparse matrix of type '<class 'numpy.float64'>'
with 3 stored elements in COOrdinate format>
In [62]: A.todense()
Out[62]:
matrix([[0., 0., 1., 3.],
[3., 0., 0., 0.],
[0., 0., 0., 0.]])
In [63]: rows
Out[63]: [(1, 1), (1, 2), (2, 1)]
In [64]: columns
Out[64]: [('a', 0), ('a', 1), ('b', 0), ('b', 1)]
Указание различных меток строк и столбцов (и отказ от их сортировки) приводит к другой разреженной матрице:
In [65]: A, rows, columns = ss.sparse.to_coo(
....: row_levels=["A", "B", "C"], column_levels=["D"], sort_labels=False
....: )
....:
In [66]: A
Out[66]:
<3x2 sparse matrix of type '<class 'numpy.float64'>'
with 3 stored elements in COOrdinate format>
In [67]: A.todense()
Out[67]:
matrix([[3., 0.],
[1., 3.],
[0., 0.]])
In [68]: rows
Out[68]: [(1, 2, 'a'), (1, 1, 'b'), (2, 1, 'b')]
In [69]: columns
Out[69]: [(0,), (1,)]
Удобный метод Series.sparse.from_coo() реализован для создания Series с разреженными значениями из scipy.sparse.coo_matrix.
In [70]: from scipy import sparse
In [71]: A = sparse.coo_matrix(([3.0, 1.0, 2.0], ([1, 0, 0], [0, 2, 3])), shape=(3, 4))
In [72]: A
Out[72]:
<3x4 sparse matrix of type '<class 'numpy.float64'>'
with 3 stored elements in COOrdinate format>
In [73]: A.todense()
Out[73]:
matrix([[0., 0., 1., 2.],
[3., 0., 0., 0.],
[0., 0., 0., 0.]])
По умолчанию (с dense_index=False) просто возвращается Series, содержащий только ненулевые записи.
In [74]: ss = pd.Series.sparse.from_coo(A)
In [75]: ss
Out[75]:
0 2 1.0
3 2.0
1 0 3.0
dtype: Sparse[float64, nan]
Указание dense_index=True приведет к индексу, являющемуся декартовым произведением координат строк и столбцов матрицы. Обратите внимание, что это потребует значительного объёма памяти (по сравнению с dense_index=False) если разреженная матрица достаточно велика (и разрежена).
In [76]: ss_dense = pd.Series.sparse.from_coo(A, dense_index=True)
In [77]: ss_dense
Out[77]:
0 0 NaN
1 NaN
2 1.0
3 2.0
1 0 3.0
1 NaN
2 NaN
3 NaN
2 0 NaN
1 NaN
2 NaN
3 NaN
dtype: Sparse[float64, nan]
© 2008–2022, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team
Licensed under the 3-clause BSD License.
https://pandas.pydata.org/pandas-docs/version/1.5.0/user_guide/sparse.html