Greasy Fork is available in English.

AtCoderStandingsAnalysis

順位表のjsonを集計し、上部にテーブルを追加します。

ของเมื่อวันที่ 05-07-2020 ดู เวอร์ชันล่าสุด

  1. // ==UserScript==
  2. // @name AtCoderStandingsAnalysis
  3. // @name:en AtCoderStandingsAnalysis
  4. // @namespace https://github.com/RTnF/AtCoderStandingsAnalysis
  5. // @version 0.2.0
  6. // @description 順位表のjsonを集計し、上部にテーブルを追加します。
  7. // @description:en It aggregates AtCoder standings/json and adds a table about the summary of it.
  8. // @author RTnF
  9. // @match https://atcoder.jp/*standings*
  10. // @exclude https://atcoder.jp/*standings/json
  11. // @grant none
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. $(function () {
  16. "use strict";
  17.  
  18. // XorShift https://sbfl.net/blog/2017/06/01/javascript-reproducible-random/
  19. class Random {
  20. constructor(seed = 88675123) {
  21. this.x = 123456789;
  22. this.y = 362436069;
  23. this.z = 521288629;
  24. this.w = seed;
  25. }
  26. next() {
  27. let t;
  28. t = this.x ^ (this.x << 11);
  29. this.x = this.y; this.y = this.z; this.z = this.w;
  30. return this.w = (this.w ^ (this.w >>> 19)) ^ (t ^ (t >>> 8));
  31. }
  32. // 閉区間
  33. nextInt(min, max) {
  34. const r = Math.abs(this.next());
  35. return min + (r % (max + 1 - min));
  36. }
  37. }
  38. const seed = 20200531;
  39. const random = new Random(seed);
  40.  
  41. // シャッフル
  42. function shuffle(arr) {
  43. let arr2 = arr.slice();
  44. for (let i = arr2.length - 1; i > 0; i--) {
  45. let j = random.nextInt(0, i);
  46. [arr2[i], arr2[j]] = [arr2[j], arr2[i]];
  47. }
  48. return arr2;
  49. }
  50.  
  51. // http://yucatio.hatenablog.com/entry/2020/02/06/085930
  52. // ([1, 2], [3, 4]) -> [[1, 3], [2, 4]]
  53. function zip(...arrays) {
  54. const length = Math.min(...(arrays.map(arr => arr.length)));
  55. return new Array(length).fill().map((_, i) => arrays.map(arr => arr[i]));
  56. }
  57.  
  58. // https://github.com/kenkoooo/AtCoderProblems/blob/56a860e53eae2cfcb422a08a0f05a9fe1299a20e/lambda-functions/time-estimator/function.py
  59. // safe*は、極端な値を避ける
  60. function safeLog(x) {
  61. return Math.log(Math.max(x, 10. ** -50));
  62. }
  63.  
  64. function safeSigmoid(x) {
  65. return 1. / (1. + Math.exp(Math.min(-x, 300)));
  66. }
  67.  
  68. // 2パラメータIRT
  69. // TODO: AGC-Aのための3パラメータIRT
  70. function fit2ParametersIRT(xs, ys) {
  71. let iter_n = Math.max(Math.floor(100000 / xs.length), 1);
  72.  
  73. let eta = 1.;
  74. let x_scale = 1000.;
  75.  
  76. let scxs = xs.map(x => x / x_scale);
  77. let samples = zip(scxs, ys);
  78.  
  79. let a = 0.;
  80. let b = 0.;
  81. let r_a = 1.;
  82. let r_b = 1.;
  83. let iterations = [];
  84. for (let iter = 0; iter < iter_n; iter++) {
  85. let logl = 0.;
  86. for (let i = 0; i < samples.length; i++) {
  87. let p = safeSigmoid(a * samples[i][0] + b);
  88. logl += safeLog(samples[i][1] === 1 ? p : (1 - p));
  89. }
  90. iterations.push([logl, a, b]);
  91. samples = shuffle(samples);
  92. for (let i = 0; i < samples.length; i++) {
  93. let p = safeSigmoid(a * samples[i][0] + b);
  94. let grad_a = samples[i][0] * (samples[i][1] - p);
  95. let grad_b = (samples[i][1] - p);
  96. r_a += grad_a ** 2;
  97. r_b += grad_b ** 2;
  98. a += eta * grad_a / (r_a ** 0.5);
  99. b += eta * grad_b / (r_b ** 0.5);
  100. }
  101. }
  102. let best_logl = -(10 ** 20);
  103. for (let iter = 0; iter < iter_n; iter++) {
  104. if (best_logl < iterations[iter][0]) {
  105. best_logl = iterations[iter][0];
  106. a = iterations[iter][1];
  107. b = iterations[iter][2];
  108. }
  109. }
  110.  
  111. a /= x_scale;
  112. return -b / a;
  113. }
  114.  
  115. // ソート済み配列のうちval未満が何個あるか求める
  116. function countLower(arr, val) {
  117. let lo = -1;
  118. let hi = arr.length;
  119. while (hi - lo > 1) {
  120. let mid = Math.floor((hi + lo) / 2);
  121. if (arr[mid] < val) {
  122. lo = mid;
  123. } else {
  124. hi = mid;
  125. }
  126. }
  127. return hi;
  128. }
  129.  
  130. // 換算: Rating -> innerRating
  131. function innerRating(rate, comp) {
  132. let ret = rate;
  133. if (rate <= 0) {
  134. throw "rate <= 0";
  135. }
  136. if (ret <= 400) {
  137. ret = 400. * (1 - Math.log(400. / rate));
  138. }
  139. ret += 1200. * (Math.sqrt(1 - (0.81 ** comp)) / (1 - (0.9 ** comp)) - 1) / (Math.sqrt(19) - 1);
  140. return ret;
  141. }
  142.  
  143. // 換算: Positivise
  144. function toPositiveRating(rate) {
  145. if (rate <= 400) {
  146. return 400. / Math.exp((400. - rate) / 400.);
  147. }
  148. return rate;
  149. }
  150.  
  151. const colors = ["#808080", "#804000", "#008000", "#00C0C0", "#0000FF", "#C0C000", "#FF8000", "#FF0000"];
  152. const threshold = [-10000, 400, 800, 1200, 1600, 2000, 2400, 2800];
  153. const canvas_width = 250;
  154. const canvas_height = 25;
  155.  
  156. // 列項目
  157. const items = {
  158. id: 0,
  159. score: 1,
  160. counts: 2,
  161. ac_rate: 3,
  162. ave_pen: 4,
  163. pen_rate: 5,
  164. diff: 6,
  165. inner_rate: 7
  166. };
  167.  
  168. // 表を先頭に追加
  169. $("#vue-standings").prepend(`
  170. <div>
  171. <table id="acsa-table" class="table table-bordered table-hover th-center td-center td-middle">
  172. <thead>
  173. </thead>
  174. <tbody>
  175. </tbody>
  176. </table>
  177. </div>
  178. `);
  179.  
  180. // 表の更新
  181. vueStandings.$watch("standings", function (new_val, old_val) {
  182. if (!new_val) {
  183. return;
  184. }
  185. let data;
  186. let task = new_val.TaskInfo;
  187. if (vueStandings.filtered) {
  188. data = vueStandings.filteredStandings;
  189. } else {
  190. data = new_val.StandingsData;
  191. }
  192.  
  193. $("#acsa-table > tbody").empty();
  194. $("#acsa-table > tbody").append(`
  195. <tr style="font-weight: bold;">
  196. <td>問題</td>
  197. <td>得点</td>
  198. <td>人数</td>
  199. <td>正解率</td>
  200. <td>平均ペナ</td>
  201. <td>ペナ率</td>
  202. <td>Diff</td>
  203. <td>内部レート</td>
  204. </tr>
  205. `);
  206. for (let i = 0; i < task.length; i++) {
  207. let is_tried = vueStandings.tries[i] > 0;
  208. $("#acsa-table > tbody").append(`
  209. <tr>
  210. <td style="padding: 4px;">` + task[i].Assignment + `</td>
  211. <td style="padding: 4px;">-</td>
  212. <td style="padding: 4px;">` + vueStandings.ac[i] + ` / ` + vueStandings.tries[i] + `</td>
  213. <td style="padding: 4px;">` + (is_tried ? (vueStandings.ac[i] / vueStandings.tries[i] * 100).toFixed(2) + "%" : "-") + `</td>
  214. <td style="padding: 4px;">-</td>
  215. <td style="padding: 4px;">-</td>
  216. <td style="padding: 4px;">-</td>
  217. <td style="padding: 4px; width: ` + canvas_width + `px;"><canvas style="vertical-align: middle;" width="` + canvas_width + `px" height="` + canvas_height + `px"></canvas></td>
  218. </tr>
  219. `);
  220. if (!is_tried) {
  221. continue;
  222. }
  223.  
  224. // トップの得点を満点とみなす
  225. let max_score = -1;
  226. let my_score = -1;
  227. // 不正解数 / 提出者数
  228. let average_penalty = 0;
  229. // ペナルティ >= 1 の人数 / 提出者数
  230. let ratio_penalty = 0;
  231. let rates_ac = [];
  232. // レートと正解かどうかを別途配列にする
  233. let rates_all = [];
  234. let rates_isac = [];
  235. for (let j = 0; j < data.length; j++) {
  236. // 参加登録していない
  237. if (!data[j].TaskResults) {
  238. continue;
  239. }
  240. // アカウント削除
  241. if (data[j].UserIsDeleted) {
  242. continue;
  243. }
  244. let result = data[j].TaskResults[task[i].TaskScreenName];
  245. // 未提出のときresult === undefined
  246. if (result) {
  247. if (data[j].UserScreenName === vueStandings.userScreenName) {
  248. my_score = result.Score;
  249. }
  250. // 赤い括弧内の数字
  251. let penalty = result.Score === 0 ? result.Failure : result.Penalty;
  252. average_penalty += penalty;
  253. if (penalty > 0) {
  254. ratio_penalty++;
  255. }
  256. if (max_score < result.Score) {
  257. max_score = result.Score;
  258. }
  259. }
  260. }
  261. // 正解者の内部レート配列を作成する
  262. // 初出場はカウントしない
  263. if (max_score > 0) {
  264. for (let j = 0; j < data.length; j++) {
  265. let inner_rating = innerRating(Math.max(data[j].Rating, 1), data[j].Competitions);
  266. if (data[j].Competitions > 0
  267. && data[j].TaskResults[task[i].TaskScreenName]
  268. && data[j].TaskResults[task[i].TaskScreenName].Score === max_score) {
  269. rates_ac.push(inner_rating);
  270. }
  271. // 提出がある
  272. if (data[j].Competitions > 0) {
  273. for (let k = 0; k < task.length; k++) {
  274. if (data[j].TaskResults[task[k].TaskScreenName]) {
  275. rates_all.push(inner_rating);
  276. rates_isac.push((data[j].TaskResults[task[i].TaskScreenName]
  277. && data[j].TaskResults[task[i].TaskScreenName].Score === max_score) ? 1 : 0);
  278. break;
  279. }
  280. }
  281. }
  282. }
  283. rates_ac.sort(function (a, b) { return a - b; });
  284. }
  285.  
  286. // jsonはもともと100倍されている
  287. my_score /= 100;
  288. max_score /= 100;
  289. average_penalty /= vueStandings.tries[i];
  290. ratio_penalty /= vueStandings.tries[i];
  291. // 百分率
  292. ratio_penalty *= 100;
  293.  
  294. // https://github.com/kenkoooo/AtCoderProblems/blob/56a860e53eae2cfcb422a08a0f05a9fe1299a20e/lambda-functions/time-estimator/function.py
  295. // コンテスト中は終了時点より高いDifficultyになる
  296. $("#acsa-table > tbody > tr:eq(" + (i + 1) + ") > td:eq(" + items.score + ")").text(my_score >= 0 ? my_score.toFixed() : "-");
  297. $("#acsa-table > tbody > tr:eq(" + (i + 1) + ") > td:eq(" + items.ave_pen + ")").text(average_penalty.toFixed(2));
  298. $("#acsa-table > tbody > tr:eq(" + (i + 1) + ") > td:eq(" + items.pen_rate + ")").text(ratio_penalty.toFixed(2) + "%");
  299. if (max_score > 0) {
  300. const diff = Math.floor(toPositiveRating(fit2ParametersIRT(rates_all, rates_isac)));
  301. if (diff > 20000) {
  302. $("#acsa-table > tbody > tr:eq(" + (i + 1) + ") > td:eq(" + items.diff + ")").text("-");
  303. } else {
  304. $("#acsa-table > tbody > tr:eq(" + (i + 1) + ") > td:eq(" + items.diff + ")").text(diff);
  305. }
  306. let canvas = $("#acsa-table > tbody > tr:eq(" + (i + 1) + ") > td:eq(" + items.inner_rate + ") > canvas")[0];
  307. if (canvas.getContext) {
  308. let context = canvas.getContext("2d");
  309. for (let k = 0; k < 8; k++) {
  310. context.fillStyle = colors[k];
  311. // 色の境界から右端までの矩形描画
  312. let x = Math.round(countLower(rates_ac, threshold[k]) / rates_ac.length * canvas_width);
  313. context.fillRect(x, 0, canvas_width - x, canvas_height);
  314. }
  315. }
  316. }
  317. }
  318. }, { deep: true, immediate: true });
  319. });