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
30 function & getUser($userid)
32 //print "getUser($userid) ";
33 global $_ratingsUserCache;
34 if (!isset($_ratingsUserCache)) {
35 $_ratingsUserCache = array();
37 if (!array_key_exists($userid, $_ratingsUserCache)) {
39 $_ratingsUserCache[$userid] = new RatingsUser($userid);
43 return $_ratingsUserCache[$userid];
48 * This class represents a user that gets ratings
59 function RatingsUser($userid)
61 $this->_userid = $userid;
62 $this->_ratings_loaded = false;
63 $this->_ratings = array();
64 $this->_num_ratings = 0;
65 $this->_mean_ratings = array();
66 $this->_pearson_sims = array();
71 return $this->_userid;
74 function & _get_rating_dbi()
76 // This is a hack, because otherwise this object doesn't know about a
77 // DBI at all. Perhaps all this ratings stuff should live somewhere
78 // else that's less of a base class.
79 if (isset($this->_rdbi))
81 $this->_rdbi = RatingsDb::getTheRatingsDb();
85 // XXX: may want to think about caching ratings in the PHP session
86 // since a WikiUser is created for *every* access, in which case rate.php
87 // will want to change to use this object instead of direct db access
90 * Check whether $user is allowed to view this user's ratings
92 * @return bool True if $user can view this user's ratings, false otherwise
94 function allow_view_ratings($user)
100 * Gets this user's ratings
102 * @return array Assoc. array [page_name][dimension] = _UserRating object
104 function get_ratings()
106 $this->_load_ratings();
107 return $this->_ratings;
111 * Gets this user's mean rating across a dimension
113 * @return float Mean rating
115 function mean_rating($dimension = 0)
117 // use memoized result if available
118 if (isset($this->_mean_ratings[$dimension])) {
119 return $this->_mean_ratings[$dimension];
122 $ratings = $this->get_ratings();
126 // walk the ratings and aggregate those in this dimension
127 foreach ($ratings as $page => $rating) {
128 if (isset($rating[$dimension])) {
129 $total += $rating[$dimension]->get_rating();
134 // memoize and return result
135 $this->_mean_ratings[$dimension] = ($n == 0 ? 0 : $total / $n);
136 return $this->_mean_ratings[$dimension];
139 // Note: the following has_rated, get_rating, set_rating, and unset_rating
140 // methods are colossally inefficient as they do a full ratings load from
141 // the database before performing their intended operation -- as such, the
142 // rate.php script still uses the direct database methods (plus, it's very
143 // ephemeral and doesn't particularly care about the ratings count or any
144 // other features that these methods might provide down the road)
146 function has_rated($pagename, $dimension = null)
148 // XXX: does this really want to do a full ratings load? (scalability?)
149 $this->_load_ratings();
150 if (isset($dimension)) {
151 if (isset($this->_ratings[$pagename][$dimension])) {
155 if (isset($this->_ratings[$pagename])) {
162 function get_rating($pagename, $dimension = 0)
164 // XXX: does this really want to do a full ratings load? (scalability?)
165 if (RATING_STORAGE == 'SQL')
166 $this->_load_ratings();
168 $rdbi = $this->_get_rating_dbi();
169 return $rdbi->metadata_get_rating($this->getId(), $pagename, $dimension);
172 if ($this->has_rated($pagename, $dimension)) {
173 return $this->_ratings[$pagename][$dimension]->get_rating();
178 function set_rating($pagename, $dimension, $rating)
180 // XXX: does this really want to do a full ratings load? (scalability?)
181 $this->_load_ratings();
183 // XXX: what to do on failure?
184 $dbi = $this->_get_rating_dbi();
185 if (!($dbi->rate($this->_userid, $pagename, $dimension, $rating))) {
189 if ($this->has_rated($pagename, $dimension)) {
190 $this->_ratings[$pagename][$dimension]->set_rating($rating);
192 $this->_num_ratings++;
193 $this->_ratings[$rating['pagename']][$rating['dimension']]
194 = new _UserRating($this->_userid, $pagename, $dimension, $rating);
198 function unset_rating($pagename, $dimension)
200 // XXX: does this really want to do a full ratings load? (scalability?)
201 $this->_load_ratings();
202 if ($this->has_rated($pagename, $dimension)) {
203 // XXX: what to do on failure?
204 if ($this->_dbi->delete_rating($this->_userid, $pagename, $dimension)) {
205 $this->_num_ratings--;
206 unset($this->_ratings[$pagename][$dimension]);
207 if (!count($this->_ratings[$pagename])) {
208 unset($this->_ratings[$pagename]);
214 function pearson_similarity($user, $dimension = 0)
216 // use memoized result if available
217 if (isset($this->_pearson_sims[$user->getId()][$dimension])) {
218 return $this->_pearson_sims[$user->getId()][$dimension];
221 $ratings1 = $this->get_ratings();
222 $mean1 = $this->mean_rating($dimension);
223 // XXX: sanify user input?
224 $ratings2 = $user->get_ratings();
225 $mean2 = $user->mean_rating($dimension);
227 // swap if it would speed things up a bit
228 if (count($ratings1) < count($ratings2)) {
230 $ratings1 = $ratings2;
237 list($sum11, $sum22, $sum12, $n) = array(0, 0, 0, 0);
239 // compute sum(x*x), sum(y*y), sum(x*y) over co-rated items
240 foreach ($ratings1 as $page => $rating1) {
241 if (isset($rating1[$dimension]) && isset($ratings2[$page])) {
242 $rating2 = $ratings2[$page];
243 if (isset($rating2[$dimension])) {
244 $r1 = $rating1[$dimension]->get_rating();
245 $r2 = $rating2[$dimension]->get_rating();
246 // print "co-rating with " . $user->getId() . " $page $r1 $r2<BR>";
259 // this returns both the computed similarity and the number of co-rated
260 // items that the similarity was based on
262 // prevent division-by-zero
263 if (sqrt($sum11) == 0 || sqrt($sum12) == 0)
266 // Pearson similarity
267 $sim = array($sum12 / (sqrt($sum11) * sqrt($sum22)), $n);
269 // print "sim is " . $sim[0] . "<BR><BR>";
272 $this->_pearson_sims[$user->getId()][$dimension] = $sim;
273 return $this->_pearson_sims[$user->getId()][$dimension] = $sim;
276 function knn_uu_predict($pagename, &$neighbors, $dimension = 0)
280 var_dump($this->_pearson_sims);
281 var_dump($this->_ratings);
283 print "pred for $pagename<BR>";
288 // foreach($neighbors as $nbor)
290 for ($i = 0; $i < count($neighbors); $i++) {
291 // more silly PHP references...
292 $nbor =& $neighbors[$i];
294 // ignore self-neighbor
295 if ($this->getId() == $nbor->getId())
298 if ($nbor->has_rated($pagename, $dimension)) {
299 list($sim, $n_items) = $this->pearson_similarity($nbor);
300 // ignore absolute sims below 0.1, negative sims??
301 // XXX: no filtering done... small-world = too few neighbors
302 if (1 || ($sim > 0 && abs($sim) >= 0.1)) {
303 // n/50 sig weighting
305 $sim *= $n_items / 50;
307 print "neighbor is " . $nbor->getId() . "<BR>";
308 print "weighted sim is " . $sim . "<BR>";
309 print "dev from mean is " . ($nbor->get_rating($pagename, $dimension) - $nbor->mean_rating($dimension)) . "<BR>";
311 $total += $sim * ($nbor->get_rating($pagename, $dimension) - $nbor->mean_rating($dimension));
312 $total_sim += abs($sim);
317 $my_mean = $this->mean_rating($dimension);
319 print "your mean is $my_mean<BR>";
320 print "pred dev from mean is " . ($total_sim == 0 ? -1 : ($total / $total_sim)) . "<BR>";
321 print "pred is " . ($total_sim == 0 ? -1 : ($total / $total_sim + $my_mean)) . "<BR><BR>";
323 // XXX: what to do if no neighbors have rated pagename?
324 return ($total_sim == 0 ? 0 : ($total / $total_sim + $my_mean));
327 function _load_ratings($force = false)
329 if (!$this->_ratings_loaded || $force) {
330 // print "load " . $this->getId() . "<BR>";
331 $this->_ratings = array();
332 $this->_num_ratings = 0;
333 // only signed-in users have ratings (XXX: authenticated?)
335 // passing null as first parameter to indicate all dimensions
336 $dbi = $this->_get_rating_dbi();
338 //$rating_iter = $dbi->sql_get_rating(null, $this->_userid, null);
339 //($dimension=null, $rater=null, $ratee=null, $orderby = null, $pageinfo = "ratee")
340 $rating_iter = $dbi->get_rating_page(null, $this->_userid);
342 while ($rating = $rating_iter->next()) {
343 if (defined('FUSIONFORGE') and FUSIONFORGE) {
344 $rating['pagename'] = preg_replace('/^' . PAGE_PREFIX . '/', '', $rating['pagename']);
346 $this->_num_ratings++;
347 $this->_ratings[$rating['pagename']][$rating['dimension']]
348 = new _UserRating($this->_userid,
350 $rating['dimension'],
351 $rating['ratingvalue']);
354 $this->_ratings_loaded = true;
359 /** Represent a rating. */
362 function _UserRating($rater, $ratee, $dimension, $rating)
364 $this->rater = (string)$rater;
365 $this->ratee = (string)$ratee;
366 $this->dimension = (int)$dimension;
367 $this->rating = (float)$rating;
380 function get_rating()
382 return $this->rating;
385 function get_dimension()
387 return $this->dimension;
392 $this->rater = (string)$rater;
397 $this->ratee = (string)$ratee;
400 function set_rating()
402 $this->rating = (float)$rating;
405 function set_dimension()
407 $this->dimension = (int)$dimension;
415 // c-hanging-comment-ender-p: nil
416 // indent-tabs-mode: nil