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());
+ }
+
+}