diff --git a/cpp/src/arrow/sparse_tensor.cc b/cpp/src/arrow/sparse_tensor.cc index b83d42e5fb21..6ff591014301 100644 --- a/cpp/src/arrow/sparse_tensor.cc +++ b/cpp/src/arrow/sparse_tensor.cc @@ -134,6 +134,10 @@ inline Status CheckSparseCOOIndexValidity(const std::shared_ptr& type, RETURN_NOT_OK(internal::CheckSparseIndexMaximumValue(type, shape)); + // Indexes with no values are considered valid + if (std::find(shape.begin(), shape.end(), 0) != shape.end()) { + return Status::OK(); + } if (!internal::IsTensorStridesContiguous(type, shape, strides)) { return Status::Invalid("SparseCOOIndex indices must be contiguous"); } diff --git a/cpp/src/arrow/sparse_tensor_test.cc b/cpp/src/arrow/sparse_tensor_test.cc index 5105724e68f7..59110089bbdc 100644 --- a/cpp/src/arrow/sparse_tensor_test.cc +++ b/cpp/src/arrow/sparse_tensor_test.cc @@ -1676,4 +1676,54 @@ TEST(TestSparseCSFMatrixForUInt64Index, Make) { ASSERT_RAISES(Invalid, SparseCSFTensor::Make(dense_tensor, uint64())); } +//----------------------------------------------------------------------------- +// Create SparseTensors from a dense Tensor with only zeros + +template +class TestSparseTensorFromDenseBase : public ::testing::Test { + public: + void SetUp() { + shape_ = {0, 12}; + dim_names_ = {"foo", "bar"}; + dense_values_ = {}; + dense_data_ = Buffer::Wrap(dense_values_); + } + + protected: + std::vector shape_; + std::vector dim_names_; + std::vector dense_values_; + std::shared_ptr dense_data_; +}; + +template +class TestSparseTensorFromDense : public TestSparseTensorFromDenseBase { +}; + +TYPED_TEST_SUITE_P(TestSparseTensorFromDense); + +TYPED_TEST_P(TestSparseTensorFromDense, TestNonZeroLength) { + using SparseTensorType = TypeParam; + + NumericTensor dense_tensor_ = + NumericTensor(this->dense_data_, this->shape_, {}, this->dim_names_); + ASSERT_OK_AND_ASSIGN( + auto sparse_tensor_, + SparseTensorType::Make(dense_tensor_, TypeTraits::type_singleton())); + ASSERT_EQ(sparse_tensor_->non_zero_length(), 0); + ASSERT_EQ(sparse_tensor_->shape(), this->shape_); + ASSERT_EQ(sparse_tensor_->dim_names(), this->dim_names_); +} + +REGISTER_TYPED_TEST_SUITE_P(TestSparseTensorFromDense, TestNonZeroLength); + +INSTANTIATE_TYPED_TEST_SUITE_P(TestSparseCOOTensor, TestSparseTensorFromDense, + SparseCOOTensor); +INSTANTIATE_TYPED_TEST_SUITE_P(TestSparseCSRMatrix, TestSparseTensorFromDense, + SparseCSRMatrix); +INSTANTIATE_TYPED_TEST_SUITE_P(TestSparseCSCMatrix, TestSparseTensorFromDense, + SparseCSCMatrix); +INSTANTIATE_TYPED_TEST_SUITE_P(TestSparseCSFTensor, TestSparseTensorFromDense, + SparseCSFTensor); + } // namespace arrow diff --git a/python/pyarrow/tests/test_sparse_tensor.py b/python/pyarrow/tests/test_sparse_tensor.py index aa7da0a74208..a395e31dbacb 100644 --- a/python/pyarrow/tests/test_sparse_tensor.py +++ b/python/pyarrow/tests/test_sparse_tensor.py @@ -33,7 +33,6 @@ except ImportError: sparse = None - tensor_type_pairs = [ ('i1', pa.int8()), ('i2', pa.int16()), @@ -434,6 +433,23 @@ def test_sparse_coo_tensor_scipy_roundtrip(dtype_str, arrow_type): assert sparse_tensor.has_canonical_format assert out_scipy_matrix.has_canonical_format + scipy_matrix = coo_matrix([[0, 0], [0, 0]]) + sparse_tensor = pa.SparseCOOTensor.from_scipy(scipy_matrix, + dim_names=dim_names) + out_scipy_matrix = sparse_tensor.to_scipy() + dense_array = scipy_matrix.toarray() + + assert scipy_matrix.has_canonical_format + assert sparse_tensor.has_canonical_format + assert out_scipy_matrix.has_canonical_format + + assert scipy_matrix.nnz == 0 + assert scipy_matrix.nnz == sparse_tensor.non_zero_length + assert np.array_equal(scipy_matrix.data, out_scipy_matrix.data) + assert np.array_equal(scipy_matrix.row, out_scipy_matrix.row) + assert np.array_equal(scipy_matrix.col, out_scipy_matrix.col) + assert np.array_equal(dense_array, sparse_tensor.to_tensor().to_numpy()) + @pytest.mark.skipif(not csr_matrix, reason="requires scipy") @pytest.mark.parametrize('dtype_str,arrow_type', tensor_type_pairs) @@ -464,6 +480,19 @@ def test_sparse_csr_matrix_scipy_roundtrip(dtype_str, arrow_type): dense_array = sparse_array.toarray() assert np.array_equal(dense_array, sparse_tensor.to_tensor().to_numpy()) + scipy_matrix = csr_matrix([[0, 0], [0, 0]]) + sparse_tensor = pa.SparseCSRMatrix.from_scipy(scipy_matrix, + dim_names=dim_names) + out_scipy_matrix = sparse_tensor.to_scipy() + dense_array = scipy_matrix.toarray() + + assert scipy_matrix.nnz == 0 + assert scipy_matrix.nnz == sparse_tensor.non_zero_length + assert np.array_equal(scipy_matrix.data, out_scipy_matrix.data) + assert np.array_equal(scipy_matrix.indptr, out_scipy_matrix.indptr) + assert np.array_equal(scipy_matrix.indices, out_scipy_matrix.indices) + assert np.array_equal(dense_array, sparse_tensor.to_tensor().to_numpy()) + @pytest.mark.skipif(not sparse, reason="requires pydata/sparse") @pytest.mark.parametrize('dtype_str,arrow_type', tensor_type_pairs) @@ -489,3 +518,16 @@ def test_pydata_sparse_sparse_coo_tensor_roundtrip(dtype_str, arrow_type): assert np.array_equal(sparse_array.coords, out_sparse_array.coords) assert np.array_equal(sparse_array.todense(), sparse_tensor.to_tensor().to_numpy()) + + sparse_array = sparse.COO.from_numpy([[0, 0], [0, 0]]) + sparse_tensor = pa.SparseCOOTensor.from_pydata_sparse(sparse_array, + dim_names=dim_names) + out_sparse_array = sparse_tensor.to_pydata_sparse() + dense_array = sparse_array.todense() + + assert sparse_array.nnz == 0 + assert sparse_array.nnz == sparse_tensor.non_zero_length + assert out_sparse_array.nnz == sparse_tensor.non_zero_length + assert np.array_equal(sparse_array.data, out_sparse_array.data) + assert np.array_equal(sparse_array.coords, out_sparse_array.coords) + assert np.array_equal(dense_array, sparse_tensor.to_tensor().to_numpy())