Разреженные структуры данных#
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 тип данных.
Разреженные объекты существуют для повышения эффективности использования памяти. Предположим, у вас есть большой, в основном 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
Как видите, плотность (% значений, которые не были 'сжаты') чрезвычайно низка. Этот разреженный объект занимает гораздо меньше памяти на диске (при сериализации) и в интерпретаторе 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#
The SparseArray.dtype свойство хранит две части информации
Тип данных неразреженных значений
Скалярное значение заполнения
In [19]: sparr.dtype
Out[19]: Sparse[float64, nan]
A SparseDtype может быть создан путём передачи только dtype
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)
Sparse аксессор#
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 матрицы с.
A .sparse аксессор был добавлен для DataFrame также. См. Sparse аксессор подробнее.
Разреженный расчет#
Вы можете применять NumPy универсальные функции (ufuncs)
to 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)
The универсальная функция (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 со sparse значениями из sparse матрицы.
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]:
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() method:
In [43]: sdf.sparse.to_coo()
Out[43]:
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 в разреженное представление 2-мерного массива, указав, что первый и второй 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]:
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]:
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() реализован для создания Series с разреженными значениями из scipy.sparse.coo_matrix.
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]:
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]