Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/vsparse/_norm_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,11 @@ def toarray(self) -> np.ndarray:
def to_scipy_sparse(self, dtype: npt.DTypeLike = np.float64) -> Any:
"""The uncentered, scaled sparse ``Delta`` term, as a real scipy sparse array.

**Not the normalized matrix when the recipe centers.** It omits
:attr:`means`, the value every structural zero carries once centered;
``parafac2``, ``scanpy`` and ``pearson`` all center. The whole matrix
is ``to_scipy_sparse().toarray() - means``, or :meth:`toarray`.

Same sparsity pattern as the underlying raw array (a ``csr_array``
for a VCSR-backed view, ``csc_array`` for VCSC), with :attr:`means`
left to subtract externally -- see :attr:`means`. Useful for handing
Expand Down
18 changes: 18 additions & 0 deletions tests/test_vcs_norm_recipes.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,3 +356,21 @@ def test_anndata_cache_does_not_pin_a_dropped_view():
assert ref() is None
# Still reusable -- from obs/varm/uns if not from the retained statistics.
assert adata.normalized("scanpy", recalculate=False) is not None


@pytest.mark.parametrize("recipe", sorted(RECIPES))
def test_to_scipy_sparse_minus_means_is_the_normalized_matrix(vcls, dense, recipe):
"""``to_scipy_sparse() - means`` must reconstruct the normalized matrix."""
if dense.sum() == 0:
pytest.skip("all-zero matrix: median row total is 0")
nv = vcls.from_scipy(_scipy_for(vcls, dense)).normalized(recipe)
np.testing.assert_allclose(nv.to_scipy_sparse().toarray() - nv.means, nv.toarray(), atol=1e-10)


@pytest.mark.parametrize("recipe", ["parafac2", "scanpy", "pearson"])
def test_to_scipy_sparse_alone_is_not_the_normalized_matrix_when_centering(vcls, recipe):
"""Dropping ``means`` really does change the answer for a centering recipe."""
rng = np.random.default_rng(0)
dense = rng.integers(1, 50, size=(40, 6)).astype(np.float64)
nv = vcls.from_scipy(_scipy_for(vcls, dense)).normalized(recipe)
assert np.abs(nv.to_scipy_sparse().toarray() - nv.toarray()).max() > 0.1
6 changes: 3 additions & 3 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading