AtCoderStandingsAnalysis

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

Versión del día 05/07/2020. Echa un vistazo a la versión más reciente.

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