You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mahout.apache.org by sr...@apache.org on 2008/05/09 23:35:17 UTC

svn commit: r654943 [3/9] - in /lucene/mahout/trunk/core: ./ lib/ src/main/examples/org/ src/main/examples/org/apache/ src/main/examples/org/apache/mahout/ src/main/examples/org/apache/mahout/cf/ src/main/examples/org/apache/mahout/cf/taste/ src/main/e...

Added: lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/correlation/GenericItemCorrelation.java
URL: http://svn.apache.org/viewvc/lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/correlation/GenericItemCorrelation.java?rev=654943&view=auto
==============================================================================
--- lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/correlation/GenericItemCorrelation.java (added)
+++ lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/correlation/GenericItemCorrelation.java Fri May  9 14:35:12 2008
@@ -0,0 +1,280 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.correlation;
+
+import org.apache.mahout.cf.taste.common.TasteException;
+import org.apache.mahout.cf.taste.correlation.ItemCorrelation;
+import org.apache.mahout.cf.taste.impl.common.IteratorIterable;
+import org.apache.mahout.cf.taste.impl.common.IteratorUtils;
+import org.apache.mahout.cf.taste.impl.recommender.TopItems;
+import org.apache.mahout.cf.taste.model.DataModel;
+import org.apache.mahout.cf.taste.model.Item;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+/**
+ * <p>A "generic" {@link ItemCorrelation} which takes a static list of precomputed {@link Item}
+ * correlations and bases its responses on that alone. The values may have been precomputed
+ * offline by another process, stored in a file, and then read and fed into an instance of this class.</p>
+ *
+ * <p>This is perhaps the best {@link ItemCorrelation} to use with
+ * {@link org.apache.mahout.cf.taste.impl.recommender.GenericItemBasedRecommender}, for now, since the point of item-based
+ * recommenders is that they can take advantage of the fact that item similarity is relatively static,
+ * can be precomputed, and then used in computation to gain a significant performance advantage.</p>
+ */
+public final class GenericItemCorrelation implements ItemCorrelation {
+
+  private final Map<Item, Map<Item, Double>> correlationMaps = new HashMap<Item, Map<Item, Double>>(1009);
+
+  /**
+   * <p>Creates a {@link GenericItemCorrelation} from a precomputed list of {@link ItemItemCorrelation}s. Each
+   * represents the correlation between two distinct items. Since correlation is assumed to be symmetric,
+   * it is not necessary to specify correlation between item1 and item2, and item2 and item1. Both are the same.
+   * It is also not necessary to specify a correlation between any item and itself; these are assumed to be 1.0.</p>
+   *
+   * <p>Note that specifying a correlation between two items twice is not an error, but, the later value will
+   * win.</p>
+   *
+   * @param correlations set of {@link ItemItemCorrelation}s on which to base this instance
+   */
+  public GenericItemCorrelation(Iterable<ItemItemCorrelation> correlations) {
+    initCorrelationMaps(correlations);
+  }
+
+  /**
+   * <p>Like {@link #GenericItemCorrelation(Iterable)}, but will only keep the specified number of correlations
+   * from the given {@link Iterable} of correlations. It will keep those with the highest correlation --
+   * those that are therefore most important.</p>
+   *
+   * <p>Thanks to tsmorton for suggesting this and providing part of the implementation.</p>
+   *
+   * @param correlations set of {@link ItemItemCorrelation}s on which to base this instance
+   * @param maxToKeep maximum number of correlations to keep
+   */
+  public GenericItemCorrelation(Iterable<ItemItemCorrelation> correlations, int maxToKeep) {
+    Iterable<ItemItemCorrelation> keptCorrelations = TopItems.getTopItemItemCorrelations(maxToKeep, correlations);
+    initCorrelationMaps(keptCorrelations);
+  }
+
+  /**
+   * <p>Builds a list of item-item correlations given an {@link ItemCorrelation} implementation and a
+   * {@link DataModel}, rather than a list of {@link ItemItemCorrelation}s.</p>
+   *
+   * <p>It's valid to build a {@link GenericItemCorrelation} this way, but perhaps missing some of the point
+   * of an item-based recommender. Item-based recommenders use the assumption that item-item correlations
+   * are relatively fixed, and might be known already independent of user preferences. Hence it is useful
+   * to inject that information, using {@link #GenericItemCorrelation(Iterable)}.</p>
+   *
+   * @param otherCorrelation other {@link ItemCorrelation} to get correlations from
+   * @param dataModel data model to get {@link Item}s from
+   * @throws TasteException if an error occurs while accessing the {@link DataModel} items
+   */
+  public GenericItemCorrelation(ItemCorrelation otherCorrelation, DataModel dataModel)
+          throws TasteException {
+    List<? extends Item> items = IteratorUtils.iterableToList(dataModel.getItems());
+    Iterator<ItemItemCorrelation> it = new DataModelCorrelationsIterator(otherCorrelation, items);
+    initCorrelationMaps(new IteratorIterable<ItemItemCorrelation>(it));
+  }
+
+  /**
+   * <p>Like {@link #GenericItemCorrelation(ItemCorrelation, DataModel)} )}, but will only
+   * keep the specified number of correlations from the given {@link DataModel}.
+   * It will keep those with the highest correlation -- those that are therefore most important.</p>
+   *
+   * <p>Thanks to tsmorton for suggesting this and providing part of the implementation.</p>
+   *
+   * @param otherCorrelation other {@link ItemCorrelation} to get correlations from
+   * @param dataModel data model to get {@link Item}s from
+   * @param maxToKeep maximum number of correlations to keep
+   * @throws TasteException if an error occurs while accessing the {@link DataModel} items
+   */
+  public GenericItemCorrelation(ItemCorrelation otherCorrelation, DataModel dataModel, int maxToKeep)
+          throws TasteException {
+    List<? extends Item> items = IteratorUtils.iterableToList(dataModel.getItems());
+    Iterator<ItemItemCorrelation> it = new DataModelCorrelationsIterator(otherCorrelation, items);
+    Iterable<ItemItemCorrelation> keptCorrelations =
+            TopItems.getTopItemItemCorrelations(maxToKeep, new IteratorIterable<ItemItemCorrelation>(it));
+    initCorrelationMaps(keptCorrelations);
+  }
+
+  private void initCorrelationMaps(Iterable<ItemItemCorrelation> correlations) {
+    for (ItemItemCorrelation iic : correlations) {
+      Item correlationItem1 = iic.getItem1();
+      Item correlationItem2 = iic.getItem2();
+      int compare = correlationItem1.compareTo(correlationItem2);
+      if (compare != 0) {
+        // Order them -- first key should be the "smaller" one
+        Item item1;
+        Item item2;
+        if (compare < 0) {
+          item1 = correlationItem1;
+          item2 = correlationItem2;
+        } else {
+          item1 = correlationItem2;
+          item2 = correlationItem1;
+        }
+        Map<Item, Double> map = correlationMaps.get(item1);
+        if (map == null) {
+          map = new HashMap<Item, Double>(1009);
+          correlationMaps.put(item1, map);
+        }
+        map.put(item2, iic.getValue());
+      }
+      // else correlation between item and itself already assumed to be 1.0
+    }
+  }
+
+  /**
+   * <p>Returns the correlation between two items. Note that correlation is assumed to be symmetric, that
+   * <code>itemCorrelation(item1, item2) == itemCorrelation(item2, item1)</code>, and that
+   * <code>itemCorrelation(item1, item1) == 1.0</code> for all items.</p>
+   *
+   * @param item1 first item
+   * @param item2 second item
+   * @return correlation between the two
+   */
+  public double itemCorrelation(Item item1, Item item2) {
+    int compare = item1.compareTo(item2);
+    if (compare == 0) {
+      return 1.0;
+    }
+    Item first;
+    Item second;
+    if (compare < 0) {
+      first = item1;
+      second = item2;
+    } else {
+      first = item2;
+      second = item1;
+    }
+    Map<Item, Double> nextMap = correlationMaps.get(first);
+    if (nextMap == null) {
+      return Double.NaN;
+    }
+    Double correlation = nextMap.get(second);
+    return correlation == null ? Double.NaN : correlation;
+  }
+
+  public void refresh() {
+    // Do nothing
+  }
+
+  /**
+   * Encapsulates a correlation between two items. Correlation must be in the range [-1.0,1.0].
+   */
+  public static final class ItemItemCorrelation {
+
+    // Somehow I think this class should be a top-level class now.
+    // But I have a love affair with inner classes.
+
+    private final Item item1;
+    private final Item item2;
+    private final double value;
+
+    /**
+     * @param item1 first item
+     * @param item2 second item
+     * @param value correlation between the two
+     * @throws IllegalArgumentException if value is NaN, less than -1.0 or greater than 1.0
+     */
+    public ItemItemCorrelation(Item item1, Item item2, double value) {
+      if (item1 == null || item2 == null) {
+        throw new IllegalArgumentException("An item is null");
+      }
+      if (Double.isNaN(value) || value < -1.0 || value > 1.0) {
+        throw new IllegalArgumentException("Illegal value: " + value);
+      }
+      this.item1 = item1;
+      this.item2 = item2;
+      this.value = value;
+    }
+
+    public Item getItem1() {
+      return item1;
+    }
+
+    public Item getItem2() {
+      return item2;
+    }
+
+    public double getValue() {
+      return value;
+    }
+
+    @Override
+    public String toString() {
+      return "ItemItemCorrelation[" + item1 + ',' + item2 + ':' + value + ']';
+    }
+
+  }
+
+  private static final class DataModelCorrelationsIterator implements Iterator<ItemItemCorrelation> {
+
+    private final ItemCorrelation otherCorrelation;
+    private final List<? extends Item> items;
+    private final int size;
+    private int i;
+    private Item item1;
+    private int j;
+
+    private DataModelCorrelationsIterator(ItemCorrelation otherCorrelation, List<? extends Item> items) {
+      this.otherCorrelation = otherCorrelation;
+      this.items = items;
+      this.size = items.size();
+      i = 0;
+      item1 = items.get(0);
+      j = 1;
+    }
+
+    public boolean hasNext() {
+      return i < size - 1;
+    }
+
+    public ItemItemCorrelation next() {
+      if (!hasNext()) {
+        throw new NoSuchElementException();
+      }
+      Item item2 = items.get(j);
+      double correlation;
+      try {
+        correlation = otherCorrelation.itemCorrelation(item1, item2);
+      } catch (TasteException te) {
+        // ugly:
+        throw new RuntimeException(te);
+      }
+      ItemItemCorrelation result = new ItemItemCorrelation(item1, item2, correlation);
+      j++;
+      if (j == size) {
+        i++;
+        item1 = items.get(i);
+        j = i + 1;
+      }
+      return result;
+    }
+
+    public void remove() {
+      throw new UnsupportedOperationException();
+    }
+
+  }
+
+}

Added: lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/correlation/PearsonCorrelation.java
URL: http://svn.apache.org/viewvc/lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/correlation/PearsonCorrelation.java?rev=654943&view=auto
==============================================================================
--- lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/correlation/PearsonCorrelation.java (added)
+++ lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/correlation/PearsonCorrelation.java Fri May  9 14:35:12 2008
@@ -0,0 +1,390 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.correlation;
+
+import org.apache.mahout.cf.taste.common.TasteException;
+import org.apache.mahout.cf.taste.correlation.ItemCorrelation;
+import org.apache.mahout.cf.taste.correlation.PreferenceInferrer;
+import org.apache.mahout.cf.taste.correlation.UserCorrelation;
+import org.apache.mahout.cf.taste.model.DataModel;
+import org.apache.mahout.cf.taste.model.Item;
+import org.apache.mahout.cf.taste.model.Preference;
+import org.apache.mahout.cf.taste.model.User;
+import org.apache.mahout.cf.taste.transforms.CorrelationTransform;
+import org.apache.mahout.cf.taste.transforms.PreferenceTransform2;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * <p>An implementation of the Pearson correlation. For {@link User}s X and Y, the following values
+ * are calculated:</p>
+ *
+ * <ul>
+ * <li>sumX2: sum of the square of all X's preference values</li>
+ * <li>sumY2: sum of the square of all Y's preference values</li>
+ * <li>sumXY: sum of the product of X and Y's preference value for all items for which both
+ * X and Y express a preference</li>
+ * </ul>
+ *
+ * <p>The correlation is then:
+ *
+ * <p><code>sumXY / sqrt(sumX2 * sumY2)</code></p>
+ *
+ * <p>where <code>size</code> is the number of {@link Item}s in the {@link DataModel}.</p>
+ *
+ * <p>Note that this correlation "centers" its data, shifts the user's preference values so that
+ * each of their means is 0. This is necessary to achieve expected behavior on all data sets.</p>
+ *
+ * <p>This correlation implementation is equivalent to the cosine measure correlation since the data it
+ * receives is assumed to be centered -- mean is 0. The correlation may be interpreted as the cosine of the
+ * angle between the two vectors defined by the users' preference values.</p>
+ */
+public final class PearsonCorrelation implements UserCorrelation, ItemCorrelation {
+
+  private static final Logger log = Logger.getLogger(PearsonCorrelation.class.getName());
+
+  private final DataModel dataModel;
+  private PreferenceInferrer inferrer;
+  private PreferenceTransform2 prefTransform;
+  private CorrelationTransform<Object> correlationTransform;
+  private boolean weighted;
+
+  /**
+   * <p>Creates a normal (unweighted) {@link PearsonCorrelation}.</p>
+   *
+   * @param dataModel
+   */
+  public PearsonCorrelation(DataModel dataModel) {
+    this(dataModel, false);
+  }
+
+  /**
+   * <p>Creates a weighted {@link PearsonCorrelation}.</p>
+   *
+   * @param dataModel
+   * @param weighted
+   */
+  public PearsonCorrelation(DataModel dataModel, boolean weighted) {
+    if (dataModel == null) {
+      throw new IllegalArgumentException("dataModel is null");
+    }
+    this.dataModel = dataModel;
+    this.weighted = weighted;
+  }
+
+  /**
+   * <p>Several subclasses in this package implement this method to actually compute the correlation
+   * from figures computed over users or items. Note that the computations in this class "center" the
+   * data, such that X and Y's mean are 0.</p>
+   *
+   * <p>Note that the sum of all X and Y values must then be 0. This value isn't passed down into
+   * the standard correlation computations as a result.</p>
+   *
+   * @param n total number of users or items
+   * @param sumXY sum of product of user/item preference values, over all items/users prefererred by
+   * both users/items
+   * @param sumX2 sum of the square of user/item preference values, over the first item/user
+   * @param sumY2 sum of the square of the user/item preference values, over the second item/user
+   * @return correlation value between -1.0 and 1.0, inclusive, or {@link Double#NaN} if no correlation
+   *         can be computed (e.g. when no {@link Item}s have been rated by both {@link User}s
+   */
+  private static double computeResult(int n, double sumXY, double sumX2, double sumY2) {
+    if (n == 0) {
+      return Double.NaN;
+    }
+    // Note that sum of X and sum of Y don't appear here since they are assumed to be 0;
+    // the data is assumed to be centered.
+    double xTerm = Math.sqrt(sumX2);
+    double yTerm = Math.sqrt(sumY2);
+    double denominator = xTerm * yTerm;
+    if (denominator == 0.0) {
+      // One or both parties has -all- the same ratings;
+      // can't really say much correlation under this measure
+      return Double.NaN;
+    }
+    return sumXY / denominator;
+  }
+
+  DataModel getDataModel() {
+    return dataModel;
+  }
+
+  PreferenceInferrer getPreferenceInferrer() {
+    return inferrer;
+  }
+
+  public void setPreferenceInferrer(PreferenceInferrer inferrer) {
+    if (inferrer == null) {
+      throw new IllegalArgumentException("inferrer is null");
+    }
+    this.inferrer = inferrer;
+  }
+
+  public PreferenceTransform2 getPrefTransform() {
+    return prefTransform;
+  }
+
+  public void setPrefTransform(PreferenceTransform2 prefTransform) {
+    this.prefTransform = prefTransform;
+  }
+
+  public CorrelationTransform<?> getCorrelationTransform() {
+    return correlationTransform;
+  }
+
+  public void setCorrelationTransform(CorrelationTransform<Object> correlationTransform) {
+    this.correlationTransform = correlationTransform;
+  }
+
+  boolean isWeighted() {
+    return weighted;
+  }
+
+  public double userCorrelation(User user1, User user2) throws TasteException {
+
+    if (user1 == null || user2 == null) {
+      throw new IllegalArgumentException("user1 or user2 is null");
+    }
+
+    Preference[] xPrefs = user1.getPreferencesAsArray();
+    Preference[] yPrefs = user2.getPreferencesAsArray();
+
+    if (xPrefs.length == 0 || yPrefs.length == 0) {
+      return Double.NaN;
+    }
+
+    Preference xPref = xPrefs[0];
+    Preference yPref = yPrefs[0];
+    Item xIndex = xPref.getItem();
+    Item yIndex = yPref.getItem();
+    int xPrefIndex = 1;
+    int yPrefIndex = 1;
+
+    double sumX = 0.0;
+    double sumX2 = 0.0;
+    double sumY = 0.0;
+    double sumY2 = 0.0;
+    double sumXY = 0.0;
+    int count = 0;
+
+    boolean hasInferrer = inferrer != null;
+    boolean hasPrefTransform = prefTransform != null;
+
+    while (true) {
+      int compare = xIndex.compareTo(yIndex);
+      if (hasInferrer || compare == 0) {
+        double x;
+        double y;
+        if (compare == 0) {
+          // Both users expressed a preference for the item
+          if (hasPrefTransform) {
+            x = prefTransform.getTransformedValue(xPref);
+            y = prefTransform.getTransformedValue(yPref);
+          } else {
+            x = xPref.getValue();
+            y = yPref.getValue();
+          }
+        } else {
+          // Only one user expressed a preference, but infer the other one's preference and tally
+          // as if the other user expressed that preference
+          if (compare < 0) {
+            // X has a value; infer Y's
+            if (hasPrefTransform) {
+              x = prefTransform.getTransformedValue(xPref);
+            } else {
+              x = xPref.getValue();
+            }
+            y = inferrer.inferPreference(user2, xIndex);
+          } else {
+            // compare > 0
+            // Y has a value; infer X's
+            x = inferrer.inferPreference(user1, yIndex);
+            if (hasPrefTransform) {
+              y = prefTransform.getTransformedValue(yPref);
+            } else {
+              y = yPref.getValue();
+            }
+          }
+        }
+        sumXY += x * y;
+        sumX += x;
+        sumX2 += x * x;
+        sumY += y;
+        sumY2 += y * y;
+        count++;
+      }
+      if (compare <= 0) {
+        if (xPrefIndex == xPrefs.length) {
+          break;
+        }
+        xPref = xPrefs[xPrefIndex++];
+        xIndex = xPref.getItem();
+      }
+      if (compare >= 0) {
+        if (yPrefIndex == yPrefs.length) {
+          break;
+        }
+        yPref = yPrefs[yPrefIndex++];
+        yIndex = yPref.getItem();
+      }
+    }
+
+    // "Center" the data. If my math is correct, this'll do it.
+    double n = (double) count;
+    double meanX = sumX / n;
+    double meanY = sumY / n;
+    double centeredSumXY = sumXY - meanY * sumX - meanX * sumY + n * meanX * meanY;
+    double centeredSumX2 = sumX2 - 2.0 * meanX * sumX + n * meanX * meanX;
+    double centeredSumY2 = sumY2 - 2.0 * meanY * sumY + n * meanY * meanY;
+
+    double result = computeResult(count, centeredSumXY, centeredSumX2, centeredSumY2);
+
+    if (correlationTransform != null) {
+      result = correlationTransform.transformCorrelation(user1, user2, result);
+    }
+
+    if (!Double.isNaN(result)) {
+      result = normalizeWeightResult(result, count, dataModel.getNumItems());
+    }
+
+    if (log.isLoggable(Level.FINER)) {
+      log.finer("UserCorrelation between " + user1 + " and " + user2 + " is " + result);
+    }
+    return result;
+  }
+
+  public double itemCorrelation(Item item1, Item item2) throws TasteException {
+
+    if (item1 == null || item2 == null) {
+      throw new IllegalArgumentException("item1 or item2 is null");
+    }
+
+    Preference[] xPrefs = dataModel.getPreferencesForItemAsArray(item1.getID());
+    Preference[] yPrefs = dataModel.getPreferencesForItemAsArray(item2.getID());
+
+    if (xPrefs.length == 0 || yPrefs.length == 0) {
+      return Double.NaN;
+    }
+
+    Preference xPref = xPrefs[0];
+    Preference yPref = yPrefs[0];
+    User xIndex = xPref.getUser();
+    User yIndex = yPref.getUser();
+    int xPrefIndex = 1;
+    int yPrefIndex = 1;
+
+    double sumX = 0.0;
+    double sumX2 = 0.0;
+    double sumY = 0.0;
+    double sumY2 = 0.0;
+    double sumXY = 0.0;
+    int count = 0;
+
+    // No, pref inferrers and transforms don't appy here. I think.
+
+    while (true) {
+      int compare = xIndex.compareTo(yIndex);
+      if (compare == 0) {
+        // Both users expressed a preference for the item
+        double x = xPref.getValue();
+        double y = yPref.getValue();
+        sumXY += x * y;
+        sumX += x;
+        sumX2 += x * x;
+        sumY += y;
+        sumY2 += y * y;
+        count++;
+      }
+      if (compare <= 0) {
+        if (xPrefIndex == xPrefs.length) {
+          break;
+        }
+        xPref = xPrefs[xPrefIndex++];
+        xIndex = xPref.getUser();
+      }
+      if (compare >= 0) {
+        if (yPrefIndex == yPrefs.length) {
+          break;
+        }
+        yPref = yPrefs[yPrefIndex++];
+        yIndex = yPref.getUser();
+      }
+    }
+
+    // See comments above on these computations
+    double n = (double) count;
+    double meanX = sumX / n;
+    double meanY = sumY / n;
+    double centeredSumXY = sumXY - meanY * sumX - meanX * sumY + n * meanX * meanY;
+    double centeredSumX2 = sumX2 - 2.0 * meanX * sumX + n * meanX * meanX;
+    double centeredSumY2 = sumY2 - 2.0 * meanY * sumY + n * meanY * meanY;
+
+    double result = computeResult(count, centeredSumXY, centeredSumX2, centeredSumY2);
+
+    if (correlationTransform != null) {
+      result = correlationTransform.transformCorrelation(item1, item2, result);
+    }
+
+    if (!Double.isNaN(result)) {
+      result = normalizeWeightResult(result, count, dataModel.getNumUsers());
+    }
+
+    if (log.isLoggable(Level.FINER)) {
+      log.finer("UserCorrelation between " + item1 + " and " + item2 + " is " + result);
+    }
+    return result;
+  }
+
+  private double normalizeWeightResult(double result, int count, int num) {
+    if (weighted) {
+      double scaleFactor = 1.0 - (double) count / (double) (num + 1);
+      if (result < 0.0) {
+        result = -1.0 + scaleFactor * (1.0 + result);
+      } else {
+        result = 1.0 - scaleFactor * (1.0 - result);
+      }
+    }
+    // Make sure the result is not accidentally a little outside [-1.0, 1.0] due to rounding:
+    if (result < -1.0) {
+      result = -1.0;
+    } else if (result > 1.0) {
+      result = 1.0;
+    }
+    return result;
+  }
+
+  public void refresh() {
+    dataModel.refresh();
+    if (inferrer != null) {
+      inferrer.refresh();
+    }
+    if (prefTransform != null) {
+      prefTransform.refresh();
+    }
+    if (correlationTransform != null) {
+      correlationTransform.refresh();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "PearsonCorrelation[dataModel:" + dataModel + ",inferrer:" + inferrer + ']';
+  }
+
+}

Added: lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/correlation/SpearmanCorrelation.java
URL: http://svn.apache.org/viewvc/lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/correlation/SpearmanCorrelation.java?rev=654943&view=auto
==============================================================================
--- lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/correlation/SpearmanCorrelation.java (added)
+++ lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/correlation/SpearmanCorrelation.java Fri May  9 14:35:12 2008
@@ -0,0 +1,143 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.correlation;
+
+import org.apache.mahout.cf.taste.common.TasteException;
+import org.apache.mahout.cf.taste.correlation.PreferenceInferrer;
+import org.apache.mahout.cf.taste.correlation.UserCorrelation;
+import org.apache.mahout.cf.taste.impl.model.ByItemPreferenceComparator;
+import org.apache.mahout.cf.taste.impl.model.ByValuePreferenceComparator;
+import org.apache.mahout.cf.taste.impl.model.GenericPreference;
+import org.apache.mahout.cf.taste.model.DataModel;
+import org.apache.mahout.cf.taste.model.Preference;
+import org.apache.mahout.cf.taste.model.User;
+
+import java.util.Arrays;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * <p>Like {@link PearsonCorrelation}, but compares relative ranking of preference values instead of preference
+ * values themselves. That is, each {@link User}'s preferences are sorted and then assign a rank as their preference
+ * value, with 1 being assigned to the least preferred item. Then the Pearson itemCorrelation of these rank values is
+ * computed.</p>
+ */
+public final class SpearmanCorrelation implements UserCorrelation {
+
+  private final UserCorrelation rankingUserCorrelation;
+  private final ReentrantLock refreshLock;
+
+  public SpearmanCorrelation(DataModel dataModel) {
+    if (dataModel == null) {
+      throw new IllegalArgumentException("dataModel is null");
+    }
+    this.rankingUserCorrelation = new PearsonCorrelation(dataModel);
+    this.refreshLock = new ReentrantLock();
+  }
+
+  public SpearmanCorrelation(UserCorrelation rankingUserCorrelation) {
+    if (rankingUserCorrelation == null) {
+      throw new IllegalArgumentException("rankingUserCorrelation is null");
+    }
+    this.rankingUserCorrelation = rankingUserCorrelation;
+    this.refreshLock = new ReentrantLock();
+  }
+
+  public double userCorrelation(User user1, User user2) throws TasteException {
+    if (user1 == null || user2 == null) {
+      throw new IllegalArgumentException("user1 or user2 is null");
+    }
+    return rankingUserCorrelation.userCorrelation(new RankedPreferenceUser(user1),
+                                                  new RankedPreferenceUser(user2));
+  }
+
+  public void setPreferenceInferrer(PreferenceInferrer inferrer) {
+    rankingUserCorrelation.setPreferenceInferrer(inferrer);
+  }
+
+  public void refresh() {
+    if (refreshLock.isLocked()) {
+      return;
+    }
+    try {
+      refreshLock.lock();
+      rankingUserCorrelation.refresh();
+    } finally {
+      refreshLock.unlock();
+    }
+  }
+
+
+  /**
+   * <p>A simple {@link User} decorator which will always return the underlying {@link User}'s
+   * preferences in order by value.</p>
+   */
+  private static final class RankedPreferenceUser implements User {
+
+    private final User delegate;
+
+    private RankedPreferenceUser(User delegate) {
+      this.delegate = delegate;
+    }
+
+    public Object getID() {
+      return delegate.getID();
+    }
+
+    public Preference getPreferenceFor(Object itemID) {
+      throw new UnsupportedOperationException();
+    }
+
+    public Iterable<Preference> getPreferences() {
+      return Arrays.asList(getPreferencesAsArray());
+    }
+
+    public Preference[] getPreferencesAsArray() {
+      Preference[] source = delegate.getPreferencesAsArray();
+      int length = source.length;
+      Preference[] sortedPrefs = new Preference[length];
+      System.arraycopy(source, 0, sortedPrefs, 0, length);
+      Arrays.sort(sortedPrefs, ByValuePreferenceComparator.getInstance());
+      for (int i = 0; i < length; i++) {
+        sortedPrefs[i] = new GenericPreference(this, sortedPrefs[i].getItem(), (double) (i + 1));
+      }
+      Arrays.sort(sortedPrefs, ByItemPreferenceComparator.getInstance());
+      return sortedPrefs;
+    }
+
+    @Override
+    public int hashCode() {
+      return delegate.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      return o instanceof RankedPreferenceUser && delegate.equals(((RankedPreferenceUser) o).delegate);
+    }
+
+    public int compareTo(User user) {
+      return delegate.compareTo(user);
+    }
+
+    @Override
+    public String toString() {
+      return "RankedPreferenceUser[user:" + delegate + ']';
+    }
+
+  }
+
+}

Added: lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/eval/AbstractDifferenceRecommenderEvaluator.java
URL: http://svn.apache.org/viewvc/lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/eval/AbstractDifferenceRecommenderEvaluator.java?rev=654943&view=auto
==============================================================================
--- lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/eval/AbstractDifferenceRecommenderEvaluator.java (added)
+++ lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/eval/AbstractDifferenceRecommenderEvaluator.java Fri May  9 14:35:12 2008
@@ -0,0 +1,122 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.eval;
+
+import org.apache.mahout.cf.taste.common.TasteException;
+import org.apache.mahout.cf.taste.eval.RecommenderBuilder;
+import org.apache.mahout.cf.taste.eval.RecommenderEvaluator;
+import org.apache.mahout.cf.taste.impl.common.RandomUtils;
+import org.apache.mahout.cf.taste.impl.model.GenericDataModel;
+import org.apache.mahout.cf.taste.impl.model.GenericItem;
+import org.apache.mahout.cf.taste.impl.model.GenericPreference;
+import org.apache.mahout.cf.taste.impl.model.GenericUser;
+import org.apache.mahout.cf.taste.model.DataModel;
+import org.apache.mahout.cf.taste.model.Item;
+import org.apache.mahout.cf.taste.model.Preference;
+import org.apache.mahout.cf.taste.model.User;
+import org.apache.mahout.cf.taste.recommender.Recommender;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * <p>Abstract superclass of a couple implementations, providing shared functionality.</p>
+ */
+abstract class AbstractDifferenceRecommenderEvaluator implements RecommenderEvaluator {
+
+  private static final Logger log = Logger.getLogger(AbstractDifferenceRecommenderEvaluator.class.getName());
+
+  private final Random random;
+
+  AbstractDifferenceRecommenderEvaluator() {
+    random = RandomUtils.getRandom();
+  }
+
+  public double evaluate(RecommenderBuilder recommenderBuilder,
+                         DataModel dataModel,
+                         double trainingPercentage,
+                         double evaluationPercentage) throws TasteException {
+
+    if (recommenderBuilder == null) {
+      throw new IllegalArgumentException("recommenderBuilder is null");
+    }
+    if (dataModel == null) {
+      throw new IllegalArgumentException("dataModel is null");
+    }
+    if (Double.isNaN(trainingPercentage) || trainingPercentage <= 0.0 || trainingPercentage >= 1.0) {
+      throw new IllegalArgumentException("Invalid trainingPercentage: " + trainingPercentage);
+    }
+    if (Double.isNaN(evaluationPercentage) || evaluationPercentage <= 0.0 || evaluationPercentage > 1.0) {
+      throw new IllegalArgumentException("Invalid evaluationPercentage: " + evaluationPercentage);
+    }
+
+    log.info("Beginning evaluation using " + trainingPercentage + " of " + dataModel);
+
+    int numUsers = dataModel.getNumUsers();
+    Collection<User> trainingUsers = new ArrayList<User>(1 + (int) (trainingPercentage * (double) numUsers));
+    Map<User, Collection<Preference>> testUserPrefs =
+            new HashMap<User, Collection<Preference>>(1 + (int) ((1.0 - trainingPercentage) * (double) numUsers));
+
+    for (User user : dataModel.getUsers()) {
+      if (random.nextDouble() < evaluationPercentage) {
+        Collection<Preference> trainingPrefs = new ArrayList<Preference>();
+        Collection<Preference> testPrefs = new ArrayList<Preference>();
+        Preference[] prefs = user.getPreferencesAsArray();
+        for (int i = 0; i < prefs.length; i++) {
+          Preference pref = prefs[i];
+          Item itemCopy = new GenericItem<String>(pref.getItem().getID().toString());
+          Preference newPref = new GenericPreference(null, itemCopy, pref.getValue());
+          if (random.nextDouble() < trainingPercentage) {
+            trainingPrefs.add(newPref);
+          } else {
+            testPrefs.add(newPref);
+          }
+        }
+        if (log.isLoggable(Level.FINE)) {
+          log.fine("Training against " + trainingPrefs.size() + " preferences");
+          log.fine("Evaluating accuracy of " + testPrefs.size() + " preferences");
+        }
+        if (!trainingPrefs.isEmpty()) {
+          User trainingUser = new GenericUser<String>(user.getID().toString(), trainingPrefs);
+          trainingUsers.add(trainingUser);
+          if (!testPrefs.isEmpty()) {
+            testUserPrefs.put(trainingUser, testPrefs);
+          }
+        }
+      }
+    }
+
+    DataModel trainingModel = new GenericDataModel(trainingUsers);
+
+    Recommender recommender = recommenderBuilder.buildRecommender(trainingModel);
+
+    double result = getEvaluation(testUserPrefs, recommender);
+    log.info("Evaluation result: " + result);
+    return result;
+  }
+
+  abstract double getEvaluation(Map<User, Collection<Preference>> testUserPrefs,
+                                Recommender recommender)
+          throws TasteException;
+
+}

Added: lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/eval/AverageAbsoluteDifferenceRecommenderEvaluator.java
URL: http://svn.apache.org/viewvc/lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/eval/AverageAbsoluteDifferenceRecommenderEvaluator.java?rev=654943&view=auto
==============================================================================
--- lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/eval/AverageAbsoluteDifferenceRecommenderEvaluator.java (added)
+++ lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/eval/AverageAbsoluteDifferenceRecommenderEvaluator.java Fri May  9 14:35:12 2008
@@ -0,0 +1,72 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.eval;
+
+import org.apache.mahout.cf.taste.common.TasteException;
+import org.apache.mahout.cf.taste.impl.common.FullRunningAverage;
+import org.apache.mahout.cf.taste.impl.common.RunningAverage;
+import org.apache.mahout.cf.taste.model.Preference;
+import org.apache.mahout.cf.taste.model.User;
+import org.apache.mahout.cf.taste.recommender.Recommender;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * <p>A {@link org.apache.mahout.cf.taste.eval.RecommenderEvaluator} which computes the average absolute difference
+ * between predicted and actual ratings for users.</p>
+ *
+ * <p>This algorithm is also called "mean average error".</p>
+ */
+public final class AverageAbsoluteDifferenceRecommenderEvaluator extends AbstractDifferenceRecommenderEvaluator {
+
+  private static final Logger log = Logger.getLogger(AverageAbsoluteDifferenceRecommenderEvaluator.class.getName());
+
+  @Override
+  double getEvaluation(Map<User, Collection<Preference>> testUserPrefs,
+                       Recommender recommender)
+          throws TasteException {
+    RunningAverage average = new FullRunningAverage();
+    for (Map.Entry<User, Collection<Preference>> entry : testUserPrefs.entrySet()) {
+      for (Preference realPref : entry.getValue()) {
+        User testUser = entry.getKey();
+        try {
+          double estimatedPreference =
+                  recommender.estimatePreference(testUser.getID(), realPref.getItem().getID());
+          if (!Double.isNaN(estimatedPreference)) {
+            average.addDatum(Math.abs(realPref.getValue() - estimatedPreference));
+          }
+        } catch (NoSuchElementException nsee) {
+          // It's possible that an item exists in the test data but not training data in which case
+          // NSEE will be thrown. Just ignore it and move on.
+          log.log(Level.INFO, "Element exists in test data but not training data: " + testUser.getID(), nsee);
+        }
+      }
+    }
+    return average.getAverage();
+  }
+
+  @Override
+  public String toString() {
+    return "AverageAbsoluteDifferenceRecommenderEvaluator";
+  }
+
+}

Added: lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/eval/GenericRecommenderIRStatsEvaluator.java
URL: http://svn.apache.org/viewvc/lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/eval/GenericRecommenderIRStatsEvaluator.java?rev=654943&view=auto
==============================================================================
--- lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/eval/GenericRecommenderIRStatsEvaluator.java (added)
+++ lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/eval/GenericRecommenderIRStatsEvaluator.java Fri May  9 14:35:12 2008
@@ -0,0 +1,138 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.eval;
+
+import org.apache.mahout.cf.taste.common.TasteException;
+import org.apache.mahout.cf.taste.eval.IRStatistics;
+import org.apache.mahout.cf.taste.eval.RecommenderBuilder;
+import org.apache.mahout.cf.taste.eval.RecommenderIRStatsEvaluator;
+import org.apache.mahout.cf.taste.impl.common.FullRunningAverage;
+import org.apache.mahout.cf.taste.impl.common.RandomUtils;
+import org.apache.mahout.cf.taste.impl.common.RunningAverage;
+import org.apache.mahout.cf.taste.impl.model.GenericDataModel;
+import org.apache.mahout.cf.taste.impl.model.GenericUser;
+import org.apache.mahout.cf.taste.model.DataModel;
+import org.apache.mahout.cf.taste.model.Item;
+import org.apache.mahout.cf.taste.model.Preference;
+import org.apache.mahout.cf.taste.model.User;
+import org.apache.mahout.cf.taste.recommender.RecommendedItem;
+import org.apache.mahout.cf.taste.recommender.Recommender;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.NoSuchElementException;
+import java.util.Random;
+
+/**
+ * <p>For each {@link org.apache.mahout.cf.taste.model.User}, these implementation determine the top <code>n</code> preferences,
+ * then evaluate the IR statistics based on a {@link DataModel} that does not have these values.
+ * This number <code>n</code> is the "at" value, as in "precision at 5". For example, this would mean precision
+ * evaluated by removing the top 5 preferences for a {@link User} and then finding the percentage of those 5
+ * {@link org.apache.mahout.cf.taste.model.Item}s included in the top 5 recommendations for that user.</p>
+ */
+public final class GenericRecommenderIRStatsEvaluator implements RecommenderIRStatsEvaluator {
+
+  private final Random random;
+
+  public GenericRecommenderIRStatsEvaluator() {
+    random = RandomUtils.getRandom();
+  }
+
+  public IRStatistics evaluate(RecommenderBuilder recommenderBuilder,
+                               DataModel dataModel,
+                               int at,
+                               double relevanceThreshold,
+                               double evaluationPercentage) throws TasteException {
+
+    if (recommenderBuilder == null) {
+      throw new IllegalArgumentException("recommenderBuilder is null");
+    }
+    if (dataModel == null) {
+      throw new IllegalArgumentException("dataModel is null");
+    }
+    if (at < 1) {
+      throw new IllegalArgumentException("at must be at least 1");
+    }
+    if (Double.isNaN(evaluationPercentage) || evaluationPercentage <= 0.0 || evaluationPercentage > 1.0) {
+      throw new IllegalArgumentException("Invalid evaluationPercentage: " + evaluationPercentage);
+    }
+    if (Double.isNaN(relevanceThreshold)) {
+      throw new IllegalArgumentException("Invalid relevanceThreshold: " + evaluationPercentage);
+    }
+
+    RunningAverage precision = new FullRunningAverage();
+    RunningAverage recall = new FullRunningAverage();
+    for (User user : dataModel.getUsers()) {
+      Object id = user.getID();
+      if (random.nextDouble() < evaluationPercentage) {
+        Collection<Item> relevantItems = new HashSet<Item>(at);
+        Preference[] prefs = user.getPreferencesAsArray();
+        for (int i = 0; i < prefs.length; i++) {
+          Preference pref = prefs[i];
+          if (pref.getValue() >= relevanceThreshold) {
+            relevantItems.add(pref.getItem());
+          }
+        }
+        int numRelevantItems = relevantItems.size();
+        if (numRelevantItems > 0) {
+          Collection<User> trainingUsers = new ArrayList<User>(dataModel.getNumUsers());
+          for (User user2 : dataModel.getUsers()) {
+            if (id.equals(user2.getID())) {
+              Collection<Preference> trainingPrefs = new ArrayList<Preference>();
+              Preference[] prefs2 = user2.getPreferencesAsArray();
+              for (int i = 0; i < prefs2.length; i++) {
+                Preference pref = prefs2[i];
+                if (!relevantItems.contains(pref.getItem())) {
+                  trainingPrefs.add(pref);
+                }
+              }
+              if (!trainingPrefs.isEmpty()) {
+                User trainingUser = new GenericUser<String>(id.toString(), trainingPrefs);
+                trainingUsers.add(trainingUser);
+              }
+            } else {
+              trainingUsers.add(user2);
+            }
+
+          }
+          DataModel trainingModel = new GenericDataModel(trainingUsers);
+          Recommender recommender = recommenderBuilder.buildRecommender(trainingModel);
+
+          try {
+            trainingModel.getUser(id);
+          } catch (NoSuchElementException nsee) {
+            continue; // Oops we excluded all prefs for the user -- just move on
+          }
+
+          int intersectionSize = 0;
+          for (RecommendedItem recommendedItem : recommender.recommend(id, at)) {
+            if (relevantItems.contains(recommendedItem.getItem())) {
+              intersectionSize++;
+            }
+          }
+          precision.addDatum((double) intersectionSize / (double) at);
+          recall.addDatum((double) intersectionSize / (double) numRelevantItems);
+        }
+      }
+    }
+
+    return new IRStatisticsImpl(precision.getAverage(), recall.getAverage());
+  }
+
+}

Added: lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/eval/IRStatisticsImpl.java
URL: http://svn.apache.org/viewvc/lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/eval/IRStatisticsImpl.java?rev=654943&view=auto
==============================================================================
--- lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/eval/IRStatisticsImpl.java (added)
+++ lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/eval/IRStatisticsImpl.java Fri May  9 14:35:12 2008
@@ -0,0 +1,57 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.eval;
+
+import org.apache.mahout.cf.taste.eval.IRStatistics;
+
+import java.io.Serializable;
+
+public final class IRStatisticsImpl implements IRStatistics, Serializable {
+
+  private final double precision;
+  private final double recall;
+
+  IRStatisticsImpl(double precision, double recall) {
+    if (precision < 0.0 || precision > 1.0) {
+      throw new IllegalArgumentException("Illegal precision: " + precision);
+    }
+    if (recall < 0.0 || recall > 1.0) {
+      throw new IllegalArgumentException("Illegal recall: " + recall);
+    }
+    this.precision = precision;
+    this.recall = recall;
+  }
+
+  public double getPrecision() {
+    return precision;
+  }
+
+  public double getRecall() {
+    return recall;
+  }
+
+  public double getF1Measure() {
+    return getFNMeasure(1.0);
+  }
+
+  public double getFNMeasure(double n) {
+    double sum = n * precision + recall;
+    return sum == 0.0 ? Double.NaN : (1.0 + n) * precision * recall / sum;
+  }
+
+}
\ No newline at end of file

Added: lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/eval/RMSRecommenderEvaluator.java
URL: http://svn.apache.org/viewvc/lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/eval/RMSRecommenderEvaluator.java?rev=654943&view=auto
==============================================================================
--- lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/eval/RMSRecommenderEvaluator.java (added)
+++ lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/eval/RMSRecommenderEvaluator.java Fri May  9 14:35:12 2008
@@ -0,0 +1,72 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.eval;
+
+import org.apache.mahout.cf.taste.common.TasteException;
+import org.apache.mahout.cf.taste.impl.common.FullRunningAverage;
+import org.apache.mahout.cf.taste.impl.common.RunningAverage;
+import org.apache.mahout.cf.taste.model.Preference;
+import org.apache.mahout.cf.taste.model.User;
+import org.apache.mahout.cf.taste.recommender.Recommender;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * <p>A {@link org.apache.mahout.cf.taste.eval.RecommenderEvaluator} which computes the "root mean squared" difference
+ * between predicted and actual ratings for users. This is the square root of the average of this difference,
+ * squared.</p>
+ */
+public final class RMSRecommenderEvaluator extends AbstractDifferenceRecommenderEvaluator {
+
+  private static final Logger log = Logger.getLogger(RMSRecommenderEvaluator.class.getName());
+
+  @Override
+  double getEvaluation(Map<User, Collection<Preference>> testUserPrefs,
+                       Recommender recommender)
+          throws TasteException {
+    RunningAverage average = new FullRunningAverage();
+    for (Map.Entry<User, Collection<Preference>> entry : testUserPrefs.entrySet()) {
+      for (Preference realPref : entry.getValue()) {
+        User testUser = entry.getKey();
+        try {
+          double estimatedPreference =
+                  recommender.estimatePreference(testUser.getID(), realPref.getItem().getID());
+          if (!Double.isNaN(estimatedPreference)) {
+            double diff = realPref.getValue() - estimatedPreference;
+            average.addDatum(diff * diff);
+          }
+        } catch (NoSuchElementException nsee) {
+          // It's possible that an item exists in the test data but not training data in which case
+          // NSEE will be thrown. Just ignore it and move on.
+          log.log(Level.INFO, "Element exists in test data but not training data: " + testUser.getID(), nsee);
+        }
+      }
+    }
+    return Math.sqrt(average.getAverage());
+  }
+
+  @Override
+  public String toString() {
+    return "RMSRecommenderEvaluator";
+  }
+
+}

Added: lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/ByItemPreferenceComparator.java
URL: http://svn.apache.org/viewvc/lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/ByItemPreferenceComparator.java?rev=654943&view=auto
==============================================================================
--- lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/ByItemPreferenceComparator.java (added)
+++ lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/ByItemPreferenceComparator.java Fri May  9 14:35:12 2008
@@ -0,0 +1,50 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.model;
+
+import org.apache.mahout.cf.taste.model.Preference;
+
+import java.io.Serializable;
+import java.util.Comparator;
+
+/**
+ * <p>{@link java.util.Comparator} that orders {@link org.apache.mahout.cf.taste.model.Preference}s by
+ * {@link org.apache.mahout.cf.taste.model.Item}.</p>
+ */
+public final class ByItemPreferenceComparator implements Comparator<Preference>, Serializable {
+
+  private static final Comparator<Preference> instance = new ByItemPreferenceComparator();
+
+  private ByItemPreferenceComparator() {
+    // singleton
+  }
+
+  public static Comparator<Preference> getInstance() {
+    return instance;
+  }
+
+  public int compare(Preference o1, Preference o2) {
+    return o1.getItem().compareTo(o2.getItem());
+  }
+
+  @Override
+  public String toString() {
+    return "ByItemPreferenceComparator";
+  }
+
+}

Added: lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/ByUserPreferenceComparator.java
URL: http://svn.apache.org/viewvc/lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/ByUserPreferenceComparator.java?rev=654943&view=auto
==============================================================================
--- lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/ByUserPreferenceComparator.java (added)
+++ lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/ByUserPreferenceComparator.java Fri May  9 14:35:12 2008
@@ -0,0 +1,49 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.model;
+
+import org.apache.mahout.cf.taste.model.Preference;
+
+import java.io.Serializable;
+import java.util.Comparator;
+
+/**
+ * <p>{@link java.util.Comparator} that orders {@link org.apache.mahout.cf.taste.model.Preference}s by
+ * {@link org.apache.mahout.cf.taste.model.User}.</p>
+ */
+public final class ByUserPreferenceComparator implements Comparator<Preference>, Serializable {
+
+  private static final Comparator<Preference> instance = new ByUserPreferenceComparator();
+
+  private ByUserPreferenceComparator() {
+  }
+
+  public static Comparator<Preference> getInstance() {
+    return instance;
+  }
+
+  public int compare(Preference o1, Preference o2) {
+    return o1.getUser().compareTo(o2.getUser());
+  }
+
+  @Override
+  public String toString() {
+    return "ByUserPreferenceComparator";
+  }
+
+}

Added: lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/ByValuePreferenceComparator.java
URL: http://svn.apache.org/viewvc/lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/ByValuePreferenceComparator.java?rev=654943&view=auto
==============================================================================
--- lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/ByValuePreferenceComparator.java (added)
+++ lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/ByValuePreferenceComparator.java Fri May  9 14:35:12 2008
@@ -0,0 +1,57 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.model;
+
+import org.apache.mahout.cf.taste.model.Preference;
+
+import java.io.Serializable;
+import java.util.Comparator;
+
+/**
+ * <p>{@link Comparator} that orders {@link org.apache.mahout.cf.taste.model.Preference}s from least preferred
+ * to most preferred -- that is, in order of ascending value.</p>
+ */
+public final class ByValuePreferenceComparator implements Comparator<Preference>, Serializable {
+
+  private static final Comparator<Preference> instance = new ByValuePreferenceComparator();
+
+  private ByValuePreferenceComparator() {
+  }
+
+  public static Comparator<Preference> getInstance() {
+    return instance;
+  }
+
+  public int compare(Preference o1, Preference o2) {
+    double value1 = o1.getValue();
+    double value2 = o2.getValue();
+    if (value1 < value2) {
+      return -1;
+    } else if (value1 > value2) {
+      return 1;
+    } else {
+      return 0;
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "ByValuePreferenceComparator";
+  }
+
+}

Added: lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/DetailedPreference.java
URL: http://svn.apache.org/viewvc/lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/DetailedPreference.java?rev=654943&view=auto
==============================================================================
--- lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/DetailedPreference.java (added)
+++ lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/DetailedPreference.java Fri May  9 14:35:12 2008
@@ -0,0 +1,55 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.model;
+
+import org.apache.mahout.cf.taste.model.Item;
+import org.apache.mahout.cf.taste.model.User;
+
+import java.text.DateFormat;
+import java.util.Date;
+
+/**
+ * <p>An expanded version of {@link GenericPreference} which adds more fields;  for now, this only includes
+ * an additional timestamp field. This is provided as a convenience to implementations and
+ * {@link org.apache.mahout.cf.taste.model.DataModel}s which wish to record and use this information in computations.
+ * This information is not added to {@link org.apache.mahout.cf.taste.impl.model.GenericPreference} to avoid expanding
+ * memory requirements of the algorithms supplied with Taste, since memory is a limiting factor.</p>
+ */
+public class DetailedPreference extends GenericPreference {
+
+  private final long timestamp;
+
+  public DetailedPreference(User user, Item item, double value, long timestamp) {
+    super(user, item, value);
+    if (timestamp < 0L) {
+      throw new IllegalArgumentException("timestamp is negative");
+    }
+    this.timestamp = timestamp;
+  }
+
+  public long getTimestamp() {
+    return timestamp;
+  }
+
+  @Override
+  public String toString() {
+    return "GenericPreference[user: " + getUser() + ", item:" + getItem() + ", value:" + getValue() +
+           ", timestamp: " + DateFormat.getDateTimeInstance().format(new Date(timestamp)) + ']';
+  }
+
+}

Added: lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericDataModel.java
URL: http://svn.apache.org/viewvc/lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericDataModel.java?rev=654943&view=auto
==============================================================================
--- lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericDataModel.java (added)
+++ lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericDataModel.java Fri May  9 14:35:12 2008
@@ -0,0 +1,187 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.model;
+
+import org.apache.mahout.cf.taste.common.TasteException;
+import org.apache.mahout.cf.taste.impl.common.ArrayIterator;
+import org.apache.mahout.cf.taste.impl.common.EmptyIterable;
+import org.apache.mahout.cf.taste.model.DataModel;
+import org.apache.mahout.cf.taste.model.Item;
+import org.apache.mahout.cf.taste.model.Preference;
+import org.apache.mahout.cf.taste.model.User;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+/**
+ * <p>A simple {@link DataModel} which uses a given {@link List} of {@link User}s as
+ * its data source. This implementation is mostly useful for small experiments and is not
+ * recommended for contexts where performance is important.</p>
+ */
+public final class GenericDataModel implements DataModel, Serializable {
+
+  private static final Preference[] NO_PREFS_ARRAY = new Preference[0];
+  private static final Iterable<Preference> NO_PREFS_ITERABLE = new EmptyIterable<Preference>();
+
+  private final List<User> users;
+  private final Map<Object, User> userMap;
+  private final List<Item> items;
+  private final Map<Object, Item> itemMap;
+  private final Map<Object, Preference[]> preferenceForItems;
+
+  /**
+   * <p>Creates a new {@link GenericDataModel} from the given {@link User}s (and their preferences).
+   * This {@link DataModel} retains all this information in memory and is effectively immutable.</p>
+   *
+   * @param users {@link User}s to include in this {@link GenericDataModel}
+   */
+  public GenericDataModel(Iterable<? extends User> users) {
+    if (users == null) {
+      throw new IllegalArgumentException("users is null");
+    }
+
+    this.userMap = new HashMap<Object, User>();
+    this.itemMap = new HashMap<Object, Item>();
+    // I'm abusing generics a little here since I want to use this (huge) map to hold Lists,
+    // then arrays, and don't want to allocate two Maps at once here.
+    Map<Object, Object> prefsForItems = new HashMap<Object, Object>();
+    for (User user : users) {
+      userMap.put(user.getID(), user);
+      Preference[] prefsArray = user.getPreferencesAsArray();
+      for (int i = 0; i < prefsArray.length; i++) {
+        Preference preference = prefsArray[i];
+        Item item = preference.getItem();
+        Object itemID = item.getID();
+        itemMap.put(itemID, item);
+        List<Preference> prefsForItem = (List<Preference>) prefsForItems.get(itemID);
+        if (prefsForItem == null) {
+          prefsForItem = new ArrayList<Preference>();
+          prefsForItems.put(itemID, prefsForItem);
+        }
+        prefsForItem.add(preference);
+      }
+    }
+
+    List<User> usersCopy = new ArrayList<User>(userMap.values());
+    Collections.sort(usersCopy);
+    this.users = Collections.unmodifiableList(usersCopy);
+
+    List<Item> itemsCopy = new ArrayList<Item>(itemMap.values());
+    Collections.sort(itemsCopy);
+    this.items = Collections.unmodifiableList(itemsCopy);
+
+    // Swap out lists for arrays here -- using the same Map. This is why the generics mess is worth it.
+    for (Map.Entry<Object, Object> entry : prefsForItems.entrySet()) {
+      List<Preference> list = (List<Preference>) entry.getValue();
+      Preference[] prefsAsArray = list.toArray(new Preference[list.size()]);
+      Arrays.sort(prefsAsArray, ByUserPreferenceComparator.getInstance());
+      entry.setValue(prefsAsArray);
+    }
+    // Yeah more generics ugliness
+    this.preferenceForItems = (Map<Object, Preference[]>) (Map<Object, ?>) prefsForItems;
+  }
+
+  /**
+   * <p>Creates a new {@link GenericDataModel} containing an immutable copy of the data from another
+   * given {@link DataModel}.</p>
+   *
+   * @param dataModel {@link DataModel} to copy
+   * @throws TasteException if an error occurs while retrieving the other {@link DataModel}'s users
+   */
+  public GenericDataModel(DataModel dataModel) throws TasteException {
+    this(dataModel.getUsers());
+  }
+
+  public Iterable<? extends User> getUsers() {
+    return users;
+  }
+
+  /**
+   * @throws NoSuchElementException if there is no such {@link User}
+   */
+  public User getUser(Object id) {
+    User user = userMap.get(id);
+    if (user == null) {
+      throw new NoSuchElementException();
+    }
+    return user;
+  }
+
+  public Iterable<? extends Item> getItems() {
+    return items;
+  }
+
+  /**
+   * @throws NoSuchElementException if there is no such {@link Item}
+   */
+  public Item getItem(Object id) {
+    Item item = itemMap.get(id);
+    if (item == null) {
+      throw new NoSuchElementException();
+    }
+    return item;
+  }
+
+  public Iterable<? extends Preference> getPreferencesForItem(Object itemID) {
+    Preference[] prefs = preferenceForItems.get(itemID);
+    return prefs == null ? NO_PREFS_ITERABLE : new ArrayIterator<Preference>(getPreferencesForItemAsArray(itemID));
+  }
+
+  public Preference[] getPreferencesForItemAsArray(Object itemID) {
+    Preference[] prefs = preferenceForItems.get(itemID);
+    return prefs == null ? NO_PREFS_ARRAY : prefs;
+  }
+
+  public int getNumItems() {
+    return items.size();
+  }
+
+  public int getNumUsers() {
+    return users.size();
+  }
+
+  /**
+   * @throws UnsupportedOperationException
+   */
+  public void setPreference(Object userID, Object itemID, double value) {
+    throw new UnsupportedOperationException();
+  }
+
+  /**
+   * @throws UnsupportedOperationException
+   */
+  public void removePreference(Object userID, Object itemID) {
+    throw new UnsupportedOperationException();
+  }
+
+  public void refresh() {
+    // Does nothing
+  }
+
+  @Override
+  public String toString() {
+    return "GenericDataModel[users:" + users + ']';
+  }
+
+}

Added: lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericItem.java
URL: http://svn.apache.org/viewvc/lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericItem.java?rev=654943&view=auto
==============================================================================
--- lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericItem.java (added)
+++ lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericItem.java Fri May  9 14:35:12 2008
@@ -0,0 +1,71 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.model;
+
+import org.apache.mahout.cf.taste.model.Item;
+
+import java.io.Serializable;
+
+/**
+ * <p>An {@link Item} which has no data other than an ID. This may be most useful for writing tests.</p>
+ */
+public class GenericItem<K extends Comparable<K>> implements Item, Serializable {
+
+  private final K id;
+  private final boolean recommendable;
+
+  public GenericItem(K id) {
+    this(id, true);
+  }
+
+  public GenericItem(K id, boolean recommendable) {
+    if (id == null) {
+      throw new IllegalArgumentException("id is null");
+    }
+    this.id = id;
+    this.recommendable = recommendable;
+  }
+
+  public Object getID() {
+    return id;
+  }
+
+  public boolean isRecommendable() {
+    return recommendable;
+  }
+
+  @Override
+  public int hashCode() {
+    return id.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    return obj instanceof Item && ((Item) obj).getID().equals(id);
+  }
+
+  @Override
+  public String toString() {
+    return "Item[id:" + String.valueOf(id) + ']';
+  }
+
+  public int compareTo(Item item) {
+    return id.compareTo((K) item.getID());
+  }
+
+}

Added: lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericPreference.java
URL: http://svn.apache.org/viewvc/lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericPreference.java?rev=654943&view=auto
==============================================================================
--- lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericPreference.java (added)
+++ lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericPreference.java Fri May  9 14:35:12 2008
@@ -0,0 +1,87 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.model;
+
+import org.apache.mahout.cf.taste.model.Item;
+import org.apache.mahout.cf.taste.model.Preference;
+import org.apache.mahout.cf.taste.model.User;
+
+import java.io.Serializable;
+
+/**
+ * <p>A simple {@link Preference} encapsulating an {@link Item} and preference value.</p>
+ */
+public class GenericPreference implements Preference, Serializable {
+
+  private User user;
+  private final Item item;
+  private double value;
+
+  public GenericPreference(User user, Item item, double value) {
+    if (item == null) {
+      throw new IllegalArgumentException("item is null");
+    }
+    if (Double.isNaN(value)) {
+      throw new IllegalArgumentException("Invalid value: " + value);
+    }
+    this.user = user;
+    this.item = item;
+    this.value = value;
+  }
+
+  public User getUser() {
+    if (user == null) {
+      throw new IllegalStateException("User was never set");
+    }
+    return user;
+  }
+
+  /**
+   * <p>Let this be set by {@link GenericUser} to avoid a circularity problem -- each
+   * wants a reference to the other in the constructor.</p>
+   *
+   * @param user user whose preference this is
+   */
+  void setUser(User user) {
+    if (user == null) {
+      throw new IllegalArgumentException("user is null");
+    }
+    this.user = user;
+  }
+
+  public Item getItem() {
+    return item;
+  }
+
+  public double getValue() {
+    return value;
+  }
+
+  public void setValue(double value) {
+    if (Double.isNaN(value)) {
+      throw new IllegalArgumentException("Invalid value: " + value);
+    }
+    this.value = value;
+  }
+
+  @Override
+  public String toString() {
+    return "GenericPreference[user: " + user + ", item:" + item + ", value:" + value + ']';
+  }
+
+}

Added: lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericUser.java
URL: http://svn.apache.org/viewvc/lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericUser.java?rev=654943&view=auto
==============================================================================
--- lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericUser.java (added)
+++ lucene/mahout/trunk/core/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericUser.java Fri May  9 14:35:12 2008
@@ -0,0 +1,101 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.model;
+
+import org.apache.mahout.cf.taste.impl.common.ArrayIterator;
+import org.apache.mahout.cf.taste.model.Preference;
+import org.apache.mahout.cf.taste.model.User;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * <p>A simple {@link User} which has simply an ID and some {@link Collection} of
+ * {@link Preference}s.</p>
+ */
+public class GenericUser<K extends Comparable<K>> implements User, Serializable {
+
+  private static final Preference[] NO_PREFS = new Preference[0];
+
+  private final K id;
+  private final Map<Object, Preference> data;
+  // Use an array for maximum performance
+  private final Preference[] values;
+
+  public GenericUser(K id, Collection<Preference> preferences) {
+    if (id == null) {
+      throw new IllegalArgumentException("id is null");
+    }
+    this.id = id;
+    if (preferences == null || preferences.isEmpty()) {
+      data = Collections.emptyMap();
+      values = NO_PREFS;
+    } else {
+      data = new HashMap<Object, Preference>();
+      values = preferences.toArray(new Preference[preferences.size()]);
+      for (Preference preference : values) {
+        // Is this hacky?
+        if (preference instanceof GenericPreference) {
+          ((GenericPreference) preference).setUser(this);
+        }
+        data.put(preference.getItem().getID(), preference);
+      }
+      Arrays.sort(values, ByItemPreferenceComparator.getInstance());
+    }
+  }
+
+  public K getID() {
+    return id;
+  }
+
+  public Preference getPreferenceFor(Object itemID) {
+    return data.get(itemID);
+  }
+
+  public Iterable<Preference> getPreferences() {
+    return new ArrayIterator<Preference>(values);
+  }
+
+  public Preference[] getPreferencesAsArray() {
+    return values;
+  }
+
+  @Override
+  public int hashCode() {
+    return id.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    return obj instanceof User && ((User) obj).getID().equals(id);
+  }
+
+  @Override
+  public String toString() {
+    return "User[id:" + String.valueOf(id) + ']';
+  }
+
+  public int compareTo(User o) {
+    return id.compareTo((K) o.getID());
+  }
+
+}