Skip to content

Exploring SciPy sparse array migration from sparse matrices #785

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 20, 2025
Merged
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
2 changes: 1 addition & 1 deletion ci/310-oldest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ dependencies:
- packaging=22
- pandas=1.4
- requests=2.27
- scipy=1.8
- scipy=1.12
- shapely=2.0.1
# testing
- codecov
Expand Down
5 changes: 0 additions & 5 deletions libpysal/graph/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@

import numpy as np
import pandas as pd
from packaging.version import Version
from scipy import __version__ as scipy_version
from scipy import sparse

from libpysal.weights import W
Expand Down Expand Up @@ -2000,9 +1998,6 @@ def higher_order(self, k=2, shortest_path=True, diagonal=False, lower_order=Fals
<Graph of 85 nodes and 1176 nonzero edges indexed by
[0, 1, 2, 3, 4, ...]>
"""
if not Version(scipy_version) >= Version("1.12.0"):
raise ImportError("Graph.higher_order() requires scipy>=1.12.0.")

binary = self.transform("B")
sp = binary.sparse

Expand Down
10 changes: 0 additions & 10 deletions libpysal/graph/tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
import numpy as np
import pandas as pd
import pytest
from packaging.version import Version
from scipy import __version__ as scipy_version
from scipy import sparse

from libpysal import graph, weights
Expand Down Expand Up @@ -1014,10 +1012,6 @@ def test_lag(self):
with pytest.raises(ValueError, match="The length of `y`"):
self.g_str.lag(list(range(1, 15)))

@pytest.mark.skipif(
Version(scipy_version) < Version("1.12.0"),
reason="sparse matrix power requires scipy>=1.12.0",
)
def test_higher_order(self):
cont = graph.Graph.build_contiguity(self.nybb)
k2 = cont.higher_order(2)
Expand Down Expand Up @@ -1091,10 +1085,6 @@ def test_higher_order(self):
lower = cont.higher_order(2, lower_order=True)
assert lower == expected

@pytest.mark.skipif(
Version(scipy_version) < Version("1.12.0"),
reason="sparse matrix power requires scipy>=1.12.0",
)
def test_higher_order_inclusive(self): # GH738
contig = graph.Graph.from_arrays(
[0, 1, 2, 3, 3, 4, 4], [0, 3, 4, 1, 4, 2, 3], [0, 1, 1, 1, 1, 1, 1]
Expand Down
6 changes: 0 additions & 6 deletions libpysal/graph/tests/test_raster.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import numpy as np
import pandas as pd
import pytest
from packaging.version import Version
from scipy import __version__ as scipy_version

from libpysal import graph
from libpysal.weights.raster import testDataArray as dummy_array # noqa: N813
Expand All @@ -18,10 +16,6 @@ def setup_method(self):
self.da4 = dummy_array((1, 1), missing_vals=False)
self.da4.data = np.array([["test"]])

@pytest.mark.skipif(
Version(scipy_version) < Version("1.12.0"),
reason="sparse matrix power requires scipy>=1.12.0",
)
def test_queen(self):
g1 = graph.Graph.build_raster_contiguity(self.da1, rook=False, k=2, n_jobs=-1)
assert g1[(1, -30.0, -180.0)].to_dict() == {
Expand Down
2 changes: 1 addition & 1 deletion libpysal/weights/raster.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ def da2WSP(
# then eliminate zeros from the data. This changes the
# sparsity of the csr_matrix !!
if k > 1 and not include_nodata:
sw = sum(sw**x for x in range(1, k + 1))
sw = sum(sparse.linalg.matrix_power(sw, x) for x in range(1, k + 1))
sw.setdiag(0)
sw.eliminate_zeros()
sw.data[:] = np.ones_like(sw.data, dtype=np.int8)
Expand Down
6 changes: 3 additions & 3 deletions libpysal/weights/set_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import copy

from numpy import ones
from scipy.sparse import isspmatrix_csr
from scipy.sparse import issparse

from .weights import WSP, W

Expand Down Expand Up @@ -501,9 +501,9 @@ def w_clip(w1, w2, outSP=True, **kwargs): # noqa: N803
if not w1.id_order:
w1.id_order = None
id_order = w1.id_order
if not isspmatrix_csr(w1):
if not issparse(w1):
w1 = w1.sparse
if not isspmatrix_csr(w2):
if not issparse(w2):
w2 = w2.sparse
w2.data = ones(w2.data.shape)
wc = w1.multiply(w2)
Expand Down
12 changes: 10 additions & 2 deletions libpysal/weights/tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,21 @@ def test_lat2_w(self):
assert w9[0] == {1: 1.0, 3: 1.0}
assert w9[3] == {0: 1.0, 4: 1.0, 6: 1.0}

def test_lat2_sw(self):
w9 = util.lat2SW(3, 3)
@pytest.mark.parametrize("row_st", [True, False])
def test_lat2_sw(self, row_st):
w9 = util.lat2SW(3, 3, row_st=row_st)
rows, cols = w9.shape
n = rows * cols
assert w9.nnz == 24
pct_nonzero = w9.nnz / float(n)
assert pct_nonzero == 0.29629629629629628
if row_st:
#### Can be this one-liner after scipy >=1.15 is assured
# w9 = (w9.T.multiply(w9.tocsr().count_nonzero(axis=1))).T
w9csr = w9.tocsr()
nnz_by_axis1 = np.diff(w9csr.indptr)
w9 = (w9csr.T.multiply(nnz_by_axis1)).T
####
data = w9.todense().tolist()
assert data[0] == [0, 1, 0, 1, 0, 0, 0, 0, 0]
assert data[1] == [1, 0, 1, 0, 1, 0, 0, 0, 0]
Expand Down
10 changes: 5 additions & 5 deletions libpysal/weights/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ def higher_order_sp(
w = w.sparse
else:
raise ValueError("Weights are not binary (0,1)")
elif scipy.sparse.isspmatrix_csr(w):
elif scipy.sparse.issparse(w) and w.format == "csr":
if not np.unique(w.data) == np.array([1.0]):
raise ValueError(
"Sparse weights matrix is not binary (0,1) weights matrix."
Expand All @@ -523,17 +523,17 @@ def higher_order_sp(
)

if lower_order:
wk = sum(w**x for x in range(1, k + 1))
shortest_path = False
wk = sum(sparse.linalg.matrix_power(w, k) for k in range(1, k + 1))
else:
wk = w**k
wk = sparse.linalg.matrix_power(w, k)

rk, ck = wk.nonzero()
sk = set(zip(rk, ck, strict=True))

if shortest_path:
for j in range(1, k):
wj = w**j
wj = sparse.linalg.matrix_power(w, j)
rj, cj = wj.nonzero()
sj = set(zip(rj, cj, strict=True))
sk.difference_update(sj)
Expand Down Expand Up @@ -1225,7 +1225,7 @@ def lat2SW(nrows=3, ncols=5, criterion="rook", row_st=False):
m = sparse.dia_matrix((data, offsets), shape=(n, n), dtype=np.int8)
m = m + m.T
if row_st:
m = sparse.spdiags(1.0 / m.sum(1).T, 0, *m.shape) * m
m = sparse.dia_matrix(((1.0 / m.sum(1).T), [0]), shape=m.shape) @ m
m = m.tocsc()
m.eliminate_zeros()
return m
Expand Down
8 changes: 4 additions & 4 deletions libpysal/weights/weights.py
Original file line number Diff line number Diff line change
Expand Up @@ -721,7 +721,7 @@ def diagW2(self):
trcW2
"""
if "diagw2" not in self._cache:
self._diagW2 = (self.sparse * self.sparse).diagonal()
self._diagW2 = (self.sparse @ self.sparse).diagonal()
self._cache["diagW2"] = self._diagW2
return self._diagW2

Expand All @@ -734,7 +734,7 @@ def diagWtW(self):
trcWtW
"""
if "diagWtW" not in self._cache:
self._diagWtW = (self.sparse.transpose() * self.sparse).diagonal()
self._diagWtW = (self.sparse.transpose() @ self.sparse).diagonal()
self._cache["diagWtW"] = self._diagWtW
return self._diagWtW

Expand All @@ -757,7 +757,7 @@ def diagWtW_WW(self):
if "diagWtW_WW" not in self._cache:
wt = self.sparse.transpose()
w = self.sparse
self._diagWtW_WW = (wt * w + w * w).diagonal()
self._diagWtW_WW = (wt @ w + w @ w).diagonal()
self._cache["diagWtW_WW"] = self._diagWtW_WW
return self._diagWtW_WW

Expand Down Expand Up @@ -1603,7 +1603,7 @@ def diagWtW_WW(self):
if "diagWtW_WW" not in self._cache:
wt = self.sparse.transpose()
w = self.sparse
self._diagWtW_WW = (wt * w + w * w).diagonal()
self._diagWtW_WW = (wt @ w + w @ w).diagonal()
self._cache["diagWtW_WW"] = self._diagWtW_WW
return self._diagWtW_WW

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ dependencies = [
"pandas>=1.4",
"platformdirs>=2.0.2",
"requests>=2.27",
"scipy>=1.8",
"scipy>=1.12",
"shapely>=2.0.1",
"scikit-learn>=1.1",
]
Expand Down