Skip to content

Commit 3dd450e

Browse files
Julien RousselJulien Roussel
authored andcommitted
doc tests made more robust
1 parent 2e5dcb6 commit 3dd450e

2 files changed

Lines changed: 70 additions & 37 deletions

File tree

qolmat/imputations/softimpute.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,13 @@ class SoftImpute(BaseEstimator, TransformerMixin):
5959
>>> from qolmat.imputations.softimpute import SoftImpute
6060
>>> D = np.array([[1, 2, np.nan, 4], [1, 5, 3, np.nan], [4, 2, 3, 2], [1, 1, 5, 4]])
6161
>>> Omega = ~np.isnan(D)
62-
>>> M, A = SoftImpute(random_state=11, tau=1).decompose(D, Omega)
63-
>>> print(M + A)
64-
[[1. 2. 3.04868607 4. ]
65-
[1. 5. 3. 3.37501463]
66-
[4. 2. 3. 2. ]
67-
[1. 1. 5. 4. ]]
68-
>>> print(SoftImpute.cost_function(D, M, A, Omega, tau=1))
69-
18.520174964466026
62+
>>> M, A = SoftImpute(random_state=10, tau=1).decompose(D, Omega)
63+
>>> naive_cost = SoftImpute.cost_function(
64+
... D, np.where(Omega, M, 0), np.zeros_like(M), Omega, tau=1
65+
... )
66+
>>> minimal_cost = SoftImpute.cost_function(D, M, A, Omega, tau=1)
67+
>>> minimal_cost < naive_cost
68+
np.True_
7069
7170
"""
7271

tests/imputations/test_softimpute.py

Lines changed: 63 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,59 +2,74 @@
22
import pytest
33
from numpy.typing import NDArray
44

5-
from qolmat.imputations import softimpute
5+
from qolmat.imputations.softimpute import SoftImpute
66

7-
X = np.random.rand(100, 100)
8-
X[np.random.choice(100, 10), np.random.choice(100, 10)] = np.nan
9-
X_non_regression_test = np.array(
10-
[[1, 2, np.nan, 4], [1, 5, 3, np.nan], [4, 2, 3, 2], [1, 1, 5, 4]]
11-
)
12-
X_expected = np.array([[1, 2, 2.9066, 4], [1, 5, 3, 2.1478], [4, 2, 3, 2], [1, 1, 5, 4]])
13-
tau = 1
14-
max_iterations = 30
15-
random_state = 50
7+
8+
@pytest.fixture
9+
def X_random() -> NDArray:
10+
"""Generate random matrix with missing values."""
11+
rng = np.random.RandomState(42)
12+
X = rng.rand(100, 100)
13+
X[rng.choice(100, 10), rng.choice(100, 10)] = np.nan
14+
return X
15+
16+
17+
@pytest.fixture
18+
def X_non_regression() -> NDArray:
19+
"""Get small test matrix for non-regression tests."""
20+
return np.array([[1, 2, np.nan, 4], [1, 5, 3, np.nan], [4, 2, 3, 2], [1, 1, 5, 4]])
21+
22+
23+
@pytest.fixture
24+
def X_expected() -> NDArray:
25+
"""Get expected imputed values for non-regression test."""
26+
return np.array([[1, 2, 2.9066, 4], [1, 5, 3, 2.1478], [4, 2, 3, 2], [1, 1, 5, 4]])
27+
28+
29+
@pytest.fixture
30+
def default_params() -> dict:
31+
"""Get default parameters for SoftImpute."""
32+
return {"tau": 1, "max_iterations": 30, "random_state": 50}
1633

1734

1835
def test_initialized_default() -> None:
1936
"""Test that initialization does not crash and has default parameters."""
20-
model = softimpute.SoftImpute()
37+
model = SoftImpute()
2138
assert model.period == 1
2239
assert model.rank is None
2340
assert model.tolerance == 1e-05
2441

2542

2643
def test_initialized_custom() -> None:
2744
"""Test that initialization does not crash and has custom parameters."""
28-
model = softimpute.SoftImpute(period=2, rank=10)
45+
model = SoftImpute(period=2, rank=10)
2946
assert model.period == 2
3047
assert model.rank == 10
3148
assert model.tau is None
3249

3350

34-
@pytest.mark.parametrize("X", [X])
35-
def test_soft_impute_decompose(X: NDArray) -> None:
51+
def test_soft_impute_decompose(X_random: NDArray, default_params: dict) -> None:
3652
"""Test fit instance and decomposition is computed."""
37-
tau = 1
38-
model = softimpute.SoftImpute(tau=tau)
39-
Omega = ~np.isnan(X)
40-
X_imputed = np.where(Omega, X, 0)
41-
cost_all_in_M = model.cost_function(X, X_imputed, np.full_like(X, 0), Omega, tau)
42-
cost_all_in_A = model.cost_function(X, np.full_like(X, 0), X_imputed, Omega, tau)
43-
M, A = model.decompose(X, Omega)
44-
cost_final = model.cost_function(X, M, A, Omega, tau)
45-
assert isinstance(model, softimpute.SoftImpute)
46-
assert M.shape == X.shape
47-
assert A.shape == X.shape
53+
tau = default_params["tau"]
54+
model = SoftImpute(tau=tau)
55+
Omega = ~np.isnan(X_random)
56+
X_imputed = np.where(Omega, X_random, 0)
57+
cost_all_in_M = model.cost_function(X_random, X_imputed, np.full_like(X_random, 0), Omega, tau)
58+
cost_all_in_A = model.cost_function(X_random, np.full_like(X_random, 0), X_imputed, Omega, tau)
59+
M, A = model.decompose(X_random, Omega)
60+
cost_final = model.cost_function(X_random, M, A, Omega, tau)
61+
assert isinstance(model, SoftImpute)
62+
assert M.shape == X_random.shape
63+
assert A.shape == X_random.shape
4864
assert not np.any(np.isnan(M))
4965
assert not np.any(np.isnan(A))
5066
assert cost_final < cost_all_in_M
5167
assert cost_final < cost_all_in_A
5268

5369

54-
@pytest.mark.parametrize("X", [X])
55-
def test_soft_impute_convergence(X: NDArray) -> None:
70+
def test_soft_impute_convergence() -> None:
5671
"""Test type of the check convergence."""
57-
model = softimpute.SoftImpute()
72+
model = SoftImpute()
5873
M = model.random_state.uniform(size=(10, 20))
5974
U, D, V = np.linalg.svd(M, full_matrices=False)
6075
ratio = model._check_convergence(U, D, V.T, U, D, V.T)
@@ -63,7 +78,7 @@ def test_soft_impute_convergence(X: NDArray) -> None:
6378

6479
def test_soft_impute_convergence_with_none() -> None:
6580
"""Test check type None and raise error."""
66-
model = softimpute.SoftImpute()
81+
model = SoftImpute()
6782
with pytest.raises(ValueError):
6883
_ = model._check_convergence(
6984
np.array([1]),
@@ -73,3 +88,22 @@ def test_soft_impute_convergence_with_none() -> None:
7388
np.array([1]),
7489
np.array([1]),
7590
)
91+
92+
93+
def test_decompose_loss_minimized(X_random: NDArray, default_params: dict) -> None:
94+
"""Test that the loss function is at a local minimum."""
95+
tau = default_params["tau"]
96+
imputer = SoftImpute(random_state=123, tau=tau)
97+
Omega = ~np.isnan(X_random)
98+
M, A = imputer.decompose(X_random, Omega)
99+
X_imputed = M + A
100+
cost_imputed = SoftImpute.cost_function(X_imputed, M, A, Omega, tau)
101+
for i in range(10):
102+
Delta = 1.1 ** (i - 9) * imputer.random_state.uniform(0, 1, size=X_random.shape)
103+
X_perturbed = X_imputed + Delta
104+
cost_perturbed = SoftImpute.cost_function(X_perturbed, M, A, Omega, tau)
105+
assert cost_perturbed > cost_imputed
106+
M = np.zeros(X_random.shape)
107+
A = X_random.copy()
108+
cost_perturbed = SoftImpute.cost_function(X_random, M, A, Omega, tau)
109+
assert cost_perturbed > cost_imputed

0 commit comments

Comments
 (0)