Skip to content

Commit eee02bc

Browse files
committed
fix cov error
1 parent 5b4f691 commit eee02bc

1 file changed

Lines changed: 163 additions & 0 deletions

File tree

tests/test_mf.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
113276
def 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

Comments
 (0)