| 1 | <?php |
|---|
| 2 | |
|---|
| 3 | // infoarena rating system |
|---|
| 4 | // |
|---|
| 5 | // In laymen terms, rating systems provide a way to rank and differentiate |
|---|
| 6 | // contestants in multiple-round competitions. Ratings are computed with |
|---|
| 7 | // black math & statistics voodoo magic :) |
|---|
| 8 | // |
|---|
| 9 | // infoarena uses home-baked rating system combining features from |
|---|
| 10 | // glicko, TrueSkill, ELO and possibly others. |
|---|
| 11 | // |
|---|
| 12 | // See development documentation on ratings: |
|---|
| 13 | // http://hackers.devnet.ro/wiki/Rating |
|---|
| 14 | |
|---|
| 15 | // Glicko parameters |
|---|
| 16 | // These may be tweaked until they yield decent ratings |
|---|
| 17 | |
|---|
| 18 | // Initial rating assumed for a new contestant. |
|---|
| 19 | // Note that we're using standard ELO ratings (spanning 3000 and beyond) |
|---|
| 20 | define("IA_RATING_INITIAL", 1500); |
|---|
| 21 | // Initial deviation |
|---|
| 22 | define("IA_RATING_DEVIATION", 99); |
|---|
| 23 | // Deviation boundaries and median |
|---|
| 24 | define("IA_RATING_MAX_DEVIATION", 360); |
|---|
| 25 | define("IA_RATING_MIN_DEVIATION", 15); |
|---|
| 26 | define("IA_RATING_MED_DEVIATION", 45); |
|---|
| 27 | // threshold value to normalize results |
|---|
| 28 | // FIXME: Ratings don't change more than this |
|---|
| 29 | define("IA_RATING_MAX_DIFF", 300); |
|---|
| 30 | // number of seconds in a time period |
|---|
| 31 | define("IA_RATING_TIME_PERIOD", 2628000); |
|---|
| 32 | // number of months before you reach maximum unreliability |
|---|
| 33 | // 4 years now |
|---|
| 34 | define("IA_RATING_CHAOS", 48); |
|---|
| 35 | // don't ask. |
|---|
| 36 | // FIXME: But I do! What do these mean? |
|---|
| 37 | define("IA_RATING_C", sqrt((IA_RATING_MAX_DEVIATION * IA_RATING_MAX_DEVIATION - IA_RATING_MED_DEVIATION * IA_RATING_MED_DEVIATION) / IA_RATING_CHAOS)); |
|---|
| 38 | define("IA_RATING_Q", log(10.0) / 400.0); |
|---|
| 39 | // we're feeding games into chunks and periodically updating ratings |
|---|
| 40 | define("IA_RATING_MAX_CHUNK", 10); |
|---|
| 41 | // tweak rating increases to avoid unusual behavior for huge contents |
|---|
| 42 | define("IA_RATING_TWEAK_PERIOD", 3); |
|---|
| 43 | define("IA_MIN_SCALED_RATING", 0); |
|---|
| 44 | define("IA_MAX_SCALED_RATING", 1500); |
|---|
| 45 | |
|---|
| 46 | // number square |
|---|
| 47 | function sqr($number) { |
|---|
| 48 | return $number * $number; |
|---|
| 49 | } |
|---|
| 50 | |
|---|
| 51 | // init user array where we store and update ratings |
|---|
| 52 | // $whole_user_list array format is (list of usernames): |
|---|
| 53 | // array( |
|---|
| 54 | // username, |
|---|
| 55 | // ... |
|---|
| 56 | // ); |
|---|
| 57 | // $ratings array is as returned by rating_last_scores() |
|---|
| 58 | // Output array format is: |
|---|
| 59 | // array( |
|---|
| 60 | // username => array( |
|---|
| 61 | // rating => (int), |
|---|
| 62 | // deviation => (int), |
|---|
| 63 | // timestamp => (int) |
|---|
| 64 | // ) |
|---|
| 65 | // ... |
|---|
| 66 | // ); |
|---|
| 67 | function rating_init($whole_user_list, $last_scores) { |
|---|
| 68 | $users = $last_scores; |
|---|
| 69 | foreach ($whole_user_list as $username) { |
|---|
| 70 | if (isset($users[$username])) { |
|---|
| 71 | continue; |
|---|
| 72 | } |
|---|
| 73 | |
|---|
| 74 | $user = array( |
|---|
| 75 | 'rating' => IA_RATING_INITIAL, |
|---|
| 76 | 'deviation' => IA_RATING_DEVIATION, |
|---|
| 77 | 'timestamp' => 0, |
|---|
| 78 | ); |
|---|
| 79 | $users[$username] = $user; |
|---|
| 80 | } |
|---|
| 81 | return $users; |
|---|
| 82 | } |
|---|
| 83 | |
|---|
| 84 | // update user deviation. consider now is $timestamp |
|---|
| 85 | // deviations must be update before updating ratings |
|---|
| 86 | // |
|---|
| 87 | // $timestamp is UNIX timestamp (seconds) |
|---|
| 88 | function rating_update_deviation(&$users, $username, $timestamp) { |
|---|
| 89 | log_assert(isset($users[$username])); |
|---|
| 90 | |
|---|
| 91 | $user =& $users[$username]; |
|---|
| 92 | $old_deviation = $user['deviation']; |
|---|
| 93 | if (!$user['timestamp']) { |
|---|
| 94 | // user never was rated, leave deviation as it is |
|---|
| 95 | log_assert(IA_RATING_DEVIATION == $user['deviation']); |
|---|
| 96 | $elapsed = 0; |
|---|
| 97 | } |
|---|
| 98 | else { |
|---|
| 99 | // compute elapsed time from the last rating to current timestamp |
|---|
| 100 | $elapsed = (int)ceil(($timestamp - $user['timestamp']) / IA_RATING_TIME_PERIOD); |
|---|
| 101 | } |
|---|
| 102 | $user['deviation'] = min(IA_RATING_MAX_DEVIATION, |
|---|
| 103 | sqrt(sqr($old_deviation) + sqr(IA_RATING_C) * $elapsed)); |
|---|
| 104 | $user['timestamp'] = $timestamp; |
|---|
| 105 | } |
|---|
| 106 | |
|---|
| 107 | // FIXME: Throw in some comments |
|---|
| 108 | function rating_gspot($deviation) { |
|---|
| 109 | $deviation = 1.0 / sqrt(1.0 + 3.0 * sqr(IA_RATING_Q) * sqr($deviation) / sqr(M_PI)); |
|---|
| 110 | return $deviation; |
|---|
| 111 | } |
|---|
| 112 | |
|---|
| 113 | // Expected score for a pair of contestants |
|---|
| 114 | function rating_expected_score($rating1, $rating2, $deviation2) { |
|---|
| 115 | $diff = max(-IA_RATING_MAX_DIFF, min(IA_RATING_MAX_DIFF, $rating1 - $rating2)); |
|---|
| 116 | $score = 1.0 / (1.0 + pow(10, -rating_gspot($deviation2) * $diff / 400.0)); |
|---|
| 117 | return $score; |
|---|
| 118 | } |
|---|
| 119 | |
|---|
| 120 | // FIXME: Throw in some comments |
|---|
| 121 | function rating_score($score1, $score2, $variance) { |
|---|
| 122 | if ($score1 > $score2) { |
|---|
| 123 | if (!$score2) { |
|---|
| 124 | return 1.0; |
|---|
| 125 | } |
|---|
| 126 | return 0.5 + min(0.5, ($score1 - $score2) / $variance * 0.25); |
|---|
| 127 | } |
|---|
| 128 | |
|---|
| 129 | if ($score1 < $score2) { |
|---|
| 130 | if (!$score1) { |
|---|
| 131 | return 0.0; |
|---|
| 132 | } |
|---|
| 133 | return 0.5 - min(0.5, ($score2 - $score1) / $variance * 0.25); |
|---|
| 134 | } |
|---|
| 135 | return 0.5; |
|---|
| 136 | } |
|---|
| 137 | |
|---|
| 138 | // Update ratings considering user_scores. Assume now is $timestamp. |
|---|
| 139 | // $timestamp is UNIX timestamp |
|---|
| 140 | // FIXME: Throw in some comments |
|---|
| 141 | // |
|---|
| 142 | // $user_scores array format is: |
|---|
| 143 | // array( |
|---|
| 144 | // username => score, |
|---|
| 145 | // ... |
|---|
| 146 | // ) |
|---|
| 147 | function rating_update(&$users, $user_scores, $timestamp) { |
|---|
| 148 | if (count($user_scores) < 1) { |
|---|
| 149 | log_warn("Need more than 1 contestant to update ratings! Nothing to do!"); |
|---|
| 150 | return; |
|---|
| 151 | } |
|---|
| 152 | log_print("Updating ratings for ".count($user_scores)." users"); |
|---|
| 153 | |
|---|
| 154 | $new_rating = array(); |
|---|
| 155 | $new_deviation = array(); |
|---|
| 156 | $user_count = count($user_scores); |
|---|
| 157 | |
|---|
| 158 | // update deviations and compute score mean |
|---|
| 159 | $score_mean = 0; |
|---|
| 160 | foreach ($user_scores as $username => $score) { |
|---|
| 161 | rating_update_deviation($users, $username, $timestamp); |
|---|
| 162 | $score_mean += $score; |
|---|
| 163 | } |
|---|
| 164 | $score_mean /= $user_count; |
|---|
| 165 | |
|---|
| 166 | // compute score variance |
|---|
| 167 | $score_variance = 0; |
|---|
| 168 | foreach ($user_scores as $username => $score) { |
|---|
| 169 | $score_variance += sqr($score - $score_mean); |
|---|
| 170 | } |
|---|
| 171 | $score_variance = sqrt($score_variance / $user_count); |
|---|
| 172 | |
|---|
| 173 | // Voodoo Magic |
|---|
| 174 | // FIXME: Throw in some comments if you understand anything about this |
|---|
| 175 | $chunk_count = ceil(($user_count-1) / IA_RATING_MAX_CHUNK); |
|---|
| 176 | foreach ($user_scores as $username => $score) { |
|---|
| 177 | log_assert(isset($users[$username])); |
|---|
| 178 | $user =& $users[$username]; |
|---|
| 179 | |
|---|
| 180 | log_assert(isset($user['rating']) && isset($user['deviation']) |
|---|
| 181 | && isset($user['timestamp'])); |
|---|
| 182 | |
|---|
| 183 | $rating1 = $user['rating']; |
|---|
| 184 | $deviation1 = $user['deviation']; |
|---|
| 185 | $pos = 0; |
|---|
| 186 | $keys = array_keys($user_scores); |
|---|
| 187 | $tweak = $chunk_count / IA_RATING_TWEAK_PERIOD; |
|---|
| 188 | |
|---|
| 189 | for ($chunk = 0; $chunk < $chunk_count; $chunk++, $pos += IA_RATING_MAX_CHUNK) { |
|---|
| 190 | $D = 0.0; |
|---|
| 191 | for ($i = $pos; $i < $pos+IA_RATING_MAX_CHUNK && $i < $user_count; $i++) { |
|---|
| 192 | $username2 = $keys[$i]; |
|---|
| 193 | $score2 = $user_scores[$username2]; |
|---|
| 194 | log_assert(isset($users[$username2])); |
|---|
| 195 | $user2 = $users[$username2]; |
|---|
| 196 | if ($username2 == $username) { |
|---|
| 197 | continue; |
|---|
| 198 | } |
|---|
| 199 | $rating2 = $user2['rating']; |
|---|
| 200 | $deviation2 = $user2['deviation']; |
|---|
| 201 | |
|---|
| 202 | $temp = rating_expected_score($rating1, $rating2, $deviation2); |
|---|
| 203 | $D = $D + sqr(rating_gspot($deviation2)) * $temp * (1.0 - $temp); |
|---|
| 204 | } |
|---|
| 205 | $D = $D * sqr(IA_RATING_Q); |
|---|
| 206 | |
|---|
| 207 | $R = 0.0; |
|---|
| 208 | for ($i = $pos; $i < $pos+IA_RATING_MAX_CHUNK && $i < $user_count; $i++) { |
|---|
| 209 | $username2 = $keys[$i]; |
|---|
| 210 | $score2 = $user_scores[$username2]; |
|---|
| 211 | log_assert(isset($users[$username2])); |
|---|
| 212 | $user2 = $users[$username2]; |
|---|
| 213 | if ($username2 == $username) { |
|---|
| 214 | continue; |
|---|
| 215 | } |
|---|
| 216 | $rating2 = $user2['rating']; |
|---|
| 217 | $deviation2 = $user2['deviation']; |
|---|
| 218 | |
|---|
| 219 | $temp = rating_expected_score($rating1, $rating2, $deviation2); |
|---|
| 220 | $weight = max(IA_RATING_Q * rating_gspot($deviation2) * sqr(IA_RATING_MIN_DEVIATION), |
|---|
| 221 | IA_RATING_Q * rating_gspot($deviation2) / (1.0 / sqr($deviation1) + $D)); |
|---|
| 222 | $R = $R + $weight * (rating_score($score, $score2, $score_variance) - $temp); |
|---|
| 223 | } |
|---|
| 224 | |
|---|
| 225 | $rating1 = max(0, $rating1 + $R / $tweak); |
|---|
| 226 | $deviation1 = max(IA_RATING_MIN_DEVIATION, |
|---|
| 227 | sqrt(1.0 / (1.0/sqr($deviation1)+$D))); |
|---|
| 228 | } |
|---|
| 229 | |
|---|
| 230 | // update rating and deviation |
|---|
| 231 | $user['rating'] = $rating1; |
|---|
| 232 | $user['deviation'] = $deviation1; |
|---|
| 233 | } |
|---|
| 234 | } |
|---|
| 235 | |
|---|
| 236 | // Represent rating in a human-friendly scale from 0 to 1000 |
|---|
| 237 | // NOTE: This is used only when displaying ratings to users. |
|---|
| 238 | // NOTE: Currently used by www/format/* and scripts/send-newsletter. |
|---|
| 239 | function rating_scale($absolute_rating) { |
|---|
| 240 | return min(max(IA_MIN_SCALED_RATING, round($absolute_rating / 3.0)), |
|---|
| 241 | IA_MAX_SCALED_RATING); |
|---|
| 242 | } |
|---|
| 243 | |
|---|
| 244 | ?> |
|---|