@@ -110,6 +110,169 @@ def test_mf_hinge_classification_fits(mf_data):
110110 assert accuracy > 0.5 , f"Hinge-loss MF accuracy ({ accuracy :.3f} ) should be > 0.5"
111111
112112
113+ def test_mf_data_validation_errors ():
114+ """Test data validation raises appropriate errors."""
115+ # Test X with wrong shape (not 2 columns)
116+ with pytest .raises (ValueError , match = "X must have shape" ):
117+ model = plqMF_Ridge (n_users = 10 , n_items = 10 , loss = {"name" : "mae" })
118+ model .fit (np .array ([[0 , 0 , 0 ]]), np .array ([1.0 ]))
119+
120+ # Test X and y mismatch
121+ with pytest .raises (ValueError , match = "X and y must have the same number" ):
122+ model = plqMF_Ridge (n_users = 10 , n_items = 10 , loss = {"name" : "mae" })
123+ model .fit (np .array ([[0 , 0 ]]), np .array ([1.0 , 2.0 ]))
124+
125+ # Test invalid user ID (negative)
126+ with pytest .raises (ValueError , match = "User IDs must be in" ):
127+ model = plqMF_Ridge (n_users = 10 , n_items = 10 , loss = {"name" : "mae" })
128+ model .fit (np .array ([[- 1 , 0 ]]), np .array ([1.0 ]))
129+
130+ # Test invalid user ID (>= n_users)
131+ with pytest .raises (ValueError , match = "User IDs must be in" ):
132+ model = plqMF_Ridge (n_users = 10 , n_items = 10 , loss = {"name" : "mae" })
133+ model .fit (np .array ([[10 , 0 ]]), np .array ([1.0 ]))
134+
135+ # Test invalid item ID (negative)
136+ with pytest .raises (ValueError , match = "Item IDs must be in" ):
137+ model = plqMF_Ridge (n_users = 10 , n_items = 10 , loss = {"name" : "mae" })
138+ model .fit (np .array ([[0 , - 1 ]]), np .array ([1.0 ]))
139+
140+ # Test invalid item ID (>= n_items)
141+ with pytest .raises (ValueError , match = "Item IDs must be in" ):
142+ model = plqMF_Ridge (n_users = 10 , n_items = 10 , loss = {"name" : "mae" })
143+ model .fit (np .array ([[0 , 10 ]]), np .array ([1.0 ]))
144+
145+
146+ def test_mf_cold_start_users_items ():
147+ """Test cold start handling: users/items with no interactions."""
148+ # Create data where user 0 and item 0 have no interactions
149+ # n_users=3, n_items=3, but only users 1,2 and items 1,2 interact
150+ X = np .array ([[1 , 1 ], [1 , 2 ], [2 , 1 ], [2 , 2 ]])
151+ y = np .array ([3.0 , 4.0 , 2.0 , 5.0 ])
152+
153+ model = plqMF_Ridge (
154+ n_users = 3 ,
155+ n_items = 3 ,
156+ loss = {"name" : "mae" },
157+ rank = 2 ,
158+ C = 0.1 ,
159+ max_iter = 1000 ,
160+ tol = 0.01 ,
161+ )
162+ model .fit (X , y )
163+
164+ # Cold start user (user 0) should have zero factors and bias
165+ assert np .allclose (model .P [0 , :], 0.0 )
166+ assert model .bu [0 ] == 0.0
167+
168+ # Cold start item (item 0) should have zero factors and bias
169+ assert np .allclose (model .Q [0 , :], 0.0 )
170+ assert model .bi [0 ] == 0.0
171+
172+
173+ def test_mf_biased_false ():
174+ """Test plqMF_Ridge with biased=False (no bias terms)."""
175+ n_users , n_items = 20 , 30
176+ X = np .array ([[0 , 0 ], [0 , 1 ], [1 , 0 ], [1 , 1 ], [2 , 2 ], [3 , 3 ]])
177+ y = np .array ([1.0 , 2.0 , 3.0 , 4.0 , 5.0 , 6.0 ])
178+
179+ model = plqMF_Ridge (
180+ n_users = n_users ,
181+ n_items = n_items ,
182+ loss = {"name" : "mae" },
183+ biased = False ,
184+ rank = 3 ,
185+ C = 0.1 ,
186+ max_iter = 1000 ,
187+ tol = 0.01 ,
188+ )
189+ model .fit (X , y )
190+
191+ # bu and bi should be None when biased=False
192+ assert model .bu is None
193+ assert model .bi is None
194+
195+ # decision_function should work without biases
196+ scores = model .decision_function (X )
197+ assert scores .shape == (len (X ),)
198+
199+ # obj should work without biases
200+ loss_term , obj_val = model .obj (X , y )
201+ assert np .isfinite (loss_term )
202+ assert np .isfinite (obj_val )
203+
204+
205+ def test_mf_verbose_output (capsys ):
206+ """Test verbose printing (lines 308, 464-466)."""
207+ X = np .array ([[0 , 0 ], [0 , 1 ], [1 , 0 ], [1 , 1 ]])
208+ y = np .array ([1.0 , 2.0 , 3.0 , 4.0 ])
209+
210+ # Test verbose=1 (CD iteration progress)
211+ model = plqMF_Ridge (
212+ n_users = 2 ,
213+ n_items = 2 ,
214+ loss = {"name" : "mae" },
215+ rank = 2 ,
216+ C = 0.1 ,
217+ max_iter = 500 ,
218+ tol = 0.01 ,
219+ max_iter_CD = 2 ,
220+ verbose = 1 ,
221+ )
222+ model .fit (X , y )
223+ captured = capsys .readouterr ()
224+ assert "Iteration" in captured .out
225+ assert "Average Loss" in captured .out
226+
227+
228+ def test_mf_convergence_warning ():
229+ """Test convergence warning when max_iter is too small."""
230+ from sklearn .exceptions import ConvergenceWarning
231+
232+ X = np .array ([[0 , 0 ], [0 , 1 ], [1 , 0 ], [1 , 1 ]])
233+ y = np .array ([1.0 , 2.0 , 3.0 , 4.0 ])
234+
235+ model = plqMF_Ridge (
236+ n_users = 2 ,
237+ n_items = 2 ,
238+ loss = {"name" : "mae" },
239+ rank = 2 ,
240+ C = 0.1 ,
241+ max_iter = 1 , # Only 1 iteration to guarantee non-convergence
242+ tol = 1e-10 ,
243+ max_iter_CD = 1 ,
244+ )
245+ with pytest .warns (ConvergenceWarning , match = "ReHLine failed to converge" ):
246+ model .fit (X , y )
247+
248+
249+ def test_mf_param_validation_errors ():
250+ """Test parameter validation raises appropriate errors."""
251+ # Test invalid rho (must be between 0 and 1)
252+ with pytest .raises (ValueError , match = "rho must be between 0 and 1" ):
253+ model = plqMF_Ridge (n_users = 10 , n_items = 10 , loss = {"name" : "mae" }, rho = 0.0 )
254+ model .fit (np .array ([[0 , 0 ]]), np .array ([1.0 ]))
255+
256+ with pytest .raises (ValueError , match = "rho must be between 0 and 1" ):
257+ model = plqMF_Ridge (n_users = 10 , n_items = 10 , loss = {"name" : "mae" }, rho = 1.0 )
258+ model .fit (np .array ([[0 , 0 ]]), np .array ([1.0 ]))
259+
260+ # Test invalid C (must be positive)
261+ with pytest .raises (ValueError , match = "C must be positive" ):
262+ model = plqMF_Ridge (n_users = 10 , n_items = 10 , loss = {"name" : "mae" }, C = 0.0 )
263+ model .fit (np .array ([[0 , 0 ]]), np .array ([1.0 ]))
264+
265+ # Test invalid tol_CD (must be positive)
266+ with pytest .raises (ValueError , match = "tol_CD must be positive" ):
267+ model = plqMF_Ridge (n_users = 10 , n_items = 10 , loss = {"name" : "mae" }, tol_CD = 0.0 )
268+ model .fit (np .array ([[0 , 0 ]]), np .array ([1.0 ]))
269+
270+ # Test invalid tol (must be positive)
271+ with pytest .raises (ValueError , match = "tol must be positive" ):
272+ model = plqMF_Ridge (n_users = 10 , n_items = 10 , loss = {"name" : "mae" }, tol = 0.0 )
273+ model .fit (np .array ([[0 , 0 ]]), np .array ([1.0 ]))
274+
275+
113276def test_mf_nonneg_constraint (mf_data ):
114277 """plqMF_Ridge with non-negative constraints should produce non-negative factors."""
115278 d = mf_data
0 commit comments