AtCoderStandingsAnalysis

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

Ajankohdalta 5.7.2020. Katso uusin versio.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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 });
});