Skip to content

Commit db43e4a

Browse files
committed
Add SparseSeries.to_coo and from_coo methods for interaction with scipy.sparse.
1 parent 576818f commit db43e4a

File tree

9 files changed

+448
-14
lines changed

9 files changed

+448
-14
lines changed

doc/source/api.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,14 @@ Serialization / IO / Conversion
620620
Series.to_string
621621
Series.to_clipboard
622622

623+
Sparse methods
624+
~~~~~~~~~~~~~~
625+
.. autosummary::
626+
:toctree: generated/
627+
628+
SparseSeries.to_coo
629+
SparseSeries.from_coo
630+
623631
.. _api.dataframe:
624632

625633
DataFrame

doc/source/sparse.rst

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,9 @@ accept scalar values or any 1-dimensional sequence:
109109
.. ipython:: python
110110
:suppress:
111111
112-
from numpy import nan
113-
114112
.. ipython:: python
115113
114+
from numpy import nan
116115
spl.append(np.array([1., nan, nan, 2., 3.]))
117116
spl.append(5)
118117
spl.append(sparr)
@@ -135,3 +134,90 @@ recommend using ``block`` as it's more memory efficient. The ``integer`` format
135134
keeps an arrays of all of the locations where the data are not equal to the
136135
fill value. The ``block`` format tracks only the locations and sizes of blocks
137136
of data.
137+
138+
.. _sparse.scipysparse:
139+
140+
Interaction with scipy.sparse
141+
-----------------------------
142+
143+
Experimental api to transform between sparse pandas and scipy.sparse structures.
144+
145+
A :meth:`SparseSeries.to_coo` method is implemented for transforming a ``SparseSeries`` indexed by a ``MultiIndex`` to a ``scipy.sparse.coo_matrix``.
146+
147+
The method requires a ``MultiIndex`` with two or more levels.
148+
149+
.. ipython:: python
150+
:suppress:
151+
152+
153+
.. ipython:: python
154+
155+
from numpy import nan
156+
s = Series([3.0, nan, 1.0, 3.0, nan, nan])
157+
s.index = MultiIndex.from_tuples([(1, 2, 'a', 0),
158+
(1, 2, 'a', 1),
159+
(1, 1, 'b', 0),
160+
(1, 1, 'b', 1),
161+
(2, 1, 'b', 0),
162+
(2, 1, 'b', 1)],
163+
names=['A', 'B', 'C', 'D'])
164+
165+
s
166+
# SparseSeries
167+
ss = s.to_sparse()
168+
ss
169+
170+
In the example below, we transform the ``SparseSeries`` to a sparse representation of a 2-d array by specifying that the first and second ``MultiIndex`` levels define labels for the rows and the third and fourth levels define labels for the columns. We also specify that the column and row labels should be sorted in the final sparse representation.
171+
172+
.. ipython:: python
173+
174+
A, il, jl = ss.to_coo(ilevels=['A', 'B'], jlevels=['C', 'D'],
175+
sort_labels=True)
176+
177+
A
178+
A.todense()
179+
il
180+
jl
181+
182+
Specifying different row and column labels (and not sorting them) yields a different sparse matrix:
183+
184+
.. ipython:: python
185+
186+
A, il, jl = ss.to_coo(ilevels=['A', 'B', 'C'], jlevels=['D'],
187+
sort_labels=False)
188+
189+
A
190+
A.todense()
191+
il
192+
jl
193+
194+
A convenience method :meth:`SparseSeries.from_coo` is implemented for creating a ``SparseSeries`` from a ``scipy.sparse.coo_matrix``.
195+
196+
.. ipython:: python
197+
:suppress:
198+
199+
.. ipython:: python
200+
201+
from scipy import sparse
202+
A = sparse.coo_matrix(([3.0, 1.0, 2.0], ([1, 0, 0], [0, 2, 3])),
203+
shape=(3, 4))
204+
A
205+
A.todense()
206+
207+
The default behaviour (with ``dense_index=False``) simply returns a ``SparseSeries`` containing
208+
only the non-null entries.
209+
210+
.. ipython:: python
211+
212+
ss = SparseSeries.from_coo(A)
213+
ss
214+
215+
Specifying ``dense_index=True`` will result in an index that is the Cartesian product of the
216+
row and columns coordinates of the matrix. Note that this will consume a significant amount of memory
217+
(relative to ``dense_index=False``) if the sparse matrix is large (and sparse) enough.
218+
219+
.. ipython:: python
220+
221+
ss_dense = SparseSeries.from_coo(A, dense_index=True)
222+
ss_dense
223+

doc/source/whatsnew/v0.16.0.txt

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,54 @@ Enhancements
110110

111111
- Added auto-complete for ``Series.str.<tab>``, ``Series.dt.<tab>`` and ``Series.cat.<tab>`` (:issue:`9322`)
112112

113+
Interaction with scipy.sparse
114+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
115+
- Added :meth:`SparseSeries.to_coo` and :meth:`SparseSeries.from_coo` methods
116+
(:issue:`8048`) for converting to and from ``scipy.sparse.coo_matrix``
117+
instances (see :ref:`here <sparse.scipysparse>`).
118+
For example, given a SparseSeries with MultiIndex we can convert to a
119+
`scipy.sparse.coo_matrix` by specifying the row and column labels as
120+
index levels:
121+
122+
.. ipython:: python
123+
124+
from numpy import nan
125+
s = Series([3.0, nan, 1.0, 3.0, nan, nan])
126+
s.index = MultiIndex.from_tuples([(1, 2, 'a', 0),
127+
(1, 2, 'a', 1),
128+
(1, 1, 'b', 0),
129+
(1, 1, 'b', 1),
130+
(2, 1, 'b', 0),
131+
(2, 1, 'b', 1)],
132+
names=['A', 'B', 'C', 'D'])
133+
134+
s
135+
# SparseSeries
136+
ss = s.to_sparse()
137+
ss
138+
139+
A, il, jl = ss.to_coo(ilevels=['A', 'B'], jlevels=['C', 'D'],
140+
sort_labels=False)
141+
142+
A
143+
A.todense()
144+
il
145+
jl
146+
147+
The from_coo method is a convenience method for creating a ``SparseSeries``
148+
from a ``scipy.sparse.coo_matrix``:
149+
150+
.. ipython:: python
151+
152+
from scipy import sparse
153+
A = sparse.coo_matrix(([3.0, 1.0, 2.0], ([1, 0, 0], [0, 2, 3])),
154+
shape=(3, 4))
155+
A
156+
A.todense()
157+
158+
ss = SparseSeries.from_coo(A)
159+
ss
160+
113161
Performance
114162
~~~~~~~~~~~
115163

pandas/sparse/scipy_sparse.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""
2+
Interaction with scipy.sparse matrices.
3+
4+
Currently only includes SparseSeries.to_coo helpers.
5+
"""
6+
from pandas.core.frame import DataFrame
7+
from pandas.core.index import MultiIndex, Index
8+
from pandas.core.series import Series
9+
import itertools
10+
import numpy
11+
from pandas.compat import OrderedDict
12+
from pandas.tools.util import cartesian_product
13+
14+
15+
def _get_label_to_i_dict(labels, sort_labels=False):
16+
""" Return OrderedDict of unique labels to number. Optionally sort by label. """
17+
labels = Index(map(tuple, labels)).unique().tolist() # squish
18+
if sort_labels:
19+
labels = sorted(list(labels))
20+
d = OrderedDict((k, i) for i, k in enumerate(labels))
21+
return(d)
22+
23+
24+
def _get_index_subset_to_coord_dict(index, subset, sort_labels=False):
25+
ilabels = list(zip(*[index.get_level_values(i) for i in subset]))
26+
labels_to_i = _get_label_to_i_dict(ilabels, sort_labels=sort_labels)
27+
return(labels_to_i)
28+
29+
30+
def _check_is_partition(parts, whole):
31+
whole = set(whole)
32+
parts = [set(x) for x in parts]
33+
if set.intersection(*parts) != set():
34+
raise ValueError(
35+
'Is not a partition because intersection is not null.')
36+
if set.union(*parts) != whole:
37+
raise ValueError('Is not a partition becuase union is not the whole.')
38+
39+
40+
def _to_ijv(ss, ilevels=(0,), jlevels=(1,), sort_labels=False):
41+
""" For arbitrary (MultiIndexed) SparseSeries return (v, i, j, ilabels, jlabels) where (v, (i, j)) is suitable for
42+
passing to scipy.sparse.coo constructor. """
43+
# index and column levels must be a partition of the index
44+
_check_is_partition([ilevels, jlevels], range(ss.index.nlevels))
45+
46+
# from the SparseSeries: get the labels and data for non-null entries
47+
values = ss._data.values._valid_sp_values
48+
blocs = ss._data.values.sp_index.blocs
49+
blength = ss._data.values.sp_index.blengths
50+
nonnull_labels = list(
51+
itertools.chain(*[ss.index.values[i:(i + j)] for i, j in zip(blocs, blength)]))
52+
53+
def get_indexers(levels):
54+
""" Return sparse coords and dense labels for subset levels """
55+
values_ilabels = [tuple(x[i] for i in levels) for x in nonnull_labels]
56+
labels_to_i = _get_index_subset_to_coord_dict(
57+
ss.index, levels, sort_labels=sort_labels)
58+
i_coord = [labels_to_i[i] for i in values_ilabels]
59+
return(i_coord, list(labels_to_i.keys()))
60+
61+
i_coord, i_labels = get_indexers(ilevels)
62+
j_coord, j_labels = get_indexers(jlevels)
63+
64+
return(values, i_coord, j_coord, i_labels, j_labels)
65+
66+
67+
def _sparse_series_to_coo(ss, ilevels=(0,), jlevels=(1,), sort_labels=False):
68+
""" Convert a SparseSeries to a scipy.sparse.coo_matrix using index levels ilevels, jlevels as the row and column
69+
labels respectively. Returns the sparse_matrix, row and column labels. """
70+
if ss.index.nlevels < 2:
71+
raise ValueError('to_coo requires MultiIndex with nlevels > 2')
72+
if not ss.index.is_unique:
73+
raise ValueError(
74+
'Duplicate index entries are not allowed in to_coo transformation.')
75+
76+
# to keep things simple, only rely on integer indexing (not labels)
77+
ilevels = [ss.index._get_level_number(x) for x in ilevels]
78+
jlevels = [ss.index._get_level_number(x) for x in jlevels]
79+
ss = ss.copy()
80+
ss.index.names = [None] * ss.index.nlevels # kill any existing labels
81+
82+
v, i, j, il, jl = _to_ijv(
83+
ss, ilevels=ilevels, jlevels=jlevels, sort_labels=sort_labels)
84+
import scipy.sparse
85+
sparse_matrix = scipy.sparse.coo_matrix(
86+
(v, (i, j)), shape=(len(il), len(jl)))
87+
return(sparse_matrix, il, jl)
88+
89+
90+
def _coo_to_sparse_series(A, dense_index=False):
91+
""" Convert a scipy.sparse.coo_matrix to a SparseSeries.
92+
Use the defaults given in the SparseSeries constructor. """
93+
s = Series(A.data, MultiIndex.from_arrays((A.row, A.col)))
94+
s = s.sort_index()
95+
s = s.to_sparse() # TODO: specify kind?
96+
if dense_index:
97+
# is there a better constructor method to use here?
98+
i = range(A.shape[0])
99+
j = range(A.shape[1])
100+
ind = MultiIndex.from_product([i, j])
101+
s = s.reindex_axis(ind)
102+
return(s)

pandas/sparse/series.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@
2929

3030
from pandas.util.decorators import Appender
3131

32+
from pandas.sparse.scipy_sparse import _sparse_series_to_coo, _coo_to_sparse_series
33+
3234
#------------------------------------------------------------------------------
3335
# Wrapper function for Series arithmetic methods
3436

3537

3638
def _arith_method(op, name, str_rep=None, default_axis=None, fill_zeros=None,
37-
**eval_kwargs):
39+
**eval_kwargs):
3840
"""
3941
Wrapper function for Series arithmetic operations, to avoid
4042
code duplication.
@@ -654,6 +656,48 @@ def combine_first(self, other):
654656
dense_combined = self.to_dense().combine_first(other)
655657
return dense_combined.to_sparse(fill_value=self.fill_value)
656658

659+
def to_coo(self, ilevels=(0,), jlevels=(1,), sort_labels=False):
660+
"""
661+
Create a scipy.sparse.coo_matrix from a SparseSeries with MultiIndex.
662+
663+
Use ilevels and jlevels to determine the row and column coordinates respectively.
664+
ilevels and jlevels are the names (labels) or numbers of the levels.
665+
{ilevels, jlevels} must be a partition of the MultiIndex level names (or numbers).
666+
667+
Parameters
668+
----------
669+
ilevels : tuple/list
670+
jlevels : tuple/list
671+
sort_labels : bool, default False
672+
Sort the row and column labels before forming the sparse matrix.
673+
674+
Returns
675+
-------
676+
y : scipy.sparse.coo_matrix
677+
il : list (row labels)
678+
jl : list (column labels)
679+
"""
680+
A, il, jl = _sparse_series_to_coo(
681+
self, ilevels, jlevels, sort_labels=sort_labels)
682+
return(A, il, jl)
683+
684+
@classmethod
685+
def from_coo(cls, A, dense_index=False):
686+
"""
687+
Create a SparseSeries from a scipy.sparse.coo_matrix.
688+
689+
Parameters
690+
----------
691+
A : scipy.sparse.coo_matrix
692+
dense_index : bool, default False
693+
If False (default), the SparseSeries index consists of only the coords of the non-null entries of the original coo_matrix.
694+
If True, the SparseSeries index consists of the full sorted (row, col) coordinates of the coo_matrix.
695+
696+
Returns
697+
-------
698+
s : SparseSeries
699+
"""
700+
return(_coo_to_sparse_series(A, dense_index=dense_index))
657701
# overwrite series methods with unaccelerated versions
658702
ops.add_special_arithmetic_methods(SparseSeries, use_numexpr=False,
659703
**ops.series_special_funcs)

0 commit comments

Comments
 (0)