2 rcs_id('$Id: RatingsUser.php,v 1.2 2004-06-21 17:01:41 rurban Exp $');
3 /* Copyright (C) 2004 Dan Frankowski
5 * This file is (not yet) part of PhpWiki.
7 * PhpWiki is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
12 * PhpWiki is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with PhpWiki; if not, write to the Free Software
19 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22 require_once("lib/wikilens/RatingsDb.php");
25 * Get a RatingsUser instance (possibly from a cache).
27 class RatingsUserFactory {
28 function & getUser($userid) {
29 //print "getUser($userid) ";
30 global $_ratingsUserCache;
31 if (!isset($_ratingsUserCache)) {
32 $_ratingsUserCache = array();
34 if (!array_key_exists($userid, $_ratingsUserCache)) {
36 $_ratingsUserCache[$userid] = new RatingsUser($userid);
41 return $_ratingsUserCache[$userid];
46 * This class represents a user that gets ratings
56 function RatingsUser($userid) {
57 $this->_userid = $userid;
58 $this->_ratings_loaded = false;
59 $this->_ratings = array();
60 $this->_num_ratings = 0;
61 $this->_mean_ratings = array();
62 $this->_pearson_sims = array();
66 return $this->_userid;
69 function _get_rating_dbi() {
70 // This is a hack, because otherwise this object doesn't know about a
71 // DBI at all. Perhaps all this ratings stuff should live somewhere
72 // else that's less of a base class.
73 if (isset($this->_rdbi))
75 $this->_rdbi = new RatingsDb();
79 // XXX: may want to think about caching ratings in the PHP session
80 // since a WikiUser is created for *every* access, in which case rate.php
81 // will want to change to use this object instead of direct db access
84 * Check whether $user is allowed to view this user's ratings
86 * @return bool True if $user can view this user's ratings, false otherwise
88 function allow_view_ratings($user)
94 * Gets this user's ratings
96 * @return array Assoc. array [page_name][dimension] = _UserRating object
98 function get_ratings()
100 $this->_load_ratings();
101 return $this->_ratings;
105 * Gets this user's mean rating across a dimension
107 * @return float Mean rating
109 function mean_rating($dimension = 0)
111 // use memoized result if available
112 if (isset($this->_mean_ratings[$dimension]))
114 return $this->_mean_ratings[$dimension];
117 $ratings = $this->get_ratings();
121 // walk the ratings and aggregate those in this dimension
122 foreach($ratings as $page => $rating)
124 if (isset($rating[$dimension]))
126 $total += $rating[$dimension]->get_rating();
131 // memoize and return result
132 $this->_mean_ratings[$dimension] = ($n == 0 ? 0 : $total / $n);
133 return $this->_mean_ratings[$dimension];
136 // Note: the following has_rated, get_rating, set_rating, and unset_rating
137 // methods are colossally inefficient as they do a full ratings load from
138 // the database before performing their intended operation -- as such, the
139 // rate.php script still uses the direct database methods (plus, it's very
140 // ephemeral and doesn't particularly care about the ratings count or any
141 // other features that these methods might provide down the road)
143 function has_rated($pagename, $dimension = null)
145 // XXX: does this really want to do a full ratings load? (scalability?)
146 $this->_load_ratings();
147 if (isset($dimension))
149 if (isset($this->_ratings[$pagename][$dimension]))
156 if (isset($this->_ratings[$pagename]))
164 function get_rating($pagename, $dimension = 0)
166 // XXX: does this really want to do a full ratings load? (scalability?)
167 $this->_load_ratings();
168 if ($this->has_rated($pagename, $dimension))
170 return $this->_ratings[$pagename][$dimension]->get_rating();
175 function set_rating($pagename, $dimension, $rating)
177 // XXX: does this really want to do a full ratings load? (scalability?)
178 $this->_load_ratings();
180 // XXX: what to do on failure?
181 $dbi = $this->_get_rating_dbi();
182 if (!($dbi->rate($this->_userid, $pagename, $dimension, $rating)))
187 if ($this->has_rated($pagename, $dimension))
189 $this->_ratings[$pagename][$dimension]->set_rating($rating);
193 $this->_num_ratings++;
194 $this->_ratings[$rating['pagename']][$rating['dimension']]
195 = new _UserRating($this->_userid, $pagename, $dimension, $rating);
199 function unset_rating($pagename, $dimension)
201 // XXX: does this really want to do a full ratings load? (scalability?)
202 $this->_load_ratings();
203 if ($this->has_rated($pagename, $dimension))
205 // XXX: what to do on failure?
206 if ($this->_dbi->delete_rating($this->_userid,$pagename,$dimension))
208 $this->_num_ratings--;
209 unset($this->_ratings[$pagename][$dimension]);
210 if (!count($this->_ratings[$pagename]))
212 unset($this->_ratings[$pagename]);
218 function pearson_similarity($user, $dimension = 0)
220 // use memoized result if available
221 if (isset($this->_pearson_sims[$user->getId()][$dimension]))
223 return $this->_pearson_sims[$user->getId()][$dimension];
226 $ratings1 = $this->get_ratings();
227 $mean1 = $this->mean_rating($dimension);
228 // XXX: sanify user input?
229 $ratings2 = $user->get_ratings();
230 $mean2 = $user->mean_rating($dimension);
232 // swap if it would speed things up a bit
233 if (count($ratings1) < count($ratings2))
236 $ratings1 = $ratings2;
243 list($sum11, $sum22, $sum12, $n) = array(0,0,0,0);
245 // compute sum(x*x), sum(y*y), sum(x*y) over co-rated items
246 foreach ($ratings1 as $page => $rating1)
248 if (isset($rating1[$dimension]) && isset($ratings2[$page]))
250 $rating2 = $ratings2[$page];
251 if (isset($rating2[$dimension]))
253 $r1 = $rating1[$dimension]->get_rating();
254 $r2 = $rating2[$dimension]->get_rating();
255 // print "co-rating with " . $user->getId() . " $page $r1 $r2<BR>";
268 // this returns both the computed similarity and the number of co-rated
269 // items that the similarity was based on
271 // prevent division-by-zero
272 if (sqrt($sum11) == 0 || sqrt($sum12) == 0)
275 // Pearson similarity
276 $sim = array($sum12 / (sqrt($sum11) * sqrt($sum22)), $n);
278 // print "sim is " . $sim[0] . "<BR><BR>";
281 $this->_pearson_sims[$user->getId()][$dimension] = $sim;
282 return $this->_pearson_sims[$user->getId()][$dimension] = $sim;
285 function knn_uu_predict($pagename, &$neighbors, $dimension = 0)
289 var_dump($this->_pearson_sims);
290 var_dump($this->_ratings);
292 print "pred for $pagename<BR>";
297 // foreach($neighbors as $nbor)
299 for($i = 0; $i < count($neighbors); $i++)
301 // more silly PHP references...
302 $nbor =& $neighbors[$i];
304 // ignore self-neighbor
305 if ($this->getId() == $nbor->getId())
308 if ($nbor->has_rated($pagename, $dimension))
310 list($sim, $n_items) = $this->pearson_similarity($nbor);
311 // ignore absolute sims below 0.1, negative sims??
312 // XXX: no filtering done... small-world = too few neighbors
313 if (1 || ($sim > 0 && abs($sim) >= 0.1))
315 // n/50 sig weighting
317 $sim *= $n_items / 50;
319 print "neighbor is " . $nbor->getId() . "<BR>";
320 print "weighted sim is " . $sim . "<BR>";
321 print "dev from mean is " . ($nbor->get_rating($pagename, $dimension) - $nbor->mean_rating($dimension)) . "<BR>";
323 $total += $sim * ($nbor->get_rating($pagename, $dimension) - $nbor->mean_rating($dimension));
324 $total_sim += abs($sim);
329 $my_mean = $this->mean_rating($dimension);
331 print "your mean is $my_mean<BR>";
332 print "pred dev from mean is " . ($total_sim == 0 ? -1 : ($total / $total_sim)) . "<BR>";
333 print "pred is " . ($total_sim == 0 ? -1 : ($total / $total_sim + $my_mean)) . "<BR><BR>";
335 // XXX: what to do if no neighbors have rated pagename?
336 return ($total_sim == 0 ? 0 : ($total / $total_sim + $my_mean));
339 function _load_ratings($force = false)
341 if (!$this->_ratings_loaded || $force)
343 // print "load " . $this->getId() . "<BR>";
344 $this->_ratings = array();
345 $this->_num_ratings = 0;
346 // only signed-in users have ratings (XXX: authenticated?)
348 // passing null as first parameter to indicate all dimensions
349 $dbi = $this->_get_rating_dbi();
350 $rating_iter = $dbi->sql_get_rating(null, $this->_userid, null);
351 //$rating_iter = $dbi->get_rating(null, $this->_userid);
352 while($rating = $rating_iter->next())
354 $this->_num_ratings++;
355 $this->_ratings[$rating['pagename']][$rating['dimension']]
356 = new _UserRating($this->_userid,
358 $rating['dimension'],
359 $rating['ratingvalue']);
361 $this->_ratings_loaded = true;
366 /** Represent a rating. */
369 function _UserRating ($rater, $ratee, $dimension, $rating)
371 $this->rater = (string)$rater;
372 $this->ratee = (string)$ratee;
373 $this->dimension = (int)$dimension;
374 $this->rating = (float)$rating;
387 function get_rating()
389 return $this->rating;
392 function get_dimension()
394 return $this->dimension;
399 $this->rater = (string)$rater;
404 $this->ratee = (string)$ratee;
407 function set_rating()
409 $this->rating = (float)$rating;
412 function set_dimension()
414 $this->dimension = (int)$dimension;
418 // $Log: not supported by cvs2svn $
419 // Revision 1.1 2004/06/18 14:42:17 rurban
420 // added wikilens libs (not yet merged good enough, some work for DanFr)
427 // c-hanging-comment-ender-p: nil
428 // indent-tabs-mode: nil