レーティング差推計

by mizarjp

HTML

<script src="//cdn.jsdelivr.net/npm/jstat@latest/dist/jstat.min.js"></script>
<h1>勝敗数→レーティング差推計</h1>
<p class="form">
勝数:<input type="number" id="win" min="0" max="999999999999" step="1" />,
引分:<input type="number" id="draw" min="0" max="999999999999" step="1" />,
敗数:<input type="number" id="lose" min="0" max="999999999999" step="1" />
<button id="calcbtn">recalc</button>
</p>
<p>
<select id="calcptn">
<option value="all">全種計算</option>
<option value="draw0">引分=無効</option>
<option value="draw1" selected>引分=0.5勝</option>
<option value="draw2">引分=0.5勝(端数無効)</option>
<option value="draw3">引分=0.5勝(端数切下げ/切上げ)</option>
</select>
<select id="relptn">
<option value="all" selected>全信頼度</option>
<option value="percent">パーセント点</option>
<option value="sigma">正規分布σ</option>
</select>
</p>
<pre id="preout"></pre>
<table><thead><th>信頼度</th><th colspan="5">信頼区間</th><th>区間幅</th><th>処理区分</th></thead><tbody id="tout"></tbody></table>
<ul>
<li>例: 信頼度「2.326σ: 98.0000000%」の場合、正規分布のグラフ上では 平均値±2.326σ の範囲に 98% が入ること、二項分布において信頼区間の範囲に収まる面積が 98% 、区間の下にぶれる可能性が 1% {=(100%-98%)/2} 、上にぶれる可能性が 1% である事を示しています。</li>
<li>「信頼区間」「区間幅」は、「区間下側のレーティング差(勝率)」~「区間上側のレーティング差(勝率)」、「区間上側のレーティング差-区間下側のレーティング差」をそれぞれ表記しています。</li>
<li>「引分」の信頼区間計算における扱い(例: Win-Draw-Lose: 1-3-1 の場合)<ul>
  <li>「引分=無効」: Win-Lose: 1-1 として計算</li>
  <li>「引分=0.5勝」: Win-Lose: 2.5-2.5 として計算</li>
  <li>「引分=0.5勝(端数無効)」: Win-Lose: 2-2 として計算(引分1を無効扱い)</li>
  <li>「引分=0.5勝(端数切下げ/切上げ)」: 下限は Win-Lose: 2-3 、上限は Win-Lose: 3-2 として計算</li>
</ul></li>
<li>入力される値によっては(特に小さすぎたり大きすぎたり、極端な値が入力される場合)、必ずしも表示される精度がそのまま有効な精度とは限りません。</li>
<li><a href="https://github.com/jstat/jstat">jStat</a> - JavaScript Statistical Library</li>
<li><a href="https://github.com/tibigame/HandicappedRook/tree/master/tool">tibigame/HandicappedRook/tool</a> - original code</li>
</ul>
<h2>レーティング差早見表</h2>
<table>
<thead><th>レート差</th><th>勝率</th><th>敗率</th></thead>
<tbody id="elorating"></tbody>
</table>

CSS

p.form {
  font-family: monospace;
}

input {
  text-align: right;
}

table {
  border-collapse: collapse;
}

th {
  color: #333;
  background-color: #ddd;
  border: 1px solid #b9b9b9;
}

td {
  padding: 2px;
  color: #333;
  background-color: #fff;
  border: 1px solid #b9b9b9;
  font-family: 'Inconsolata', 'Consolas', monospace;
}

td.right {
  text-align: right;
}

td.reliability {
  background-color: #ffffff;
}

td.draw0 {
  background-color: #ffffff;
}

td.draw1 {
  background-color: #f7f7f7;
}

td.draw2 {
  background-color: #eeeeee;
}

td.draw3 {
  background-color: #e6e6e6;
}

JavaScript

function elorating(winrate)
{
  if (winrate <= 0) return Number.NEGATIVE_INFINITY;
  if (winrate >= 1) return Number.POSITIVE_INFINITY;
  return 400 * Math.log10(winrate / (1 - winrate));
}
function elorating_inv(rdiff)
{
  return 1 / (1 + Math.exp(-Math.LN10 * 0.0025 * rdiff));
}
function clopper_pearson(k, n, alpha)
{
  var alpha2 = (1 - alpha) / 2;
  // 勝率・敗率をあわせて計算(0%,100%に近い勝率の情報落ち・桁落ち誤差を減らすため)
  // beta.inv(p, a, b) == 1 - beta.inv(1 - p, b, a)
  var lower_w = jStat.beta.inv(alpha2, k, n - k + 1); // 勝率
  var lower_l = jStat.beta.inv(1 - alpha2, n - k + 1, k); // 敗率
  var upper_w = jStat.beta.inv(1 - alpha2, k + 1, n - k); // 勝率
  var upper_l = jStat.beta.inv(alpha2, n - k, k + 1); // 敗率
  return {
    lower: Number.isNaN(lower_w) ? Number.isNaN(lower_l) ? 0 : 1 - lower_l : (lower_w <= 0.5 || Number.isNaN(lower_l)) ? lower_w : 1 - lower_l,
    upper: Number.isNaN(upper_w) ? Number.isNaN(upper_l) ? 1 : 1 - upper_l : (upper_w <= 0.5 || Number.isNaN(upper_l)) ? upper_w : 1 - upper_l,
    lower_win: lower_w,
    lower_lose: lower_l,
    upper_win: upper_w,
    upper_lose: upper_l,
    lowerrate: Number.isNaN(lower_w) ? Number.isNaN(lower_l) ? Number.NEGATIVE_INFINITY : -elForate(lower_l) : (lower_w <= 0.5 || Number.isNaN(lower_l)) ? elorating(lower_w) : -elorating(lower_l),
    upperrate: Number.isNaN(upper_w) ? Number.isNaN(upper_l) ? Number.POSITIVE_INFINITY : -elorating(upper_l) : (upper_w <= 0.5 || Number.isNaN(upper_l)) ? elorating(upper_w) : -elorating(upper_l),
    k: k,
    n: n,
    alpha: alpha
  };
}
function addtd(parent, text, classes, attr)
{
  if(!text) text = "";
  if(!classes) classes = "";
  if(!attr) attr = {};
  var elem = document.createElement("td");
  text.split("\n").forEach(function(t, i, a)
  {
    elem.appendChild(document.createTextNode(t));
    if(i + 1 < a.length)
      elem.appendChild(document.createElement("br"));
  });
  classes.split(" ").forEach(function(c){
    if(c !== "") elem.classList.add(c);
  });
 ...