public class DataModelFactory {

    public static <U, I> DataModelIF<U, I> getDefaultModel() {
        return getSimpleModel();
    }

    public static <U, I> TemporalDataModelIF<U, I> getDefaultTemporalModel() {
        return getSimpleTemporalModel();
    }

    public static <U, I> DataModelIF<U, I> getSimpleModel() {
        return new DataModel<>();
    }

    public static <U, I> TemporalDataModelIF<U, I> getSimpleTemporalModel() {
        return new TemporalDataModel<>();
    }

    public static TemporalDataModelIF<Long, Long> getMahoutTemporalModel() {
        return new MahoutDataModel();
    }

    public static <U, I> DataModelIF<U, I> getRankSysModel() {
        return new RankSysDataModel<>();
    }
} public interface DataModelIF<U, I> {

    /**
     * Method that returns the preference between a user and an item.
     *
     * @param u the user.
     * @param i the item.
     * @return the preference between a user and an item.
     */
    public Double getUserItemPreference(U u, I i);

    /**
     * Method that returns the items of a user.
     *
     * @param u the user.
     * @return the items of a user.
     */
    public Iterable<I> getUserItems(U u);

    /**
     * Method that adds a preference to the model between a user and an item.
     *
     * @param u the user.
     * @param i the item.
     * @param d the preference.
     */
    public void addPreference(final U u, final I i, final Double d);

    /**
     * Method that returns the items in the model.
     *
     * @return the items in the model.
     */
    public Iterable<I> getItems();

    /**
     * Method that returns the users in the model.
     *
     * @return the users in the model.
     */
    public Iterable<U> getUsers();

    /**
     * Method that returns the number of items in the model.
     *
     * @return the number of items in the model.
     */
    public int getNumItems();

    /**
     * Method that returns the number of users in the model.
     *
     * @return the number of users in the model.
     */
    public int getNumUsers();

    /**
     * Method that clears all the maps contained in the model.
     */
    public void clear();
} public final class DataModelUtils {

    /**
     * Utility classes should not have a public constructor.
     */
    private DataModelUtils() {
    }

    /**
     * Method that saves a data model to a file.
     *
     * @param dm the data model
     * @param outfile file where the model will be saved
     * @param overwrite flag that indicates if the file should be overwritten
     * @param delimiter field delimiter
     * @param <U> type of users
     * @param <I> type of items
     * @throws FileNotFoundException when outfile cannot be used.
     * @throws UnsupportedEncodingException when the requested encoding (UTF-8)
     * is not available.
     */
    public static <U, I> void saveDataModel(final DataModelIF<U, I> dm, final String outfile, final boolean overwrite, final String delimiter)
            throws FileNotFoundException, UnsupportedEncodingException {
        if (new File(outfile).exists() && !overwrite) {
            System.out.println("Ignoring " + outfile);
        } else {
            PrintStream out = new PrintStream(outfile, "UTF-8");
            for (U user : dm.getUsers()) {
                for (I item : dm.getUserItems(user)) {
                    Double pref = dm.getUserItemPreference(user, item);
                    out.println(user + delimiter + item + delimiter + pref);
                }
            }
            out.close();
        }
    }

    /**
     * Method that saves a temporal data model to a file.
     *
     * @param dm the data model
     * @param outfile file where the model will be saved
     * @param overwrite flag that indicates if the file should be overwritten
     * @param delimiter field delimiter
     * @param <U> type of users
     * @param <I> type of items
     * @throws FileNotFoundException when outfile cannot be used.
     * @throws UnsupportedEncodingException when the requested encoding (UTF-8)
     * is not available.
     */
    public static <U, I> void saveDataModel(final TemporalDataModelIF<U, I> dm, final String outfile, final boolean overwrite, String delimiter)
            throws FileNotFoundException, UnsupportedEncodingException {
        if (new File(outfile).exists() && !overwrite) {
            System.out.println("Ignoring " + outfile);
        } else {
            PrintStream out = new PrintStream(outfile, "UTF-8");
            for (U user : dm.getUsers()) {
                for (I item : dm.getUserItems(user)) {
                    Double pref = dm.getUserItemPreference(user, item);
                    Iterable<Long> time = dm.getUserItemTimestamps(user, item);
                    if (time == null) {
                        out.println(user + delimiter + item + delimiter + pref + delimiter + "-1");
                    } else {
                        for (Long t : time) {
                            out.println(user + delimiter + item + delimiter + pref + delimiter + t);
                        }
                    }
                }
            }
            out.close();
        }
    }
} public class SimpleParser implements Parser<Long, Long> {

    /**
     * The column index for the user id in the file.
     */
    public static final int USER_TOK = 0;
    /**
     * The column index for the item id in the file.
     */
    public static final int ITEM_TOK = 1;
    /**
     * The column index for the rating in the file.
     */
    public static final int RATING_TOK = 2;
    /**
     * The column index for the time in the file.
     */
    public static final int TIME_TOK = 3;

    /**
     * {@inheritDoc}
     */
    @Override
    public DataModelIF<Long, Long> parseData(final File f) throws IOException {
        return parseData(f, "\t", false);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public TemporalDataModelIF<Long, Long> parseTemporalData(final File f) throws IOException {
        return parseData(f, "\t", true);
    }

    /**
     * Parses a data file with a specific separator between fields.
     *
     * @param f The file to be parsed.
     * @param token The separator to be used.
     * @param isTemporal A flag indicating if the file contains temporal
     * information.
     * @return A dataset created from the file.
     * @throws IOException if the file cannot be read.
     */
    public TemporalDataModelIF<Long, Long> parseData(final File f, final String token, final boolean isTemporal) throws IOException {
        TemporalDataModelIF<Long, Long> dataset = DataModelFactory.getDefaultTemporalModel();

        BufferedReader br = SimpleParser.getBufferedReader(f);
        String line = br.readLine();
        if ((line != null) && (!line.matches(".*[a-zA-Z].*"))) {
            parseLine(line, dataset, token, isTemporal);
        }
        while ((line = br.readLine()) != null) {
            parseLine(line, dataset, token, isTemporal);
        }
        br.close();

        return dataset;
    }

    /**
     * Obtains an instance of BufferedReader depending on the file extension: if
     * it ends with gz, zip, or tgz then a compressed reader is used instead of
     * the standard one.
     *
     * @param f The file to be opened.
     * @return An instance of BufferedReader or null if there is a problem
     * @throws IOException when the file cannot be read.
     * @see BufferedReader
     */
    public static BufferedReader getBufferedReader(final File f) throws IOException {
        BufferedReader br = null;
        if ((f == null) || (!f.isFile())) {
            return br;
        }
        if (f.getName().endsWith(".gz") || f.getName().endsWith(".zip") || f.getName().endsWith(".tgz")) {
            br = new BufferedReader(new InputStreamReader(new GZIPInputStream(new FileInputStream(f)), "UTF-8"));
        } else {
            br = new BufferedReader(new InputStreamReader(new FileInputStream(f), "UTF-8"));
        }
        return br;
    }

    /**
     * Parses line from data file.
     *
     * @param line The line to be parsed.
     * @param dataset The dataset to add data from line to.
     * @param token the token to split on.
     * @param isTemporal A flag indicating if the line contains temporal
     * information.
     */
    private void parseLine(final String line, final TemporalDataModelIF<Long, Long> dataset, final String token, final boolean isTemporal) {
        if (line == null) {
            return;
        }
        String[] toks = line.split(token);
        // user
        long userId = Long.parseLong(toks[USER_TOK]);
        // item
        long itemId = Long.parseLong(toks[ITEM_TOK]);
        // preference
        double preference = Double.parseDouble(toks[RATING_TOK]);
        // timestamp
        long timestamp = -1;
        // allow no timestamp information
        if (isTemporal && toks.length > TIME_TOK) {
            timestamp = Long.parseLong(toks[TIME_TOK]);
        }
        //////
        // update information
        //////
        dataset.addPreference(userId, itemId, preference);
        if (timestamp != -1) {
            dataset.addTimestamp(userId, itemId, timestamp);
        }
    }
} public class UIPParser extends AbstractParser implements Parser<Long, Long> {

    /**
     * Default constructor.
     */
    public UIPParser() {
        super();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public TemporalDataModelIF<Long, Long> parseTemporalData(final File f) throws IOException {
        TemporalDataModelIF<Long, Long> dataset = DataModelFactory.getDefaultTemporalModel();
        Reader in = new InputStreamReader(new FileInputStream(f), "UTF-8");

        Iterable<CSVRecord> records;
        if (isHasHeader()) {
            records = CSVFormat.EXCEL.withDelimiter(getDelimiter()).withHeader().parse(in);
        } else {
            records = CSVFormat.EXCEL.withDelimiter(getDelimiter()).parse(in);
        }
        for (CSVRecord record : records) {
            long userID = Long.parseLong(record.get(getUserTok()));
            long itemID = Long.parseLong(record.get(getItemTok()));
            long timestamp = -1L;
            if (getTimeTok() != -1) {
                timestamp = Long.parseLong(record.get(getTimeTok()));
            }
            double preference = Double.parseDouble(record.get(getPrefTok()));
            dataset.addPreference(userID, itemID, preference);
            dataset.addTimestamp(userID, itemID, timestamp);
        }
        in.close();
        return dataset;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public DataModelIF<Long, Long> parseData(final File f) throws IOException {
        DataModelIF<Long, Long> dataset = new DataModel<>();
        Reader in = new InputStreamReader(new FileInputStream(f), "UTF-8");

        Iterable<CSVRecord> records;
        if (isHasHeader()) {
            records = CSVFormat.EXCEL.withDelimiter(getDelimiter()).withHeader().parse(in);
        } else {
            records = CSVFormat.EXCEL.withDelimiter(getDelimiter()).parse(in);
        }
        for (CSVRecord record : records) {
            long userID = Long.parseLong(record.get(getUserTok()));
            long itemID = Long.parseLong(record.get(getItemTok()));
            double preference = Double.parseDouble(record.get(getPrefTok()));
            dataset.addPreference(userID, itemID, preference);
        }
        in.close();
        return dataset;
    }
} public class RMSE<U, I> extends AbstractErrorMetric<U, I> implements EvaluationMetric<U> {

    /**
     * Default constructor with predictions and groundtruth information.
     *
     * @param predictions predicted scores for users and items
     * @param test groundtruth information for users and items
     */
    public RMSE(final DataModelIF<U, I> predictions, final DataModelIF<U, I> test) {
        super(predictions, test);
    }

    /**
     * Constructor where the error strategy can be initialized.
     *
     * @param predictions predicted scores for users and items
     * @param test groundtruth information for users and items
     * @param errorStrategy the error strategy
     */
    public RMSE(final DataModelIF<U, I> predictions, final DataModelIF<U, I> test, final ErrorStrategy errorStrategy) {
        super(predictions, test, errorStrategy);
    }

    /**
     * Instantiates and computes the RMSE value. Prior to running this, there is
     * no valid value.
     *
     */
    @Override
    public void compute() {
        if (!Double.isNaN(getValue())) {
            // since the data cannot change, avoid re-doing the calculations
            return;
        }
        iniCompute();

        Map<U, List<Double>> data = processDataAsPredictedDifferencesToTest();
        int testItems = 0;
        for (U testUser : getTest().getUsers()) {
            int userItems = 0;
            double umse = 0.0;

            if (data.containsKey(testUser)) {
                for (double difference : data.get(testUser)) {
                    umse += difference * difference;
                    userItems++;
                }
            }

            testItems += userItems;
            setValue(getValue() + umse);
            if (userItems == 0) {
                umse = Double.NaN;
            } else {
                umse = Math.sqrt(umse / userItems);
            }
            getMetricPerUser().put(testUser, umse);
        }
        if (testItems == 0) {
            setValue(Double.NaN);
        } else {
            setValue(Math.sqrt(getValue() / testItems));
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return "RMSE_" + getStrategy();
    }
} public class NDCG<U, I> extends AbstractRankingMetric<U, I> implements EvaluationMetric<U> {

    /**
     * Type of nDCG computation (linear or exponential).
     */
    public static enum TYPE {

        /**
         * Linear.
         */
        LIN,
        /**
         * Exponential.
         */
        EXP,
        /**
         * As implemented in trec_eval.
         */
        TREC_EVAL;
    }
    /**
     * Type of nDCG computation (linear or exponential).
     */
    private TYPE type;
    /**
     * DCG values per user at each cutoff level.
     */
    private Map<Integer, Map<U, Double>> userDcgAtCutoff;
    /**
     * Ideal DCG values per user at each cutoff level.
     */
    private Map<Integer, Map<U, Double>> userIdcgAtCutoff;

    /**
     * Default constructor with predictions and groundtruth information.
     *
     * @param predictions predicted scores for users and items
     * @param test groundtruth information for users and items
     */
    public NDCG(final DataModelIF<U, I> predictions, final DataModelIF<U, I> test) {
        this(predictions, test, new int[]{});
    }

    /**
     * Constructor where the cutoff levels can be initialized.
     *
     * @param predictions predicted ratings
     * @param test groundtruth ratings
     * @param ats cutoffs
     */
    public NDCG(final DataModelIF<U, I> predictions, final DataModelIF<U, I> test, final int[] ats) {
        this(predictions, test, 1.0, ats, TYPE.EXP);
    }

    /**
     * Constructor where the cutoff levels and the type of NDCG computation can
     * be initialized.
     *
     * @param predictions predicted ratings
     * @param test groundtruth ratings
     * @param relThreshold the relevance threshold
     * @param ats cutoffs
     * @param ndcgType type of NDCG computation
     */
    public NDCG(final DataModelIF<U, I> predictions, final DataModelIF<U, I> test, final double relThreshold, final int[] ats, final TYPE ndcgType) {
        super(predictions, test, relThreshold, ats);
        this.type = ndcgType;
    }

    /**
     * Computes the global NDCG by first summing the NDCG for each user and then
     * averaging by the number of users.
     */
    @Override
    public void compute() {
        if (!Double.isNaN(getValue())) {
            // since the data cannot change, avoid re-doing the calculations
            return;
        }
        iniCompute();

        Map<U, List<Pair<I, Double>>> data = processDataAsRankedTestRelevance();
        userDcgAtCutoff = new HashMap<Integer, Map<U, Double>>();
        userIdcgAtCutoff = new HashMap<Integer, Map<U, Double>>();

        int nUsers = 0;
        for (Map.Entry<U, List<Pair<I, Double>>> e : data.entrySet()) {
            U user = e.getKey();
            List<Pair<I, Double>> sortedList = e.getValue();
            double dcg = 0.0;
            int rank = 0;
            for (Pair<I, Double> pair : sortedList) {
                double rel = pair.getSecond();
                rank++;
                dcg += computeDCG(rel, rank);
                // compute at a particular cutoff
                for (int at : getCutoffs()) {
                    if (rank == at) {
                        Map<U, Double> m = userDcgAtCutoff.get(at);
                        if (m == null) {
                            m = new HashMap<U, Double>();
                            userDcgAtCutoff.put(at, m);
                        }
                        m.put(user, dcg);
                    }
                }
            }
            // assign the ndcg of the whole list to those cutoffs larger than the list's size
            for (int at : getCutoffs()) {
                if (rank <= at) {
                    Map<U, Double> m = userDcgAtCutoff.get(at);
                    if (m == null) {
                        m = new HashMap<U, Double>();
                        userDcgAtCutoff.put(at, m);
                    }
                    m.put(user, dcg);
                }
            }
            Map<I, Double> userTest = new HashMap<>();
            for (I i : getTest().getUserItems(user)) {
                userTest.put(i, getTest().getUserItemPreference(user, i));
            }
            double idcg = computeIDCG(user, userTest);
            double undcg = dcg / idcg;
            if (!Double.isNaN(undcg)) {
                setValue(getValue() + undcg);
                getMetricPerUser().put(user, undcg);
                nUsers++;
            }
        }
        setValue(getValue() / nUsers);
    }

    /**
     * Method that computes the discounted cumulative gain of a specific item,
     * taking into account its ranking in a user's list and its relevance value.
     *
     * @param rel the item's relevance
     * @param rank the item's rank in a user's list (sorted by predicted rating)
     * @return the dcg of the item
     */
    protected double computeDCG(final double rel, final int rank) {
        double dcg = 0.0;
        if (rel >= getRelevanceThreshold()) {
            switch (type) {
                default:
                case EXP:
                    dcg = (Math.pow(2.0, rel) - 1.0) / (Math.log(rank + 1) / Math.log(2));
                    break;
                case LIN:
                    dcg = rel;
                    if (rank > 1) {
                        dcg /= (Math.log(rank) / Math.log(2));
                    }
                    break;
                case TREC_EVAL:
                    dcg = rel / (Math.log(rank + 1) / Math.log(2));
                    break;
            }
        }
        return dcg;
    }

    /**
     * Computes the ideal <a
     * href="http://recsyswiki.com/wiki/Discounted_Cumulative_Gain"
     * target="_blank">discounted cumulative gain</a> (IDCG) given the test set
     * (groundtruth items) for a user.
     *
     * @param user the user
     * @param userTestItems the groundtruth items of a user.
     * @return the IDCG
     */
    private double computeIDCG(final U user, final Map<I, Double> userTestItems) {
        double idcg = 0.0;
        // sort the items according to their relevance level
        List<Double> sortedList = rankScores(userTestItems);
        int rank = 1;
        for (double itemRel : sortedList) {
            idcg += computeDCG(itemRel, rank);
            // compute at a particular cutoff
            for (int at : getCutoffs()) {
                if (rank == at) {
                    Map<U, Double> m = userIdcgAtCutoff.get(at);
                    if (m == null) {
                        m = new HashMap<U, Double>();
                        userIdcgAtCutoff.put(at, m);
                    }
                    m.put(user, idcg);
                }
            }
            rank++;
        }
        // assign the ndcg of the whole list to those cutoffs larger than the list's size
        for (int at : getCutoffs()) {
            if (rank <= at) {
                Map<U, Double> m = userIdcgAtCutoff.get(at);
                if (m == null) {
                    m = new HashMap<U, Double>();
                    userIdcgAtCutoff.put(at, m);
                }
                m.put(user, idcg);
            }
        }
        return idcg;
    }

    /**
     * Method to return the NDCG value at a particular cutoff level.
     *
     * @param at cutoff level
     * @return the NDCG corresponding to the requested cutoff level
     */
    @Override
    public double getValueAt(final int at) {
        if (userDcgAtCutoff.containsKey(at) && userIdcgAtCutoff.containsKey(at)) {
            int n = 0;
            double ndcg = 0.0;
            for (U u : userIdcgAtCutoff.get(at).keySet()) {
                double udcg = getValueAt(u, at);
                if (!Double.isNaN(udcg)) {
                    ndcg += udcg;
                    n++;
                }
            }
            if (n == 0) {
                ndcg = 0.0;
            } else {
                ndcg = ndcg / n;
            }
            return ndcg;
        }
        return Double.NaN;
    }

    /**
     * Method to return the NDCG value at a particular cutoff level for a given
     * user.
     *
     * @param user the user
     * @param at cutoff level
     * @return the NDCG corresponding to the requested user at the cutoff level
     */
    @Override
    public double getValueAt(final U user, final int at) {
        if (userDcgAtCutoff.containsKey(at) && userDcgAtCutoff.get(at).containsKey(user)
                && userIdcgAtCutoff.containsKey(at) && userIdcgAtCutoff.get(at).containsKey(user)) {
            double idcg = userIdcgAtCutoff.get(at).get(user);
            double dcg = userDcgAtCutoff.get(at).get(user);
            return dcg / idcg;
        }
        return Double.NaN;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return "NDCG" + type + "_" + getRelevanceThreshold();
    }
} public class Precision<U, I> extends AbstractRankingMetric<U, I> implements EvaluationMetric<U> {

    /**
     * Precision values per user at each cutoff level.
     */
    private Map<Integer, Map<U, Double>> userPrecAtCutoff;

    /**
     * Default constructor with predictions and groundtruth information.
     *
     * @param predictions predicted scores for users and items
     * @param test groundtruth information for users and items
     */
    public Precision(final DataModelIF<U, I> predictions, final DataModelIF<U, I> test) {
        this(predictions, test, 1.0);
    }

    /**
     * Constructor where the relevance threshold can be initialized.
     *
     * @param predictions predicted ratings
     * @param test groundtruth ratings
     * @param relThreshold relevance threshold
     */
    public Precision(final DataModelIF<U, I> predictions, final DataModelIF<U, I> test, final double relThreshold) {
        this(predictions, test, relThreshold, new int[]{});
    }

    /**
     * Constructor where the cutoff levels can be initialized.
     *
     * @param predictions predicted ratings
     * @param test groundtruth ratings
     * @param relThreshold relevance threshold
     * @param ats cutoffs
     */
    public Precision(final DataModelIF<U, I> predictions, final DataModelIF<U, I> test, final double relThreshold, final int[] ats) {
        super(predictions, test, relThreshold, ats);
    }

    /**
     * Computes the global precision by first summing the precision for each
     * user and then averaging by the number of users.
     */
    @Override
    public void compute() {
        if (!Double.isNaN(getValue())) {
            // since the data cannot change, avoid re-doing the calculations
            return;
        }
        iniCompute();

        Map<U, List<Pair<I, Double>>> data = processDataAsRankedTestRelevance();
        userPrecAtCutoff = new HashMap<Integer, Map<U, Double>>();

        int nUsers = 0;
        for (Map.Entry<U, List<Pair<I, Double>>> e : data.entrySet()) {
            U user = e.getKey();
            List<Pair<I, Double>> sortedList = e.getValue();
            double uprec = 0.0;
            int rank = 0;
            for (Pair<I, Double> pair : sortedList) {
                double rel = pair.getSecond();
                rank++;
                uprec += computeBinaryPrecision(rel);
                // compute at a particular cutoff
                for (int at : getCutoffs()) {
                    if (rank == at) {
                        Map<U, Double> m = userPrecAtCutoff.get(at);
                        if (m == null) {
                            m = new HashMap<U, Double>();
                            userPrecAtCutoff.put(at, m);
                        }
                        m.put(user, uprec / rank);
                    }
                }
            }
            // DO NOT assign the precision of the whole list to those cutoffs larger than the list's size
            // instead, we fill with not relevant items until such cutoff
            for (int at : getCutoffs()) {
                if (rank <= at) {
                    Map<U, Double> m = userPrecAtCutoff.get(at);
                    if (m == null) {
                        m = new HashMap<U, Double>();
                        userPrecAtCutoff.put(at, m);
                    }
                    m.put(user, uprec / at);
                }
            }
            // normalize by list size
            uprec /= rank;
            if (!Double.isNaN(uprec)) {
                setValue(getValue() + uprec);
                getMetricPerUser().put(user, uprec);
                nUsers++;
            }
        }
        setValue(getValue() / nUsers);
    }

    /**
     * Method to return the precision value at a particular cutoff level.
     *
     * @param at cutoff level
     * @return the precision corresponding to the requested cutoff level
     */
    @Override
    public double getValueAt(final int at) {
        if (userPrecAtCutoff.containsKey(at)) {
            int n = 0;
            double prec = 0.0;
            for (U u : userPrecAtCutoff.get(at).keySet()) {
                double uprec = getValueAt(u, at);
                if (!Double.isNaN(uprec)) {
                    prec += uprec;
                    n++;
                }
            }
            if (n == 0) {
                prec = 0.0;
            } else {
                prec = prec / n;
            }
            return prec;
        }
        return Double.NaN;
    }

    /**
     * Method to return the precision value at a particular cutoff level for a
     * given user.
     *
     * @param user the user
     * @param at cutoff level
     * @return the precision corresponding to the requested user at the cutoff
     * level
     */
    @Override
    public double getValueAt(final U user, final int at) {
        if (userPrecAtCutoff.containsKey(at) && userPrecAtCutoff.get(at).containsKey(user)) {
            return userPrecAtCutoff.get(at).get(user);
        }
        return Double.NaN;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return "Precision_" + getRelevanceThreshold();
    }
} public interface EvaluationStrategy<U, I> {

    /**
     * Enumeration that defines two output formats: a simple one (tab-separated)
     * and another compatible with the one used by the treceval program.
     */
    public enum OUTPUT_FORMAT {

        /**
         * Tab-separated format.
         */
        SIMPLE,
        /**
         * Format as followed by the trec_eval program.
         */
        TRECEVAL;
    }

    /**
     * Get the items to rank.
     *
     * @param user The user.
     * @return The items to rank.
     */
    Set<I> getCandidateItemsToRank(U user);

    /**
     * Print rankings for a user.
     *
     * @param user The user.
     * @param scoredItems The scored items to print.
     * @param out Where to print.
     * @param format The format of the printer (see {@link OUTPUT_FORMAT}).
     */
    void printRanking(U user, List<Pair<I, Double>> scoredItems, PrintStream out, OUTPUT_FORMAT format);

    /**
     * Print the ground truth.
     *
     * @param user The user.
     * @param out Where to print.
     * @param format The format of the printer (see {@link OUTPUT_FORMAT}).
     */
    void printGroundtruth(U user, PrintStream out, OUTPUT_FORMAT format);
} public class DataDownloader {

    /**
     * The URL.
     */
    private String url;
    /**
     * The folder where the file will be unzipped it.
     */
    private String folder;

    /**
     * Default constructor.
     *
     * @param theUrl the URL from where the file will be downloaded
     * @param theFolder the folder where the file will be uncompressed
     */
    public DataDownloader(final String theUrl, final String theFolder) {
        this.url = theUrl;
        this.folder = theFolder;
    }

    /**
     * Main method.
     *
     * @param args argument (not used)
     */
    public static void main(final String[] args) {
        String url = "http://files.grouplens.org/datasets/movielens/ml-100k.zip";
        String folder = "data3/ml-100k";
        DataDownloader dd = new DataDownloader(url, folder);
        dd.downloadAndUnzip();
    }

    /**
     * Downloads the file from the provided url.
     */
    public void download() {
        URL dataURL = null;
        String fileName = folder + "/" + url.substring(url.lastIndexOf("/") + 1);
        if (new File(fileName).exists()) {
            return;
        }
        try {
            dataURL = new URL(url);
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }

        File downloadedData = new File(fileName);
        try {
            assert dataURL != null;
            FileUtils.copyURLToFile(dataURL, downloadedData);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * Downloads the file from the provided url and uncompresses it to the given
     * folder.
     */
    public void downloadAndUnzip() {
        URL dataURL = null;
        String fileName = folder + "/" + url.substring(url.lastIndexOf("/") + 1);
        File compressedData = new File(fileName);
        if (!new File(fileName).exists()) {
            try {
                dataURL = new URL(url);
            } catch (MalformedURLException e) {
                e.printStackTrace();
            }
            try {
                assert dataURL != null;
                FileUtils.copyURLToFile(dataURL, compressedData);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        try {
            ZipFile zipFile = new ZipFile(compressedData);
            File dataFolder = new File(folder);
            zipFile.extractAll(dataFolder.getCanonicalPath());
        } catch (ZipException | IOException e) {
            e.printStackTrace();
        }
    }
} public final class RecommenderIO {

    /**
     * Utility classes should not have a public or default constructor.
     */
    private RecommenderIO() {
    }

    /**
     * Write recommendations to file.
     *
     * @param user the user
     * @param recommendations the recommendations
     * @param path directory where fileName will be written (if not null)
     * @param fileName name of the file, if null recommendations will not be
     * printed
     * @param append flag to decide if recommendations should be appended to
     * file
     * @param model if not null, recommendations will be saved here
     */
    public static void writeData(final long user, final List<Preference<Long, Long>> recommendations, final String path, final String fileName, final boolean append, final TemporalDataModelIF<Long, Long> model) {
        BufferedWriter out = null;
        try {
            File dir = null;
            if (path != null) {
                dir = new File(path);
                if (!dir.isDirectory()) {
                    if (!dir.mkdir() && (fileName != null)) {
                        System.out.println("Directory " + path + " could not be created");
                        return;
                    }
                }
            }
            if ((path != null) && (fileName != null)) {
                out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(path + "/" + fileName, append), "UTF-8"));
            }
            for (Preference<Long, Long> recItem : recommendations) {
                if (out != null) {
                    out.write(user + "\t" + recItem.getItem()+ "\t" + recItem.getScore() + "\n");
                }
                if (model != null) {
                    model.addPreference(user, recItem.getItem(), recItem.getScore());
                }
            }
            if (out != null) {
                out.flush();
                out.close();
            }
        } catch (IOException e) {
            System.out.println(e.getMessage());
//            logger.error(e.getMessage());
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static class Preference<U, I> {

        private U user;
        private I item;
        private double score;

        public Preference(U user, I item, double score) {
            this.user = user;
            this.item = item;
            this.score = score;
        }

        public U getUser() {
            return user;
        }

        public I getItem() {
            return item;
        }

        public double getScore() {
            return score;
        }

    }
} public final class GenericRecommenderBuilder implements RecommenderBuilder {

    /**
     * Number of neighbors.
     */
    public static final int DEFAULT_N = 50;
    /**
     * Number of neighbors when no neighbors are to be used.
     */
    public static final int NO_N = 0;
    /**
     * Number of iterations.
     */
    public static final int NOITER = 0;
    /**
     * Number of factors.
     */
    public static final int NOFACTORS = 0;

    /**
     * Builds a random recommender which will recommend items from the data
     * model passed as a parameter.
     *
     * @param dataModel the data model
     * @return the recommender
     * @throws TasteException when the recommender is instantiated incorrectly.
     */
    @Override
    public Recommender buildRecommender(final DataModel dataModel)
            throws TasteException {
        return new RandomRecommender(dataModel);
    }

    /**
     * CF recommender with default parameters.
     *
     * @param dataModel the data model
     * @param recType the recommender type (as Mahout class)
     * @return the recommender
     * @throws RecommenderException see {@link #buildRecommender(org.apache.mahout.cf.taste.model.DataModel, java.lang.String, java.lang.String, int, int, int, java.lang.String)}
     */
    public Recommender buildRecommender(final DataModel dataModel, final String recType)
            throws RecommenderException {
        return buildRecommender(dataModel, recType, null, DEFAULT_N, NOFACTORS, NOITER, null);
    }

    /**
     * Recommender based on given recType and simType (with default parameters).
     *
     * @param dataModel the data model
     * @param recType the recommender type (as Mahout class)
     * @param simType the similarity type (as Mahout class)
     * @return the recommender
     * @throws RecommenderException see {@link #buildRecommender(org.apache.mahout.cf.taste.model.DataModel, java.lang.String, java.lang.String, int, int, int, java.lang.String)}
     */
    public Recommender buildRecommender(final DataModel dataModel, final String recType, final String simType)
            throws RecommenderException {
        return buildRecommender(dataModel, recType, simType, DEFAULT_N, NOFACTORS, NOITER, null);
    }

    /**
     * Recommender based on given recType, simType and neighborhood type.
     *
     * @param dataModel the data model
     * @param recType the recommender type (as Mahout class)
     * @param simType the similarity type (as Mahout class)
     * @param nbSize the neighborhood size
     * @return the recommender
     * @throws RecommenderException see {@link #buildRecommender(org.apache.mahout.cf.taste.model.DataModel, java.lang.String, java.lang.String, int, int, int, java.lang.String)}
     */
    public Recommender buildRecommender(final DataModel dataModel, final String recType, final String simType, final int nbSize)
            throws RecommenderException {
        return buildRecommender(dataModel, recType, simType, nbSize, NOFACTORS, NOITER, null);
    }

    /**
     * SVD.
     *
     * @param dataModel the data model
     * @param recType the recommender type (as Mahout class)
     * @param facType the factorizer (as Mahout class)
     * @param iterations number of iterations
     * @param factors number of factors
     * @return the recommender
     * @throws RecommenderException see
     * {@link #buildRecommender(org.apache.mahout.cf.taste.model.DataModel, java.lang.String, java.lang.String, int, int, int, java.lang.String)}
     */
    public Recommender buildRecommender(final DataModel dataModel, final String recType, final String facType, final int iterations, final int factors)
            throws RecommenderException {
        return buildRecommender(dataModel, recType, null, NO_N, factors, iterations, facType);
    }

    /**
     * General method to instantiate a Mahout recommender.
     *
     * @param dataModel the data model
     * @param recType the type of the recommender (as Mahout class)
     * @param similarityType the type of the similarity (as Mahout class), e.g.
     * PearsonCorrelation
     * @param neighborhoodSize the neighborhood size
     * @param factors number of factors
     * @param iterations number of iterations
     * @param facType the factorizer (as Mahout class)
     * @return the recommender
     * @throws RecommenderException when there is a problem with instantiation
     */
    public Recommender buildRecommender(final DataModel dataModel,
            final String recType,
            final String similarityType,
            final int neighborhoodSize,
            final int factors,
            final int iterations,
            final String facType)
            throws RecommenderException {
        String neighborhoodType = "org.apache.mahout.cf.taste.impl.neighborhood.NearestNUserNeighborhood";
        Object simObj = null;
        /**
         * Instantiate similarity class
         */
        if (similarityType != null) {
            Class<?> similarityClass = null;
            try {
                similarityClass = Class.forName(similarityType);
                simObj = similarityClass.getConstructor(DataModel.class).newInstance(dataModel);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
                throw new RecommenderException("Could not create Similarity class " + e.getMessage());
            } catch (IllegalAccessException e) {
                e.printStackTrace();
                throw new RecommenderException("Could not create Similarity class " + e.getMessage());
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
                throw new RecommenderException("Could not create Similarity class " + e.getMessage());
            } catch (InstantiationException e) {
                e.printStackTrace();
                throw new RecommenderException("Could not create Similarity class " + e.getMessage());
            } catch (InvocationTargetException e) {
                e.printStackTrace();
                throw new RecommenderException("Could not create Similarity class " + e.getMessage());
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
                throw new RecommenderException("Could not create Similarity class " + e.getMessage());
            } catch (SecurityException e) {
                e.printStackTrace();
                throw new RecommenderException("Could not create Similarity class " + e.getMessage());
            }
        }
        /**
         * Instantiate neighborhood class
         */
        Object neighObj = null;
        if (neighborhoodSize != NO_N) {
            Class<?> neighborhoodClass = null;
            try {
                neighborhoodClass = Class.forName(neighborhoodType);
                neighObj = neighborhoodClass.getConstructor(int.class, UserSimilarity.class, DataModel.class).newInstance(neighborhoodSize, simObj, dataModel);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
                throw new RecommenderException("Could not create Neighborhood class " + e.getMessage());
            } catch (IllegalAccessException e) {
                e.printStackTrace();
                throw new RecommenderException("Could not create Neighborhood class " + e.getMessage());
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
                throw new RecommenderException("Could not create Neighborhood class " + e.getMessage());
            } catch (InstantiationException e) {
                e.printStackTrace();
                throw new RecommenderException("Could not create Neighborhood class " + e.getMessage());
            } catch (InvocationTargetException e) {
                e.printStackTrace();
                throw new RecommenderException("Could not create Neighborhood class " + e.getMessage());
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
                throw new RecommenderException("Could not create Neighborhood class " + e.getMessage());
            } catch (SecurityException e) {
                e.printStackTrace();
                throw new RecommenderException("Could not create Neighborhood class " + e.getMessage());
            }
        }
        /**
         * Instantiate factorizer class
         */
        if (facType != null) {
            Class<?> factorizerClass = null;
            try {
                factorizerClass = Class.forName(facType);
                simObj = factorizerClass.getConstructor(DataModel.class, int.class, int.class).newInstance(dataModel, factors, iterations);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
                throw new RecommenderException("Could not create Factorizer " + e.getMessage());
            } catch (IllegalAccessException e) {
                e.printStackTrace();
                throw new RecommenderException("Could not create Factorizer " + e.getMessage());
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
                throw new RecommenderException("Could not create Factorizer " + e.getMessage());
            } catch (InstantiationException e) {
                e.printStackTrace();
                throw new RecommenderException("Could not create Factorizer " + e.getMessage());
            } catch (InvocationTargetException e) {
                e.printStackTrace();
                throw new RecommenderException("Could not create Factorizer " + e.getMessage());
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
                throw new RecommenderException("Could not create Factorizer " + e.getMessage());
            } catch (SecurityException e) {
                e.printStackTrace();
                throw new RecommenderException("Could not create Factorizer " + e.getMessage());
            }
        }
        /**
         * Instantiate recommender class
         */
        final Class<?> recommenderClass;
        try {
            recommenderClass = Class.forName(recType);
        } catch (ClassNotFoundException e) {
            throw new RecommenderException("Could not create Recommender class " + e.getMessage());
        }
        final Object recObj;
        try {
            if (facType != null) {
                recObj = recommenderClass.getConstructor(DataModel.class, Factorizer.class).newInstance(dataModel, (Factorizer) simObj);
            } else if (recType.contains("UserBased")) {
                // user-based similarity with neighborhood
                recObj = recommenderClass.getConstructor(DataModel.class, UserNeighborhood.class, UserSimilarity.class).newInstance(dataModel, neighObj, simObj);
            } else if (similarityType != null) {
                // item-based similarity, no neighborhood
                recObj = recommenderClass.getConstructor(DataModel.class, ItemSimilarity.class).newInstance(dataModel, simObj);
            } else {
                recObj = recommenderClass.getConstructor(DataModel.class).newInstance(dataModel);
            }
        } catch (IllegalAccessException e) {
            throw new RecommenderException("Could not create Recommender: " + e.getMessage());
        } catch (IllegalArgumentException e) {
            throw new RecommenderException("Could not create Recommender: " + e.getMessage());
        } catch (InstantiationException e) {
            throw new RecommenderException("Could not create Recommender: " + e.getMessage());
        } catch (InvocationTargetException e) {
            throw new RecommenderException("Could not create Recommender: " + e.getMessage());
        } catch (NoSuchMethodException e) {
            throw new RecommenderException("Could not create Recommender: " + e.getMessage());
        } catch (SecurityException e) {
            throw new RecommenderException("Could not create Recommender: " + e.getMessage());
        }
        return (Recommender) recObj;
    }
} @SuppressWarnings("serial")
public class RecommenderException extends Exception {

    /**
     * Constructs an exception with the specified message.
     *
     * @param msg the message to be shown later.
     */
    public RecommenderException(final String msg) {
        super(msg);
    }

    /**
     * Constructs a new exception with the specified message and cause.
     *
     * @param msg the message to be shown later.
     * @param t the cause of the exception.
     */
    public RecommenderException(final String msg, final Throwable t) {
        super(msg, t);
    }
} public class RandomSplitter<U, I> implements Splitter<U, I> {

    /**
     * The percentage of training to be used by the splitter.
     */
    private float percentageTraining;
    /**
     * The flag that indicates if the split should be done in a per user basis.
     */
    private boolean perUser;
    /**
     * The flag that indicates if the split should consider all the items
     * independently.
     */
    private boolean doSplitPerItems;
    /**
     * An instance of a Random class.
     */
    private Random rnd;

    /**
     * Constructor.
     *
     * Raises an exception if perUser is "true" and doSplitPerItems is "false".
     *
     * @param percentageTrainingRatio percentage of training data to be split
     * @param perUserFlag flag to do the split in a per user basis
     * @param seed value to initialize a Random class
     * @param doSplitPerItemsFlag if true, every interaction between a user and
     * a specific item is considered as one, and hence all of them will be
     * either on the training or on the test split
     *
     * @exception IllegalArgumentException if perUser == true and
     * doSplitPerItems == false
     */
    public RandomSplitter(final float percentageTrainingRatio, final boolean perUserFlag, final long seed, final boolean doSplitPerItemsFlag) {
        if (this.perUser && !this.doSplitPerItems) {
            throw new IllegalArgumentException("Unsupported parameters combination: perUser == true and doSplitPerItems == false");
        }
        this.percentageTraining = percentageTrainingRatio;
        this.perUser = perUserFlag;
        this.doSplitPerItems = doSplitPerItemsFlag;
        rnd = new Random(seed);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public DataModelIF<U, I>[] split(final DataModelIF<U, I> data) {
        @SuppressWarnings("unchecked")
        final DataModelIF<U, I>[] splits = new DataModelIF[2];
        splits[0] = DataModelFactory.getDefaultModel(); // training
        splits[1] = DataModelFactory.getDefaultModel(); // test
        if (perUser) {
            for (U user : data.getUsers()) {
                if (doSplitPerItems) {
                    List<I> items = new ArrayList<>();
                    data.getUserItems(user).forEach(items::add);
                    Collections.shuffle(items, rnd);
                    int splitPoint = Math.round(percentageTraining * items.size());
                    for (int i = 0; i < items.size(); i++) {
                        I item = items.get(i);
                        Double pref = data.getUserItemPreference(user, item);
                        DataModelIF<U, I> datamodel = splits[0]; // training
                        if (i > splitPoint) {
                            datamodel = splits[1]; // test
                        }
                        if (pref != null) {
                            datamodel.addPreference(user, item, pref);
                        }
                    }
                }
            }
        } else {
            for (U user : data.getUsers()) {
                for (I item : data.getUserItems(user)) {
                    Double pref = data.getUserItemPreference(user, item);
                    if (doSplitPerItems) {
                        DataModelIF<U, I> datamodel = splits[0]; // training
                        if (rnd.nextDouble() > percentageTraining) {
                            datamodel = splits[1]; // test
                        }
                        if (pref != null) {
                            datamodel.addPreference(user, item, pref);
                        }
                    } else {
                        DataModelIF<U, I> datamodel = splits[0]; // training
                        if (rnd.nextDouble() > percentageTraining) {
                            datamodel = splits[1]; // test
                        }
                        if (pref != null) {
                            datamodel.addPreference(user, item, pref);
                        }
                    }
                }
            }
        }
        return splits;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public TemporalDataModelIF<U, I>[] split(final TemporalDataModelIF<U, I> data) {
        @SuppressWarnings("unchecked")
        final TemporalDataModelIF<U, I>[] splits = new TemporalDataModelIF[2];
        splits[0] = new TemporalDataModel<>(); // training
        splits[1] = new TemporalDataModel<>(); // test
        if (perUser) {
            for (U user : data.getUsers()) {
                if (doSplitPerItems) {
                    List<I> items = new ArrayList<>();
                    data.getUserItems(user).forEach(items::add);
                    Collections.shuffle(items, rnd);
                    int splitPoint = Math.round(percentageTraining * items.size());
                    for (int i = 0; i < items.size(); i++) {
                        I item = items.get(i);
                        Double pref = data.getUserItemPreference(user, item);
                        Iterable<Long> time = data.getUserItemTimestamps(user, item);
                        TemporalDataModelIF<U, I> datamodel = splits[0]; // training
                        if (i > splitPoint) {
                            datamodel = splits[1]; // test
                        }
                        if (pref != null) {
                            datamodel.addPreference(user, item, pref);
                        }
                        if (time != null) {
                            for (Long t : time) {
                                datamodel.addTimestamp(user, item, t);
                            }
                        }
                    }
                } else {
                    List<Pair<I, Long>> itemsTime = new ArrayList<>();
                    for (I i : data.getUserItems(user)) {
                        for (Long t : data.getUserItemTimestamps(user, i)) {
                            itemsTime.add(new Pair<>(i, t));
                        }
                    }
                    Collections.shuffle(itemsTime, rnd);
                    int splitPoint = Math.round(percentageTraining * itemsTime.size());
                    for (int i = 0; i < itemsTime.size(); i++) {
                        Pair<I, Long> it = itemsTime.get(i);
                        I item = it.getFirst();
                        Long time = it.getSecond();
                        Double pref = data.getUserItemPreference(user, item);
                        TemporalDataModelIF<U, I> datamodel = splits[0]; // training
                        if (i > splitPoint) {
                            datamodel = splits[1]; // test
                        }
                        if (pref != null) {
                            datamodel.addPreference(user, item, pref);
                        }
                        if (time != null) {
                            datamodel.addTimestamp(user, item, time);
                        }
                    }
                }
            }
        } else {
            for (U user : data.getUsers()) {
                for (I item : data.getUserItems(user)) {
                    Double pref = data.getUserItemPreference(user, item);
                    Iterable<Long> time = data.getUserItemTimestamps(user, item);
                    if (doSplitPerItems) {
                        TemporalDataModelIF<U, I> datamodel = splits[0]; // training
                        if (rnd.nextDouble() > percentageTraining) {
                            datamodel = splits[1]; // test
                        }
                        if (pref != null) {
                            datamodel.addPreference(user, item, pref);
                        }
                        if (time != null) {
                            for (Long t : time) {
                                datamodel.addTimestamp(user, item, t);
                            }
                        }
                    } else if (time != null) {
                        for (Long t : time) {
                            TemporalDataModelIF<U, I> datamodel = splits[0]; // training
                            if (rnd.nextDouble() > percentageTraining) {
                                datamodel = splits[1]; // test
                            }
                            if (pref != null) {
                                datamodel.addPreference(user, item, pref);
                            }
                            datamodel.addTimestamp(user, item, t);
                        }
                    } else {
                        TemporalDataModelIF<U, I> datamodel = splits[0]; // training
                        if (rnd.nextDouble() > percentageTraining) {
                            datamodel = splits[1]; // test
                        }
                        if (pref != null) {
                            datamodel.addPreference(user, item, pref);
                        }
                    }
                }
            }
        }
        return splits;
    }

    private static class Pair<A, B> {

        private A a;
        private B b;

        public Pair(A a, B b) {
            this.a = a;
            this.b = b;
        }

        public A getFirst() {
            return a;
        }

        public B getSecond() {
            return b;
        }

    }
}
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import net.recommenders.rival.core.DataModelFactory;
import net.recommenders.rival.core.DataModelIF;
import net.recommenders.rival.core.DataModelUtils;
import net.recommenders.rival.core.SimpleParser;
import net.recommenders.rival.core.UIPParser;
import net.recommenders.rival.evaluation.metric.error.RMSE;
import net.recommenders.rival.evaluation.metric.ranking.NDCG;
import net.recommenders.rival.evaluation.metric.ranking.Precision;
import net.recommenders.rival.evaluation.strategy.EvaluationStrategy;
import net.recommenders.rival.examples.DataDownloader;
import net.recommenders.rival.recommend.frameworks.RecommenderIO;
import net.recommenders.rival.recommend.frameworks.mahout.GenericRecommenderBuilder;
import net.recommenders.rival.recommend.frameworks.exceptions.RecommenderException;
import net.recommenders.rival.split.splitter.RandomSplitter;
import org.apache.mahout.cf.taste.common.TasteException;
import org.apache.mahout.cf.taste.impl.common.LongPrimitiveIterator;
import org.apache.mahout.cf.taste.impl.model.file.FileDataModel;
import org.apache.mahout.cf.taste.recommender.RecommendedItem;
import org.apache.mahout.cf.taste.recommender.Recommender;
/*
 * Copyright 2015 recommenders.net.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.recommenders.rival.examples.movietweetings;


/**
 * Example where data from MovieTweetings is used to evaluate an IB recommender.
 *
 * @author <a href="http://github.com/alansaid">Alan</a>.
 */
public final class RandomMahoutIBRecommenderEvaluator {

    /**
     * Default percentage.
     */
    public static final float PERCENTAGE = 0.2f;
    /**
     * Default neighbohood size.
     */
    public static final int NEIGH_SIZE = 50;
    /**
     * Default cutoff for evaluation metrics.
     */
    public static final int AT = 10;
    /**
     * Default relevance threshold.
     */
    public static final double REL_TH = 3.0;
    /**
     * Default seed.
     */
    public static final long SEED = 2048L;
    /**
     * Token position for user information in MovieTweetings.
     */
    private static final int USER_TOK = 0;
    /**
     * Token position for item information in MovieTweetings.
     */
    private static final int ITEM_TOK = 2;
    /**
     * Token position for preference information in MovieTweetings.
     */
    private static final int PREF_TOK = 4;
    /**
     * Token position for time information in MovieTweetings.
     */
    private static final int TIME_TOK = 6;

    /**
     * Utility classes should not have a public or default constructor.
     */
    private RandomMahoutIBRecommenderEvaluator() {
    }

    /**
     * Main method. Parameter is not used.
     *
     * @param args the arguments (not used)
     */
    public static void main(final String[] args) {
        String url = "https://raw.githubusercontent.com/sidooms/MovieTweetings/master/snapshots/10K/ratings.dat";
        String modelPath = "data/movietweeting/model/";
        String recPath = "data/movietweeting/recommendations/";
        String folder = "data/movietweeting";
        float percentage = PERCENTAGE;
        prepareSplits(url, percentage, "data/movietweeting/ratings.dat", folder, modelPath);
        recommend(modelPath, recPath);
        prepareStrategy(modelPath, recPath, modelPath);
        evaluate(modelPath, recPath);
    }

    /**
     * Downloads a dataset and stores the splits generated from it.
     *
     * @param url url where dataset can be downloaded from
     * @param percentage percentage to be used in the random split
     * @param inFile file to be used once the dataset has been downloaded
     * @param folder folder where dataset will be stored
     * @param outPath path where the splits will be stored
     */
    public static void prepareSplits(final String url, final float percentage, final String inFile, final String folder, final String outPath) {
        DataDownloader dd = new DataDownloader(url, folder);
        dd.download();

        boolean perUser = true;
        long seed = SEED;
        UIPParser parser = new UIPParser();

        parser.setDelimiter(':');
        parser.setUserTok(USER_TOK);
        parser.setItemTok(ITEM_TOK);
        parser.setPrefTok(PREF_TOK);
        parser.setTimeTok(TIME_TOK);

        DataModelIF<Long, Long> data = null;
        try {
            data = parser.parseData(new File(inFile));
        } catch (IOException e) {
            e.printStackTrace();
        }

        DataModelIF<Long, Long>[] splits = new RandomSplitter<Long, Long>(percentage, perUser, seed, false).split(data);
        File dir = new File(outPath);
        if (!dir.exists()) {
            if (!dir.mkdir()) {
                System.err.println("Directory " + dir + " could not be created");
                return;
            }
        }
        for (int i = 0; i < splits.length / 2; i++) {
            DataModelIF<Long, Long> training = splits[2 * i];
            DataModelIF<Long, Long> test = splits[2 * i + 1];
            String trainingFile = outPath + "train_" + i + ".csv";
            String testFile = outPath + "test_" + i + ".csv";
            System.out.println("train: " + trainingFile);
            System.out.println("test: " + testFile);
            boolean overwrite = true;
            try {
                DataModelUtils.saveDataModel(training, trainingFile, overwrite, "\t");