Skip to content

Commit 9ac01a7

Browse files
cottrelljreback
authored andcommitted
Add SparseSeries.to_coo and from_coo methods for interaction with scipy.sparse.
1 parent 7da9178 commit 9ac01a7

File tree

9 files changed

+536
-15
lines changed

9 files changed

+536
-15
lines changed

doc/source/api.rst

+8
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,14 @@ Serialization / IO / Conversion
634634
Series.to_string
635635
Series.to_clipboard
636636

637+
Sparse methods
638+
~~~~~~~~~~~~~~
639+
.. autosummary::
640+
:toctree: generated/
641+
642+
SparseSeries.to_coo
643+
SparseSeries.from_coo
644+
637645
.. _api.dataframe:
638646

639647
DataFrame

doc/source/sparse.rst

+90-2
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,92 @@ 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, rows, columns = ss.to_coo(row_levels=['A', 'B'],
175+
column_levels=['C', 'D'],
176+
sort_labels=True)
177+
178+
A
179+
A.todense()
180+
rows
181+
columns
182+
183+
Specifying different row and column labels (and not sorting them) yields a different sparse matrix:
184+
185+
.. ipython:: python
186+
187+
A, rows, columns = ss.to_coo(row_levels=['A', 'B', 'C'],
188+
column_levels=['D'],
189+
sort_labels=False)
190+
191+
A
192+
A.todense()
193+
rows
194+
columns
195+
196+
A convenience method :meth:`SparseSeries.from_coo` is implemented for creating a ``SparseSeries`` from a ``scipy.sparse.coo_matrix``.
197+
198+
.. ipython:: python
199+
:suppress:
200+
201+
.. ipython:: python
202+
203+
from scipy import sparse
204+
A = sparse.coo_matrix(([3.0, 1.0, 2.0], ([1, 0, 0], [0, 2, 3])),
205+
shape=(3, 4))
206+
A
207+
A.todense()
208+
209+
The default behaviour (with ``dense_index=False``) simply returns a ``SparseSeries`` containing
210+
only the non-null entries.
211+
212+
.. ipython:: python
213+
214+
ss = SparseSeries.from_coo(A)
215+
ss
216+
217+
Specifying ``dense_index=True`` will result in an index that is the Cartesian product of the
218+
row and columns coordinates of the matrix. Note that this will consume a significant amount of memory
219+
(relative to ``dense_index=False``) if the sparse matrix is large (and sparse) enough.
220+
221+
.. ipython:: python
222+
223+
ss_dense = SparseSeries.from_coo(A, dense_index=True)
224+
ss_dense
225+

doc/source/whatsnew/v0.16.0.txt

+49-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ calculate the ratio, and plot
6767
.plot(kind='scatter', x='SepalRatio', y='PetalRatio'))
6868

6969
.. image:: _static/whatsnew_assign.png
70-
70+
7171
See the :ref:`documentation <dsintro.chained_assignment>` for more. (:issue:`9229`)
7272

7373
.. _whatsnew_0160.api:
@@ -253,6 +253,54 @@ Enhancements
253253
- ``StringMethods.pad()`` and ``center()`` now accept ``fillchar`` option to specify filling character (:issue:`9352`)
254254
- Added ``StringMethods.zfill()`` which behave as the same as standard ``str`` (:issue:`9387`)
255255

256+
Interaction with scipy.sparse
257+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
258+
259+
.. _whatsnew_0160.enhancements.sparse:
260+
261+
Added :meth:`SparseSeries.to_coo` and :meth:`SparseSeries.from_coo` methods (:issue:`8048`) for converting to and from ``scipy.sparse.coo_matrix`` instances (see :ref:`here <sparse.scipysparse>`). For example, given a SparseSeries with MultiIndex we can convert to a `scipy.sparse.coo_matrix` by specifying the row and column labels as index levels:
262+
263+
.. ipython:: python
264+
265+
from numpy import nan
266+
s = Series([3.0, nan, 1.0, 3.0, nan, nan])
267+
s.index = MultiIndex.from_tuples([(1, 2, 'a', 0),
268+
(1, 2, 'a', 1),
269+
(1, 1, 'b', 0),
270+
(1, 1, 'b', 1),
271+
(2, 1, 'b', 0),
272+
(2, 1, 'b', 1)],
273+
names=['A', 'B', 'C', 'D'])
274+
275+
s
276+
277+
# SparseSeries
278+
ss = s.to_sparse()
279+
ss
280+
281+
A, rows, columns = ss.to_coo(row_levels=['A', 'B'],
282+
column_levels=['C', 'D'],
283+
sort_labels=False)
284+
285+
A
286+
A.todense()
287+
rows
288+
columns
289+
290+
The from_coo method is a convenience method for creating a ``SparseSeries``
291+
from a ``scipy.sparse.coo_matrix``:
292+
293+
.. ipython:: python
294+
295+
from scipy import sparse
296+
A = sparse.coo_matrix(([3.0, 1.0, 2.0], ([1, 0, 0], [0, 2, 3])),
297+
shape=(3, 4))
298+
A
299+
A.todense()
300+
301+
ss = SparseSeries.from_coo(A)
302+
ss
303+
256304
Performance
257305
~~~~~~~~~~~
258306

pandas/sparse/scipy_sparse.py

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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 as np
11+
from pandas.compat import OrderedDict
12+
from pandas.tools.util import cartesian_product
13+
14+
15+
def _check_is_partition(parts, whole):
16+
whole = set(whole)
17+
parts = [set(x) for x in parts]
18+
if set.intersection(*parts) != set():
19+
raise ValueError(
20+
'Is not a partition because intersection is not null.')
21+
if set.union(*parts) != whole:
22+
raise ValueError('Is not a partition becuase union is not the whole.')
23+
24+
25+
def _to_ijv(ss, row_levels=(0,), column_levels=(1,), sort_labels=False):
26+
""" For arbitrary (MultiIndexed) SparseSeries return
27+
(v, i, j, ilabels, jlabels) where (v, (i, j)) is suitable for
28+
passing to scipy.sparse.coo constructor. """
29+
# index and column levels must be a partition of the index
30+
_check_is_partition([row_levels, column_levels], range(ss.index.nlevels))
31+
32+
# from the SparseSeries: get the labels and data for non-null entries
33+
values = ss._data.values._valid_sp_values
34+
35+
nonnull_labels = ss.dropna()
36+
37+
def get_indexers(levels):
38+
""" Return sparse coords and dense labels for subset levels """
39+
40+
# TODO: how to do this better? cleanly slice nonnull_labels given the
41+
# coord
42+
values_ilabels = [tuple(x[i] for i in levels)
43+
for x in nonnull_labels.index]
44+
if len(levels) == 1:
45+
values_ilabels = [x[0] for x in values_ilabels]
46+
47+
#######################################################################
48+
# # performance issues with groupby ###################################
49+
# TODO: these two lines can rejplace the code below but
50+
# groupby is too slow (in some cases at least)
51+
# labels_to_i = ss.groupby(level=levels, sort=sort_labels).first()
52+
# labels_to_i[:] = np.arange(labels_to_i.shape[0])
53+
54+
def _get_label_to_i_dict(labels, sort_labels=False):
55+
""" Return OrderedDict of unique labels to number.
56+
Optionally sort by label. """
57+
labels = Index(map(tuple, labels)).unique().tolist() # squish
58+
if sort_labels:
59+
labels = sorted(list(labels))
60+
d = OrderedDict((k, i) for i, k in enumerate(labels))
61+
return(d)
62+
63+
def _get_index_subset_to_coord_dict(index, subset, sort_labels=False):
64+
def robust_get_level_values(i):
65+
# if index has labels (that are not None) use those,
66+
# else use the level location
67+
try:
68+
return(index.get_level_values(index.names[i]))
69+
except KeyError:
70+
return(index.get_level_values(i))
71+
ilabels = list(
72+
zip(*[robust_get_level_values(i) for i in subset]))
73+
labels_to_i = _get_label_to_i_dict(
74+
ilabels, sort_labels=sort_labels)
75+
labels_to_i = Series(labels_to_i)
76+
labels_to_i.index = MultiIndex.from_tuples(labels_to_i.index)
77+
labels_to_i.index.names = [index.names[i] for i in subset]
78+
labels_to_i.name = 'value'
79+
return(labels_to_i)
80+
81+
labels_to_i = _get_index_subset_to_coord_dict(
82+
ss.index, levels, sort_labels=sort_labels)
83+
#######################################################################
84+
#######################################################################
85+
86+
i_coord = labels_to_i[values_ilabels].tolist()
87+
i_labels = labels_to_i.index.tolist()
88+
89+
return i_coord, i_labels
90+
91+
i_coord, i_labels = get_indexers(row_levels)
92+
j_coord, j_labels = get_indexers(column_levels)
93+
94+
return values, i_coord, j_coord, i_labels, j_labels
95+
96+
97+
def _sparse_series_to_coo(ss, row_levels=(0,), column_levels=(1,), sort_labels=False):
98+
""" Convert a SparseSeries to a scipy.sparse.coo_matrix using index
99+
levels row_levels, column_levels as the row and column
100+
labels respectively. Returns the sparse_matrix, row and column labels. """
101+
102+
import scipy.sparse
103+
104+
if ss.index.nlevels < 2:
105+
raise ValueError('to_coo requires MultiIndex with nlevels > 2')
106+
if not ss.index.is_unique:
107+
raise ValueError(
108+
'Duplicate index entries are not allowed in to_coo transformation.')
109+
110+
# to keep things simple, only rely on integer indexing (not labels)
111+
row_levels = [ss.index._get_level_number(x) for x in row_levels]
112+
column_levels = [ss.index._get_level_number(x) for x in column_levels]
113+
114+
v, i, j, rows, columns = _to_ijv(
115+
ss, row_levels=row_levels, column_levels=column_levels, sort_labels=sort_labels)
116+
sparse_matrix = scipy.sparse.coo_matrix(
117+
(v, (i, j)), shape=(len(rows), len(columns)))
118+
return sparse_matrix, rows, columns
119+
120+
121+
def _coo_to_sparse_series(A, dense_index=False):
122+
""" Convert a scipy.sparse.coo_matrix to a SparseSeries.
123+
Use the defaults given in the SparseSeries constructor. """
124+
s = Series(A.data, MultiIndex.from_arrays((A.row, A.col)))
125+
s = s.sort_index()
126+
s = s.to_sparse() # TODO: specify kind?
127+
if dense_index:
128+
# is there a better constructor method to use here?
129+
i = range(A.shape[0])
130+
j = range(A.shape[1])
131+
ind = MultiIndex.from_product([i, j])
132+
s = s.reindex_axis(ind)
133+
return s

0 commit comments

Comments
 (0)