22import pytest
33from 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
1835def 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
2643def 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
6479def 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