- // ==UserScript==
- // @name AtCoderStandingsAnalysis
- // @name:en AtCoderStandingsAnalysis
- // @namespace https://github.com/RTnF/AtCoderStandingsAnalysis
- // @version 0.2.0
- // @description 順位表のjsonを集計し、上部にテーブルを追加します。
- // @description:en It aggregates AtCoder standings/json and adds a table about the summary of it.
- // @author RTnF
- // @match https://atcoder.jp/*standings*
- // @exclude https://atcoder.jp/*standings/json
- // @grant none
- // @license MIT
- // ==/UserScript==
-
- $(function () {
- "use strict";
-
- // XorShift https://sbfl.net/blog/2017/06/01/javascript-reproducible-random/
- class Random {
- constructor(seed = 88675123) {
- this.x = 123456789;
- this.y = 362436069;
- this.z = 521288629;
- this.w = seed;
- }
- next() {
- let t;
- t = this.x ^ (this.x << 11);
- this.x = this.y; this.y = this.z; this.z = this.w;
- return this.w = (this.w ^ (this.w >>> 19)) ^ (t ^ (t >>> 8));
- }
- // 閉区間
- nextInt(min, max) {
- const r = Math.abs(this.next());
- return min + (r % (max + 1 - min));
- }
- }
- const seed = 20200531;
- const random = new Random(seed);
-
- // シャッフル
- function shuffle(arr) {
- let arr2 = arr.slice();
- for (let i = arr2.length - 1; i > 0; i--) {
- let j = random.nextInt(0, i);
- [arr2[i], arr2[j]] = [arr2[j], arr2[i]];
- }
- return arr2;
- }
-
- // http://yucatio.hatenablog.com/entry/2020/02/06/085930
- // ([1, 2], [3, 4]) -> [[1, 3], [2, 4]]
- function zip(...arrays) {
- const length = Math.min(...(arrays.map(arr => arr.length)));
- return new Array(length).fill().map((_, i) => arrays.map(arr => arr[i]));
- }
-
- // https://github.com/kenkoooo/AtCoderProblems/blob/56a860e53eae2cfcb422a08a0f05a9fe1299a20e/lambda-functions/time-estimator/function.py
- // safe*は、極端な値を避ける
- function safeLog(x) {
- return Math.log(Math.max(x, 10. ** -50));
- }
-
- function safeSigmoid(x) {
- return 1. / (1. + Math.exp(Math.min(-x, 300)));
- }
-
- // 2パラメータIRT
- // TODO: AGC-Aのための3パラメータIRT
- function fit2ParametersIRT(xs, ys) {
- let iter_n = Math.max(Math.floor(100000 / xs.length), 1);
-
- let eta = 1.;
- let x_scale = 1000.;
-
- let scxs = xs.map(x => x / x_scale);
- let samples = zip(scxs, ys);
-
- let a = 0.;
- let b = 0.;
- let r_a = 1.;
- let r_b = 1.;
- let iterations = [];
- for (let iter = 0; iter < iter_n; iter++) {
- let logl = 0.;
- for (let i = 0; i < samples.length; i++) {
- let p = safeSigmoid(a * samples[i][0] + b);
- logl += safeLog(samples[i][1] === 1 ? p : (1 - p));
- }
- iterations.push([logl, a, b]);
- samples = shuffle(samples);
- for (let i = 0; i < samples.length; i++) {
- let p = safeSigmoid(a * samples[i][0] + b);
- let grad_a = samples[i][0] * (samples[i][1] - p);
- let grad_b = (samples[i][1] - p);
- r_a += grad_a ** 2;
- r_b += grad_b ** 2;
- a += eta * grad_a / (r_a ** 0.5);
- b += eta * grad_b / (r_b ** 0.5);
- }
- }
- let best_logl = -(10 ** 20);
- for (let iter = 0; iter < iter_n; iter++) {
- if (best_logl < iterations[iter][0]) {
- best_logl = iterations[iter][0];
- a = iterations[iter][1];
- b = iterations[iter][2];
- }
- }
-
- a /= x_scale;
- return -b / a;
- }
-
- // ソート済み配列のうちval未満が何個あるか求める
- function countLower(arr, val) {
- let lo = -1;
- let hi = arr.length;
- while (hi - lo > 1) {
- let mid = Math.floor((hi + lo) / 2);
- if (arr[mid] < val) {
- lo = mid;
- } else {
- hi = mid;
- }
- }
- return hi;
- }
-
- // 換算: Rating -> innerRating
- function innerRating(rate, comp) {
- let ret = rate;
- if (rate <= 0) {
- throw "rate <= 0";
- }
- if (ret <= 400) {
- ret = 400. * (1 - Math.log(400. / rate));
- }
- ret += 1200. * (Math.sqrt(1 - (0.81 ** comp)) / (1 - (0.9 ** comp)) - 1) / (Math.sqrt(19) - 1);
- return ret;
- }
-
- // 換算: Positivise
- function toPositiveRating(rate) {
- if (rate <= 400) {
- return 400. / Math.exp((400. - rate) / 400.);
- }
- return rate;
- }
-
- const colors = ["#808080", "#804000", "#008000", "#00C0C0", "#0000FF", "#C0C000", "#FF8000", "#FF0000"];
- const threshold = [-10000, 400, 800, 1200, 1600, 2000, 2400, 2800];
- const canvas_width = 250;
- const canvas_height = 25;
-
- // 列項目
- const items = {
- id: 0,
- score: 1,
- counts: 2,
- ac_rate: 3,
- ave_pen: 4,
- pen_rate: 5,
- diff: 6,
- inner_rate: 7
- };
-
- // 表を先頭に追加
- $("#vue-standings").prepend(`
- <div>
- <table id="acsa-table" class="table table-bordered table-hover th-center td-center td-middle">
- <thead>
- </thead>
- <tbody>
- </tbody>
- </table>
- </div>
- `);
-
- // 表の更新
- vueStandings.$watch("standings", function (new_val, old_val) {
- if (!new_val) {
- return;
- }
- let data;
- let task = new_val.TaskInfo;
- if (vueStandings.filtered) {
- data = vueStandings.filteredStandings;
- } else {
- data = new_val.StandingsData;
- }
-
- $("#acsa-table > tbody").empty();
- $("#acsa-table > tbody").append(`
- <tr style="font-weight: bold;">
- <td>問題</td>
- <td>得点</td>
- <td>人数</td>
- <td>正解率</td>
- <td>平均ペナ</td>
- <td>ペナ率</td>
- <td>Diff</td>
- <td>内部レート</td>
- </tr>
- `);
- for (let i = 0; i < task.length; i++) {
- let is_tried = vueStandings.tries[i] > 0;
- $("#acsa-table > tbody").append(`
- <tr>
- <td style="padding: 4px;">` + task[i].Assignment + `</td>
- <td style="padding: 4px;">-</td>
- <td style="padding: 4px;">` + vueStandings.ac[i] + ` / ` + vueStandings.tries[i] + `</td>
- <td style="padding: 4px;">` + (is_tried ? (vueStandings.ac[i] / vueStandings.tries[i] * 100).toFixed(2) + "%" : "-") + `</td>
- <td style="padding: 4px;">-</td>
- <td style="padding: 4px;">-</td>
- <td style="padding: 4px;">-</td>
- <td style="padding: 4px; width: ` + canvas_width + `px;"><canvas style="vertical-align: middle;" width="` + canvas_width + `px" height="` + canvas_height + `px"></canvas></td>
- </tr>
- `);
- if (!is_tried) {
- continue;
- }
-
- // トップの得点を満点とみなす
- let max_score = -1;
- let my_score = -1;
- // 不正解数 / 提出者数
- let average_penalty = 0;
- // ペナルティ >= 1 の人数 / 提出者数
- let ratio_penalty = 0;
- let rates_ac = [];
- // レートと正解かどうかを別途配列にする
- let rates_all = [];
- let rates_isac = [];
- for (let j = 0; j < data.length; j++) {
- // 参加登録していない
- if (!data[j].TaskResults) {
- continue;
- }
- // アカウント削除
- if (data[j].UserIsDeleted) {
- continue;
- }
- let result = data[j].TaskResults[task[i].TaskScreenName];
- // 未提出のときresult === undefined
- if (result) {
- if (data[j].UserScreenName === vueStandings.userScreenName) {
- my_score = result.Score;
- }
- // 赤い括弧内の数字
- let penalty = result.Score === 0 ? result.Failure : result.Penalty;
- average_penalty += penalty;
- if (penalty > 0) {
- ratio_penalty++;
- }
- if (max_score < result.Score) {
- max_score = result.Score;
- }
- }
- }
- // 正解者の内部レート配列を作成する
- // 初出場はカウントしない
- if (max_score > 0) {
- for (let j = 0; j < data.length; j++) {
- let inner_rating = innerRating(Math.max(data[j].Rating, 1), data[j].Competitions);
- if (data[j].Competitions > 0
- && data[j].TaskResults[task[i].TaskScreenName]
- && data[j].TaskResults[task[i].TaskScreenName].Score === max_score) {
- rates_ac.push(inner_rating);
- }
- // 提出がある
- if (data[j].Competitions > 0) {
- for (let k = 0; k < task.length; k++) {
- if (data[j].TaskResults[task[k].TaskScreenName]) {
- rates_all.push(inner_rating);
- rates_isac.push((data[j].TaskResults[task[i].TaskScreenName]
- && data[j].TaskResults[task[i].TaskScreenName].Score === max_score) ? 1 : 0);
- break;
- }
- }
- }
- }
- rates_ac.sort(function (a, b) { return a - b; });
- }
-
- // jsonはもともと100倍されている
- my_score /= 100;
- max_score /= 100;
- average_penalty /= vueStandings.tries[i];
- ratio_penalty /= vueStandings.tries[i];
- // 百分率
- ratio_penalty *= 100;
-
- // https://github.com/kenkoooo/AtCoderProblems/blob/56a860e53eae2cfcb422a08a0f05a9fe1299a20e/lambda-functions/time-estimator/function.py
- // コンテスト中は終了時点より高いDifficultyになる
- $("#acsa-table > tbody > tr:eq(" + (i + 1) + ") > td:eq(" + items.score + ")").text(my_score >= 0 ? my_score.toFixed() : "-");
- $("#acsa-table > tbody > tr:eq(" + (i + 1) + ") > td:eq(" + items.ave_pen + ")").text(average_penalty.toFixed(2));
- $("#acsa-table > tbody > tr:eq(" + (i + 1) + ") > td:eq(" + items.pen_rate + ")").text(ratio_penalty.toFixed(2) + "%");
- if (max_score > 0) {
- const diff = Math.floor(toPositiveRating(fit2ParametersIRT(rates_all, rates_isac)));
- if (diff > 20000) {
- $("#acsa-table > tbody > tr:eq(" + (i + 1) + ") > td:eq(" + items.diff + ")").text("-");
- } else {
- $("#acsa-table > tbody > tr:eq(" + (i + 1) + ") > td:eq(" + items.diff + ")").text(diff);
- }
- let canvas = $("#acsa-table > tbody > tr:eq(" + (i + 1) + ") > td:eq(" + items.inner_rate + ") > canvas")[0];
- if (canvas.getContext) {
- let context = canvas.getContext("2d");
- for (let k = 0; k < 8; k++) {
- context.fillStyle = colors[k];
- // 色の境界から右端までの矩形描画
- let x = Math.round(countLower(rates_ac, threshold[k]) / rates_ac.length * canvas_width);
- context.fillRect(x, 0, canvas_width - x, canvas_height);
- }
- }
- }
- }
- }, { deep: true, immediate: true });
- });