Skip to content

Commit adaca51

Browse files
kerncAnkurDedania
authored andcommitted
ENH: Native conversion from/to scipy.sparse matrix to SparseDataFrame
closes pandas-dev#4343 Author: Kernc <[email protected]> Closes pandas-dev#15497 from kernc/scipy-sparse and squashes the following commits: a0f2208 [Kernc] DOC: Fix some whatsnew/v0.20.0.txt sphinx warnings e72e594 [Kernc] ENH: Native conversion from/to scipy.sparse matrix to SparseDataFrame
1 parent c1065da commit adaca51

File tree

10 files changed

+266
-22
lines changed

10 files changed

+266
-22
lines changed

doc/source/api.rst

+9-2
Original file line numberDiff line numberDiff line change
@@ -711,8 +711,8 @@ Serialization / IO / Conversion
711711
Series.to_string
712712
Series.to_clipboard
713713

714-
Sparse methods
715-
~~~~~~~~~~~~~~
714+
Sparse
715+
~~~~~~
716716
.. autosummary::
717717
:toctree: generated/
718718

@@ -1030,6 +1030,13 @@ Serialization / IO / Conversion
10301030
DataFrame.to_string
10311031
DataFrame.to_clipboard
10321032

1033+
Sparse
1034+
~~~~~~
1035+
.. autosummary::
1036+
:toctree: generated/
1037+
1038+
SparseDataFrame.to_coo
1039+
10331040
.. _api.panel:
10341041

10351042
Panel

doc/source/sparse.rst

+31-1
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,37 @@ the correct dense result.
186186
Interaction with scipy.sparse
187187
-----------------------------
188188

189-
Experimental api to transform between sparse pandas and scipy.sparse structures.
189+
SparseDataFrame
190+
~~~~~~~~~~~~~~~
191+
192+
.. versionadded:: 0.20.0
193+
194+
Pandas supports creating sparse dataframes directly from ``scipy.sparse`` matrices.
195+
196+
.. ipython:: python
197+
198+
from scipy.sparse import csr_matrix
199+
200+
arr = np.random.random(size=(1000, 5))
201+
arr[arr < .9] = 0
202+
203+
sp_arr = csr_matrix(arr)
204+
sp_arr
205+
206+
sdf = pd.SparseDataFrame(sp_arr)
207+
sdf
208+
209+
All sparse formats are supported, but matrices that are not in :mod:`COOrdinate <scipy.sparse>` format will be converted, copying data as needed.
210+
To convert a ``SparseDataFrame`` back to sparse SciPy matrix in COO format, you can use the :meth:`SparseDataFrame.to_coo` method:
211+
212+
.. ipython:: python
213+
214+
sdf.to_coo()
215+
216+
SparseSeries
217+
~~~~~~~~~~~~
218+
219+
.. versionadded:: 0.16.0
190220

191221
A :meth:`SparseSeries.to_coo` method is implemented for transforming a ``SparseSeries`` indexed by a ``MultiIndex`` to a ``scipy.sparse.coo_matrix``.
192222

doc/source/whatsnew/v0.20.0.txt

+28-1
Original file line numberDiff line numberDiff line change
@@ -237,10 +237,37 @@ You must enable this by setting the ``display.html.table_schema`` option to True
237237
.. _Table Schema: http://specs.frictionlessdata.io/json-table-schema/
238238
.. _nteract: http://nteract.io/
239239

240+
.. _whatsnew_0200.enhancements.scipy_sparse:
241+
242+
SciPy sparse matrix from/to SparseDataFrame
243+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
244+
245+
Pandas now supports creating sparse dataframes directly from ``scipy.sparse.spmatrix`` instances.
246+
See the :ref:`documentation <sparse.scipysparse>` for more information. (:issue:`4343`)
247+
248+
All sparse formats are supported, but matrices that are not in :mod:`COOrdinate <scipy.sparse>` format will be converted, copying data as needed.
249+
250+
.. ipython:: python
251+
252+
from scipy.sparse import csr_matrix
253+
arr = np.random.random(size=(1000, 5))
254+
arr[arr < .9] = 0
255+
sp_arr = csr_matrix(arr)
256+
sp_arr
257+
sdf = pd.SparseDataFrame(sp_arr)
258+
sdf
259+
260+
To convert a ``SparseDataFrame`` back to sparse SciPy matrix in COO format, you can use:
261+
262+
.. ipython:: python
263+
264+
sdf.to_coo()
265+
240266
.. _whatsnew_0200.enhancements.other:
241267

242268
Other enhancements
243269
^^^^^^^^^^^^^^^^^^
270+
244271
- Integration with the ``feather-format``, including a new top-level ``pd.read_feather()`` and ``DataFrame.to_feather()`` method, see :ref:`here <io.feather>`.
245272
- ``Series.str.replace()`` now accepts a callable, as replacement, which is passed to ``re.sub`` (:issue:`15055`)
246273
- ``Series.str.replace()`` now accepts a compiled regular expression as a pattern (:issue:`15446`)
@@ -752,7 +779,6 @@ Bug Fixes
752779
- Bug in ``Rolling.quantile`` function that caused a segmentation fault when called with a quantile value outside of the range [0, 1] (:issue:`15463`)
753780
- Bug in ``pd.cut()`` with a single bin on an all 0s array (:issue:`15428`)
754781
- Bug in ``pd.qcut()`` with a single quantile and an array with identical values (:issue:`15431`)
755-
- Bug in ``SparseSeries.reindex`` on single level with list of length 1 (:issue:`15447`)
756782

757783

758784

@@ -783,6 +809,7 @@ Bug Fixes
783809
- Bug in ``to_sql`` when writing a DataFrame with numeric index names (:issue:`15404`).
784810
- Bug in ``Series.iloc`` where a ``Categorical`` object for list-like indexes input was returned, where a ``Series`` was expected. (:issue:`14580`)
785811
- Bug in repr-formatting a ``SparseDataFrame`` after a value was set on (a copy of) one of its series (:issue:`15488`)
812+
- Bug in ``SparseSeries.reindex`` on single level with list of length 1 (:issue:`15447`)
786813

787814

788815
- Bug in groupby operations with timedelta64 when passing ``numeric_only=False`` (:issue:`5724`)

pandas/sparse/array.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
is_integer_dtype,
2121
is_bool_dtype,
2222
is_list_like,
23+
is_string_dtype,
2324
is_scalar, is_dtype_equal)
2425
from pandas.types.cast import (_possibly_convert_platform, _maybe_promote,
2526
_astype_nansafe, _find_common_type)
@@ -769,14 +770,20 @@ def make_sparse(arr, kind='block', fill_value=None):
769770
if isnull(fill_value):
770771
mask = notnull(arr)
771772
else:
773+
# For str arrays in NumPy 1.12.0, operator!= below isn't
774+
# element-wise but just returns False if fill_value is not str,
775+
# so cast to object comparison to be safe
776+
if is_string_dtype(arr):
777+
arr = arr.astype(object)
778+
772779
mask = arr != fill_value
773780

774781
length = len(arr)
775782
if length != mask.size:
776783
# the arr is a SparseArray
777784
indices = mask.sp_index.indices
778785
else:
779-
indices = np.arange(length, dtype=np.int32)[mask]
786+
indices = mask.nonzero()[0].astype(np.int32)
780787

781788
index = _make_index(length, indices, kind)
782789
sparsified_values = arr[mask]

pandas/sparse/frame.py

+90-17
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
import numpy as np
1212

1313
from pandas.types.missing import isnull, notnull
14-
from pandas.types.cast import _maybe_upcast
15-
from pandas.types.common import _ensure_platform_int
14+
from pandas.types.cast import _maybe_upcast, _find_common_type
15+
from pandas.types.common import _ensure_platform_int, is_scipy_sparse
1616

1717
from pandas.core.common import _try_sort
1818
from pandas.compat.numpy import function as nv
@@ -25,6 +25,7 @@
2525
create_block_manager_from_arrays)
2626
import pandas.core.generic as generic
2727
from pandas.sparse.series import SparseSeries, SparseArray
28+
from pandas.sparse.libsparse import BlockIndex, get_blocks
2829
from pandas.util.decorators import Appender
2930
import pandas.core.ops as ops
3031

@@ -39,15 +40,15 @@ class SparseDataFrame(DataFrame):
3940
4041
Parameters
4142
----------
42-
data : same types as can be passed to DataFrame
43+
data : same types as can be passed to DataFrame or scipy.sparse.spmatrix
4344
index : array-like, optional
4445
column : array-like, optional
4546
default_kind : {'block', 'integer'}, default 'block'
4647
Default sparse kind for converting Series to SparseSeries. Will not
4748
override SparseSeries passed into constructor
4849
default_fill_value : float
49-
Default fill_value for converting Series to SparseSeries. Will not
50-
override SparseSeries passed in
50+
Default fill_value for converting Series to SparseSeries
51+
(default: nan). Will not override SparseSeries passed in.
5152
"""
5253
_constructor_sliced = SparseSeries
5354
_subtyp = 'sparse_frame'
@@ -84,22 +85,19 @@ def __init__(self, data=None, index=None, columns=None, default_kind=None,
8485
self._default_kind = default_kind
8586
self._default_fill_value = default_fill_value
8687

87-
if isinstance(data, dict):
88-
mgr = self._init_dict(data, index, columns)
89-
if dtype is not None:
90-
mgr = mgr.astype(dtype)
88+
if is_scipy_sparse(data):
89+
mgr = self._init_spmatrix(data, index, columns, dtype=dtype,
90+
fill_value=default_fill_value)
91+
elif isinstance(data, dict):
92+
mgr = self._init_dict(data, index, columns, dtype=dtype)
9193
elif isinstance(data, (np.ndarray, list)):
92-
mgr = self._init_matrix(data, index, columns)
93-
if dtype is not None:
94-
mgr = mgr.astype(dtype)
94+
mgr = self._init_matrix(data, index, columns, dtype=dtype)
9595
elif isinstance(data, SparseDataFrame):
9696
mgr = self._init_mgr(data._data,
9797
dict(index=index, columns=columns),
9898
dtype=dtype, copy=copy)
9999
elif isinstance(data, DataFrame):
100-
mgr = self._init_dict(data, data.index, data.columns)
101-
if dtype is not None:
102-
mgr = mgr.astype(dtype)
100+
mgr = self._init_dict(data, data.index, data.columns, dtype=dtype)
103101
elif isinstance(data, BlockManager):
104102
mgr = self._init_mgr(data, axes=dict(index=index, columns=columns),
105103
dtype=dtype, copy=copy)
@@ -174,7 +172,43 @@ def _init_dict(self, data, index, columns, dtype=None):
174172
return to_manager(sdict, columns, index)
175173

176174
def _init_matrix(self, data, index, columns, dtype=None):
175+
""" Init self from ndarray or list of lists """
177176
data = _prep_ndarray(data, copy=False)
177+
index, columns = self._prep_index(data, index, columns)
178+
data = dict([(idx, data[:, i]) for i, idx in enumerate(columns)])
179+
return self._init_dict(data, index, columns, dtype)
180+
181+
def _init_spmatrix(self, data, index, columns, dtype=None,
182+
fill_value=None):
183+
""" Init self from scipy.sparse matrix """
184+
index, columns = self._prep_index(data, index, columns)
185+
data = data.tocoo()
186+
N = len(index)
187+
188+
# Construct a dict of SparseSeries
189+
sdict = {}
190+
values = Series(data.data, index=data.row, copy=False)
191+
for col, rowvals in values.groupby(data.col):
192+
# get_blocks expects int32 row indices in sorted order
193+
rows = rowvals.index.values.astype(np.int32)
194+
rows.sort()
195+
blocs, blens = get_blocks(rows)
196+
197+
sdict[columns[col]] = SparseSeries(
198+
rowvals.values, index=index,
199+
fill_value=fill_value,
200+
sparse_index=BlockIndex(N, blocs, blens))
201+
202+
# Add any columns that were empty and thus not grouped on above
203+
sdict.update({column: SparseSeries(index=index,
204+
fill_value=fill_value,
205+
sparse_index=BlockIndex(N, [], []))
206+
for column in columns
207+
if column not in sdict})
208+
209+
return self._init_dict(sdict, index, columns, dtype)
210+
211+
def _prep_index(self, data, index, columns):
178212
N, K = data.shape
179213
if index is None:
180214
index = _default_index(N)
@@ -187,9 +221,48 @@ def _init_matrix(self, data, index, columns, dtype=None):
187221
if len(index) != N:
188222
raise ValueError('Index length mismatch: %d vs. %d' %
189223
(len(index), N))
224+
return index, columns
190225

191-
data = dict([(idx, data[:, i]) for i, idx in enumerate(columns)])
192-
return self._init_dict(data, index, columns, dtype)
226+
def to_coo(self):
227+
"""
228+
Return the contents of the frame as a sparse SciPy COO matrix.
229+
230+
.. versionadded:: 0.20.0
231+
232+
Returns
233+
-------
234+
coo_matrix : scipy.sparse.spmatrix
235+
If the caller is heterogeneous and contains booleans or objects,
236+
the result will be of dtype=object. See Notes.
237+
238+
Notes
239+
-----
240+
The dtype will be the lowest-common-denominator type (implicit
241+
upcasting); that is to say if the dtypes (even of numeric types)
242+
are mixed, the one that accommodates all will be chosen.
243+
244+
e.g. If the dtypes are float16 and float32, dtype will be upcast to
245+
float32. By numpy.find_common_type convention, mixing int64 and
246+
and uint64 will result in a float64 dtype.
247+
"""
248+
try:
249+
from scipy.sparse import coo_matrix
250+
except ImportError:
251+
raise ImportError('Scipy is not installed')
252+
253+
dtype = _find_common_type(self.dtypes)
254+
cols, rows, datas = [], [], []
255+
for col, name in enumerate(self):
256+
s = self[name]
257+
row = s.sp_index.to_int_index().indices
258+
cols.append(np.repeat(col, len(row)))
259+
rows.append(row)
260+
datas.append(s.sp_values.astype(dtype, copy=False))
261+
262+
cols = np.concatenate(cols)
263+
rows = np.concatenate(rows)
264+
datas = np.concatenate(datas)
265+
return coo_matrix((datas, (rows, cols)), shape=self.shape)
193266

194267
def __array_wrap__(self, result):
195268
return self._constructor(

pandas/tests/sparse/common.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import pytest
2+
3+
import pandas.util.testing as tm
4+
5+
6+
@pytest.fixture(params=['bsr', 'coo', 'csc', 'csr', 'dia', 'dok', 'lil'])
7+
def spmatrix(request):
8+
tm._skip_if_no_scipy()
9+
from scipy import sparse
10+
return getattr(sparse, request.param + '_matrix')

0 commit comments

Comments
 (0)