// Copyright (C) 2011 Zeno Gantner, Chris Newell
//
//This file is part of MyMediaLite.
//
//MyMediaLite is free software: you can redistribute it and/or modify
//it under the terms of the GNU General Public License as published by
//the Free Software Foundation, either version 3 of the License, or
//(at your option) any later version.
//
//MyMediaLite is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//GNU General Public License for more details.
//
//You should have received a copy of the GNU General Public License
//along with MyMediaLite. If not, see <http://www.gnu.org/licenses/>.
package org.mymedialite.eval;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import org.mymedialite.IRecommender;
import org.mymedialite.itemrec.IIncrementalItemRecommender;
import org.mymedialite.util.Utils;
import org.mymedialite.data.IPosOnlyFeedback;
import org.mymedialite.data.PosOnlyFeedback;
import org.mymedialite.datatype.SparseBooleanMatrix;
/**
* Online evaluation for rankings of items
* @version 2.03
*/
public class ItemsOnline {
//TODO consider micro- (by item) and macro-averaging (by user, the current thing); repeated events
/**
* Online evaluation for rankings of items.
* @param recommender the item recommender to be evaluated
* @param test test cases
* @param training training data (must be connected to the recommender's training data)
* @param test_users a list of all test user IDs
* @param candidate_items a list of all candidate item IDs
* @param candidate_item_mode the mode used to determine the candidate items
* @return a dictionary containing the evaluation results (averaged by user)
*/
public static HashMap<String, Double> evaluate(
IRecommender recommender,
IPosOnlyFeedback test,
IPosOnlyFeedback training,
List<Integer> test_users,
List<Integer> candidate_items,
CandidateItems candidate_item_mode) {
if (!(recommender instanceof IIncrementalItemRecommender))
throw new IllegalArgumentException("recommender must be of type IIncrementalItemRecommender");
IIncrementalItemRecommender incremental_recommender = (IIncrementalItemRecommender)recommender;
// prepare candidate items once to avoid recreating them
if(candidate_item_mode.equals(CandidateItems.TRAINING))
candidate_items = training.allItems();
else if(candidate_item_mode.equals(CandidateItems.TEST))
candidate_items = test.allItems();
else if(candidate_item_mode.equals(CandidateItems.OVERLAP))
candidate_items = new ArrayList<Integer>(Utils.intersect(test.allItems(), training.allItems()));
else if(candidate_item_mode.equals(CandidateItems.UNION))
candidate_items = new ArrayList<Integer>(Utils.union(test.allItems(), training.allItems()));
candidate_item_mode = CandidateItems.EXPLICIT;
// For better handling, move test data points into arrays.
int[] users = new int[test.size()];
int[] items = new int[test.size()];
int pos = 0;
for (int user_id : test.userMatrix().nonEmptyColumnIDs()) {
for (int item_id : test.userMatrix().get(user_id)) {
users[pos] = user_id;
items[pos] = item_id;
pos++;
}
}
// Random order of the test data points.
List<Integer> random_index = new ArrayList<Integer>(test.size());
for (int index = 0; index < random_index.size(); index++) random_index.add(index, index);
Collections.shuffle(random_index);
HashMap<Integer, HashMap<String, Double>> results_by_user = new HashMap<Integer, HashMap<String, Double>>();
for (int index : random_index) {
if (test_users.contains(users[index]) && candidate_items.contains(items[index])) {
// Evaluate user.
PosOnlyFeedback<SparseBooleanMatrix> current_test = null;
try {
current_test = new PosOnlyFeedback<SparseBooleanMatrix>(SparseBooleanMatrix.class);
} catch (Exception e) {
e.printStackTrace();
}
current_test.add(users[index], items[index]);
HashMap<String, Double> current_result = evaluate(recommender, current_test, training, current_test.allUsers(), candidate_items,candidate_item_mode);
if (current_result.get("num_users") == 1) {
HashMap<String, Double> result = results_by_user.get(users[index]);
if (results_by_user.containsKey(users[index])) {
for (String measure : Items.getMeasures()) {
result.put(measure, result.get(measure) + current_result.get(measure));
}
result.put("num_items", result.get("num_items") + 1);
} else {
results_by_user.put(users[index], current_result);
current_result.put("num_items", 1D);
current_result.remove("num_users");
}
}
}
// Update recommender
incremental_recommender.addFeedback(users[index], items[index]);
}
HashMap<String, Double> results = new HashMap<String, Double>();
for (String measure : Items.getMeasures())
results.put(measure, 0D);
for (int u : results_by_user.keySet()) {
HashMap<String, Double> userResult = results_by_user.get(u);
for (String measure : Items.getMeasures()) {
results.put(measure, userResult.get(measure) / userResult.get("num_items"));
}
}
for (String measure : Items.getMeasures()) {
results.put(measure, results.get(measure) / results_by_user.size());
}
results.put("num_users", new Double(results_by_user.size()));
results.put("num_items", new Double(candidate_items.size()));
results.put("num_lists", new Double(test.size())); // FIXME this is not exact
return results;
}
}