3 /* Copyright (C) 2004 Dan Frankowski
4 * Copyright (C) 2010 Roger Guignard, Alcatel-Lucent
6 * This file is part of PhpWiki.
8 * PhpWiki is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
13 * PhpWiki is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License along
19 * with PhpWiki; if not, write to the Free Software Foundation, Inc.,
20 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23 require_once 'lib/wikilens/RatingsDb.php';
26 * Get a RatingsUser instance (possibly from a cache).
28 class RatingsUserFactory {
29 function & getUser($userid) {
30 //print "getUser($userid) ";
31 global $_ratingsUserCache;
32 if (!isset($_ratingsUserCache)) {
33 $_ratingsUserCache = array();
35 if (!array_key_exists($userid, $_ratingsUserCache)) {
37 $_ratingsUserCache[$userid] = new RatingsUser($userid);
42 return $_ratingsUserCache[$userid];
47 * This class represents a user that gets ratings
57 function RatingsUser($userid) {
58 $this->_userid = $userid;
59 $this->_ratings_loaded = false;
60 $this->_ratings = array();
61 $this->_num_ratings = 0;
62 $this->_mean_ratings = array();
63 $this->_pearson_sims = array();
67 return $this->_userid;
70 function & _get_rating_dbi() {
71 // This is a hack, because otherwise this object doesn't know about a
72 // DBI at all. Perhaps all this ratings stuff should live somewhere
73 // else that's less of a base class.
74 if (isset($this->_rdbi))
76 $this->_rdbi = RatingsDb::getTheRatingsDb();
80 // XXX: may want to think about caching ratings in the PHP session
81 // since a WikiUser is created for *every* access, in which case rate.php
82 // will want to change to use this object instead of direct db access
85 * Check whether $user is allowed to view this user's ratings
87 * @return bool True if $user can view this user's ratings, false otherwise
89 function allow_view_ratings($user)
95 * Gets this user's ratings
97 * @return array Assoc. array [page_name][dimension] = _UserRating object
99 function get_ratings()
101 $this->_load_ratings();
102 return $this->_ratings;
106 * Gets this user's mean rating across a dimension
108 * @return float Mean rating
110 function mean_rating($dimension = 0)
112 // use memoized result if available
113 if (isset($this->_mean_ratings[$dimension]))
115 return $this->_mean_ratings[$dimension];
118 $ratings = $this->get_ratings();
122 // walk the ratings and aggregate those in this dimension
123 foreach($ratings as $page => $rating)
125 if (isset($rating[$dimension]))
127 $total += $rating[$dimension]->get_rating();
132 // memoize and return result
133 $this->_mean_ratings[$dimension] = ($n == 0 ? 0 : $total / $n);
134 return $this->_mean_ratings[$dimension];
137 // Note: the following has_rated, get_rating, set_rating, and unset_rating
138 // methods are colossally inefficient as they do a full ratings load from
139 // the database before performing their intended operation -- as such, the
140 // rate.php script still uses the direct database methods (plus, it's very
141 // ephemeral and doesn't particularly care about the ratings count or any
142 // other features that these methods might provide down the road)
144 function has_rated($pagename, $dimension = null)
146 // XXX: does this really want to do a full ratings load? (scalability?)
147 $this->_load_ratings();
148 if (isset($dimension))
150 if (isset($this->_ratings[$pagename][$dimension]))
157 if (isset($this->_ratings[$pagename]))
165 function get_rating($pagename, $dimension = 0)
167 // XXX: does this really want to do a full ratings load? (scalability?)
168 if (RATING_STORAGE == 'SQL')
169 $this->_load_ratings();
171 $rdbi = $this->_get_rating_dbi();
172 return $rdbi->metadata_get_rating($this->getId(), $pagename, $dimension);
175 if ($this->has_rated($pagename, $dimension))
177 return $this->_ratings[$pagename][$dimension]->get_rating();
182 function set_rating($pagename, $dimension, $rating)
184 // XXX: does this really want to do a full ratings load? (scalability?)
185 $this->_load_ratings();
187 // XXX: what to do on failure?
188 $dbi = $this->_get_rating_dbi();
189 if (!($dbi->rate($this->_userid, $pagename, $dimension, $rating)))
194 if ($this->has_rated($pagename, $dimension))
196 $this->_ratings[$pagename][$dimension]->set_rating($rating);
200 $this->_num_ratings++;
201 $this->_ratings[$rating['pagename']][$rating['dimension']]
202 = new _UserRating($this->_userid, $pagename, $dimension, $rating);
206 function unset_rating($pagename, $dimension)
208 // XXX: does this really want to do a full ratings load? (scalability?)
209 $this->_load_ratings();
210 if ($this->has_rated($pagename, $dimension))
212 // XXX: what to do on failure?
213 if ($this->_dbi->delete_rating($this->_userid,$pagename,$dimension))
215 $this->_num_ratings--;
216 unset($this->_ratings[$pagename][$dimension]);
217 if (!count($this->_ratings[$pagename]))
219 unset($this->_ratings[$pagename]);
225 function pearson_similarity($user, $dimension = 0)
227 // use memoized result if available
228 if (isset($this->_pearson_sims[$user->getId()][$dimension]))
230 return $this->_pearson_sims[$user->getId()][$dimension];
233 $ratings1 = $this->get_ratings();
234 $mean1 = $this->mean_rating($dimension);
235 // XXX: sanify user input?
236 $ratings2 = $user->get_ratings();
237 $mean2 = $user->mean_rating($dimension);
239 // swap if it would speed things up a bit
240 if (count($ratings1) < count($ratings2))
243 $ratings1 = $ratings2;
250 list($sum11, $sum22, $sum12, $n) = array(0,0,0,0);
252 // compute sum(x*x), sum(y*y), sum(x*y) over co-rated items
253 foreach ($ratings1 as $page => $rating1)
255 if (isset($rating1[$dimension]) && isset($ratings2[$page]))
257 $rating2 = $ratings2[$page];
258 if (isset($rating2[$dimension]))
260 $r1 = $rating1[$dimension]->get_rating();
261 $r2 = $rating2[$dimension]->get_rating();
262 // print "co-rating with " . $user->getId() . " $page $r1 $r2<BR>";
275 // this returns both the computed similarity and the number of co-rated
276 // items that the similarity was based on
278 // prevent division-by-zero
279 if (sqrt($sum11) == 0 || sqrt($sum12) == 0)
282 // Pearson similarity
283 $sim = array($sum12 / (sqrt($sum11) * sqrt($sum22)), $n);
285 // print "sim is " . $sim[0] . "<BR><BR>";
288 $this->_pearson_sims[$user->getId()][$dimension] = $sim;
289 return $this->_pearson_sims[$user->getId()][$dimension] = $sim;
292 function knn_uu_predict($pagename, &$neighbors, $dimension = 0)
296 var_dump($this->_pearson_sims);
297 var_dump($this->_ratings);
299 print "pred for $pagename<BR>";
304 // foreach($neighbors as $nbor)
306 for($i = 0; $i < count($neighbors); $i++)
308 // more silly PHP references...
309 $nbor =& $neighbors[$i];
311 // ignore self-neighbor
312 if ($this->getId() == $nbor->getId())
315 if ($nbor->has_rated($pagename, $dimension))
317 list($sim, $n_items) = $this->pearson_similarity($nbor);
318 // ignore absolute sims below 0.1, negative sims??
319 // XXX: no filtering done... small-world = too few neighbors
320 if (1 || ($sim > 0 && abs($sim) >= 0.1))
322 // n/50 sig weighting
324 $sim *= $n_items / 50;
326 print "neighbor is " . $nbor->getId() . "<BR>";
327 print "weighted sim is " . $sim . "<BR>";
328 print "dev from mean is " . ($nbor->get_rating($pagename, $dimension) - $nbor->mean_rating($dimension)) . "<BR>";
330 $total += $sim * ($nbor->get_rating($pagename, $dimension) - $nbor->mean_rating($dimension));
331 $total_sim += abs($sim);
336 $my_mean = $this->mean_rating($dimension);
338 print "your mean is $my_mean<BR>";
339 print "pred dev from mean is " . ($total_sim == 0 ? -1 : ($total / $total_sim)) . "<BR>";
340 print "pred is " . ($total_sim == 0 ? -1 : ($total / $total_sim + $my_mean)) . "<BR><BR>";
342 // XXX: what to do if no neighbors have rated pagename?
343 return ($total_sim == 0 ? 0 : ($total / $total_sim + $my_mean));
346 function _load_ratings($force = false)
348 if (!$this->_ratings_loaded || $force)
350 // print "load " . $this->getId() . "<BR>";
351 $this->_ratings = array();
352 $this->_num_ratings = 0;
353 // only signed-in users have ratings (XXX: authenticated?)
355 // passing null as first parameter to indicate all dimensions
356 $dbi = $this->_get_rating_dbi();
358 //$rating_iter = $dbi->sql_get_rating(null, $this->_userid, null);
359 //($dimension=null, $rater=null, $ratee=null, $orderby = null, $pageinfo = "ratee")
360 $rating_iter = $dbi->get_rating_page(null, $this->_userid);
362 while($rating = $rating_iter->next())
364 if (defined('FUSIONFORGE') and FUSIONFORGE) {
365 $rating['pagename'] = preg_replace('/^'.PAGE_PREFIX.'/', '', $rating['pagename']);
367 $this->_num_ratings++;
368 $this->_ratings[$rating['pagename']][$rating['dimension']]
369 = new _UserRating($this->_userid,
371 $rating['dimension'],
372 $rating['ratingvalue']);
375 $this->_ratings_loaded = true;
380 /** Represent a rating. */
383 function _UserRating ($rater, $ratee, $dimension, $rating)
385 $this->rater = (string)$rater;
386 $this->ratee = (string)$ratee;
387 $this->dimension = (int)$dimension;
388 $this->rating = (float)$rating;
401 function get_rating()
403 return $this->rating;
406 function get_dimension()
408 return $this->dimension;
413 $this->rater = (string)$rater;
418 $this->ratee = (string)$ratee;
421 function set_rating()
423 $this->rating = (float)$rating;
426 function set_dimension()
428 $this->dimension = (int)$dimension;
436 // c-hanging-comment-ender-p: nil
437 // indent-tabs-mode: nil