import numpy as np import pytest import scipy.sparse as sp from sklearn.metrics import roc_auc_score from lightfm.lightfm import LightFM from lightfm import evaluation def _generate_data(num_users, num_items, density=0.1, test_fraction=0.2): # Generate a dataset where every user has interactions # in both the train and the test set. train = sp.lil_matrix((num_users, num_items), dtype=np.float32) test = sp.lil_matrix((num_users, num_items), dtype=np.float32) for user_id in range(num_users): positives = np.random.choice( num_items, size=int(density * num_items), replace=False ) for item_id in positives[: int(test_fraction * len(positives))]: test[user_id, item_id] = 1.0 for item_id in positives[int(test_fraction * len(positives)) :]: train[user_id, item_id] = 1.0 return train.tocoo(), test.tocoo() def _precision_at_k( model, ground_truth, k, train=None, user_features=None, item_features=None ): # Alternative test implementation ground_truth = ground_truth.tocsr() no_users, no_items = ground_truth.shape pid_array = np.arange(no_items, dtype=np.int32) precisions = [] uid_array = np.empty(no_items, dtype=np.int32) if train is not None: train = train.tocsr() for user_id, row in enumerate(ground_truth): uid_array.fill(user_id) predictions = model.predict( uid_array, pid_array, user_features=user_features, item_features=item_features, num_threads=4, ) if train is not None: train_items = train[user_id].indices top_k = set( [x for x in np.argsort(-predictions) if x not in train_items][:k] ) else: top_k = set(np.argsort(-predictions)[:k]) true_pids = set(row.indices[row.data == 1]) if true_pids: precisions.append(len(top_k & true_pids) / float(k)) return sum(precisions) / len(precisions) def _recall_at_k( model, ground_truth, k, train=None, user_features=None, item_features=None ): # Alternative test implementation ground_truth = ground_truth.tocsr() no_users, no_items = ground_truth.shape pid_array = np.arange(no_items, dtype=np.int32) recalls = [] uid_array = np.empty(no_items, dtype=np.int32) if train is not None: train = train.tocsr() for user_id, row in enumerate(ground_truth): uid_array.fill(user_id) predictions = model.predict( uid_array, pid_array, user_features=user_features, item_features=item_features, num_threads=4, ) if train is not None: train_items = train[user_id].indices top_k = set( [x for x in np.argsort(-predictions) if x not in train_items][:k] ) else: top_k = set(np.argsort(-predictions)[:k]) true_pids = set(row.indices[row.data == 1]) if true_pids: recalls.append(len(top_k & true_pids) / float(len(true_pids))) return sum(recalls) / len(recalls) def _auc(model, ground_truth, train=None, user_features=None, item_features=None): ground_truth = ground_truth.tocsr() no_users, no_items = ground_truth.shape pid_array = np.arange(no_items, dtype=np.int32) scores = [] if train is not None: train = train.tocsr() for user_id, row in enumerate(ground_truth): uid_array = np.empty(no_items, dtype=np.int32) uid_array.fill(user_id) predictions = model.predict( uid_array, pid_array, user_features=user_features, item_features=item_features, num_threads=4, ) true_pids = row.indices[row.data == 1] grnd = np.zeros(no_items, dtype=np.int32) grnd[true_pids] = 1 if not len(true_pids): continue if train is not None: train_indices = train[user_id].indices not_in_train = np.array([x not in train_indices for x in range(no_items)]) scores.append(roc_auc_score(grnd[not_in_train], predictions[not_in_train])) else: scores.append(roc_auc_score(grnd, predictions)) return scores def test_precision_at_k(): no_users, no_items = (10, 100) train, test = _generate_data(no_users, no_items) model = LightFM(loss="bpr") # We want a high precision to catch the k=1 case model.fit_partial(test) for k in (10, 5, 1): # Without omitting train interactions precision = evaluation.precision_at_k(model, test, k=k) expected_mean_precision = _precision_at_k(model, test, k) assert np.allclose(precision.mean(), expected_mean_precision) assert len(precision) == (test.getnnz(axis=1) > 0).sum() assert ( len(evaluation.precision_at_k(model, train, preserve_rows=True)) == test.shape[0] ) # With omitting train interactions precision = evaluation.precision_at_k( model, test, k=k, train_interactions=train ) expected_mean_precision = _precision_at_k(model, test, k, train=train) assert np.allclose(precision.mean(), expected_mean_precision) def test_precision_at_k_with_ties(): no_users, no_items = (10, 100) train, test = _generate_data(no_users, no_items) model = LightFM(loss="bpr") model.fit_partial(train) # Make all predictions zero model.user_embeddings = np.zeros_like(model.user_embeddings) model.item_embeddings = np.zeros_like(model.item_embeddings) model.user_biases = np.zeros_like(model.user_biases) model.item_biases = np.zeros_like(model.item_biases) k = 10 precision = evaluation.precision_at_k(model, test, k=k) # Pessimistic precision with all ties assert precision.mean() == 0.0 def test_recall_at_k(): no_users, no_items = (10, 100) train, test = _generate_data(no_users, no_items) model = LightFM(loss="bpr") model.fit_partial(test) for k in (10, 5, 1): # Without omitting train interactions recall = evaluation.recall_at_k(model, test, k=k) expected_mean_recall = _recall_at_k(model, test, k) assert np.allclose(recall.mean(), expected_mean_recall) assert len(recall) == (test.getnnz(axis=1) > 0).sum() assert ( len(evaluation.recall_at_k(model, train, preserve_rows=True)) == test.shape[0] ) # With omitting train interactions recall = evaluation.recall_at_k(model, test, k=k, train_interactions=train) expected_mean_recall = _recall_at_k(model, test, k, train=train) assert np.allclose(recall.mean(), expected_mean_recall) def test_auc_score(): no_users, no_items = (10, 100) train, test = _generate_data(no_users, no_items) model = LightFM(loss="bpr") model.fit_partial(train) auc = evaluation.auc_score(model, test, num_threads=2) expected_auc = np.array(_auc(model, test)) assert auc.shape == expected_auc.shape assert np.abs(auc.mean() - expected_auc.mean()) < 0.01 assert len(auc) == (test.getnnz(axis=1) > 0).sum() assert len(evaluation.auc_score(model, train, preserve_rows=True)) == test.shape[0] # With omitting train interactions auc = evaluation.auc_score(model, test, train_interactions=train, num_threads=2) expected_auc = np.array(_auc(model, test, train)) assert np.abs(auc.mean() - expected_auc.mean()) < 0.01 def test_intersections_check(): no_users, no_items = (10, 100) train, test = _generate_data(no_users, no_items) model = LightFM(loss="bpr") model.fit_partial(train) # check error is raised when train and test have interactions in common with pytest.raises(ValueError): evaluation.auc_score( model, train, train_interactions=train, check_intersections=True ) with pytest.raises(ValueError): evaluation.recall_at_k( model, train, train_interactions=train, check_intersections=True ) with pytest.raises(ValueError): evaluation.precision_at_k( model, train, train_interactions=train, check_intersections=True ) with pytest.raises(ValueError): evaluation.reciprocal_rank( model, train, train_interactions=train, check_intersections=True ) # check no errors raised when train and test have no interactions in common evaluation.auc_score( model, test, train_interactions=train, check_intersections=True ) evaluation.recall_at_k( model, test, train_interactions=train, check_intersections=True ) evaluation.precision_at_k( model, test, train_interactions=train, check_intersections=True ) evaluation.reciprocal_rank( model, test, train_interactions=train, check_intersections=True ) # check no error is raised when there are intersections but flag is False evaluation.auc_score( model, train, train_interactions=train, check_intersections=False ) evaluation.recall_at_k( model, train, train_interactions=train, check_intersections=False ) evaluation.precision_at_k( model, train, train_interactions=train, check_intersections=False ) evaluation.reciprocal_rank( model, train, train_interactions=train, check_intersections=False )