BPI(Beat Power Indicator)とはBeatmania IIDXというゲームの腕前(SP)を数値化した指標です。
この数値は100点満点で☆12の各曲のスコアを元に算出されます。 100点が全一、0点が皆伝平均となるように計算式が作られています。
☆12のそれぞれの曲のスコアより求められるものを単曲BPIとし、 ☆12の曲全ての単曲BPIより算出された総合的なSPの腕前の指標を総合BPIと定義します。
考案時の意図として、総合BPIが50の人と30の人と10の人の間にある上手さの差を、 なるべく同じものとして比較できたらいいな、というコンセプトがありました。
BPIはかなり複雑な数式を用いて算出されます。以下に、旧ページの内容をベースに算出方法を残します。
BPIを算出するにあたって不可欠なPGFという関数について説明します。 PGFとは日本語に直すとピカグレ関数です。
これは、あるスコアレートを出すためには、何ノーツに1回黄グレを出していいのかを求める関数です。 PGFは以下のように定義されます。
PGF(x) = 1 + (x - 0.5) / (1 - x)
例として、スコアレート90%を出すためには何ノーツに1回黄グレを出してよいのかを求めるには、
x = 0.9 をPGFに代入します。
PGF(0.9) = 1 + (0.9 - 0.5) / (1 - 0.9) = 5
つまり、5ノーツに1回黄グレを出せば90%になると考えることができます。
2000ノーツの曲なら、90%は 2000 × 2 × 0.9 = 3600 です。
5ノーツに1回黄グレを出しながら2000ノーツ叩くと
PG:1600 / GR:400 で、スコアは 1600 × 2 + 400 = 3600 となり、
確かに90%になります。
スコアレートが100%の場合には、例外的にPGFはノーツ数の倍の値を返します。
補足として、PGFは50%以下のスコアレートには対応していません。
そのため、そこまで含めて扱いたいなら、nノーツに1回GOODを出す関数
f(x) = 1 / (1 - x) にしてもよいといえばよいです。
単曲BPIを求めるには以下の情報が必要です。
最初に、PGFに代入するために、s, k, z を m で割ることでスコアレートを求めます。
s' = s / mk' = k / mz' = z / m続いて、求めたそれぞれのスコアレートをPGFに代入します。
S = PGF(s')K = PGF(k')Z = PGF(z')スコアレートが100%の場合には、例外的にノーツ数の倍の値をPGFは返します。
ここで、SとZをKで割り、規格化します。 これにより、自分のスコアが皆伝平均に比べ何倍光っているか、 歴代が皆伝平均に比べ何倍光っているかを求めることが出来ます。
PGFで求められるのは「何ノーツに1回黄グレを出すか」であり、 20ノーツに1回黄グレを出す人は、10ノーツに1回黄グレを出す人より倍光らせることが出来る、 という考えの下でこの計算を行っています。
S' = S / KZ' = Z / K
ここで単純に 100 × S' / Z' を行うと、
皆伝平均が1点で歴代が100点となりますが、
歴代があまりに高いスコアレートであった場合に厳しすぎたり、
逆に歴代がそこまで高くない場合に簡単に高い点が出てしまったりして、
直感と合わない問題があります。
旧BPIではこれを抑制するために、 S' と Z' に自然対数 ln を取り、その値を 1.5 乗する という手法を採用していました。
こうすることで、全一のスコアレートの値によって大きく曲線の形が変わることを抑えつつ、 点数が高くなるにつれて1点の重みが増していくという事情を反映しようとしていました。
また、自然対数を用いることで、自分のスコアが皆伝平均であれば
ln(S'(=1)) = 0 となるため、皆伝平均が0点になります。
旧来の式は以下でした。負の値の1.5乗は実数域ではそのまま定義しづらいため、式を分けていました。
旧単曲BPI = 100 × ln(S')^1.5 / ln(Z')^1.5 (s ≥ k)旧単曲BPI = -100 × (-ln(S'))^1.5 / ln(Z')^1.5 (s < k)ただ、過去は単曲BPIの指数に1.5を採用していましたが、 考案時から比べて全1がここまで理論値に近づいてくるとは思っていませんでした。 今は 1.5 ではなく 1 のほうがよいのではないかと考えています。
つまり、今の感覚としては次のような式のほうが自然だと思っています。
単曲BPI = 100 × ln(S') / ln(Z') (s ≥ k)単曲BPI = -100 × (-ln(S')) / ln(Z') (s < k)加えて、 BPIManagerさん では、曲ごとの順位分布を求めたうえで、指数をそれにフィットさせるような形を採用していたりします。
例として、AA(SPA)の現在(2026/03)の条件で、係数を1.0にした場合を考えます。
このとき、BPIと必要スコアの関係はだいたい次のようになります。
このように、単曲BPIが高くなるにつれて1点上げるのに必要なスコアも小さくなります。 これは歴代スコア近傍において1点上げるということが非常に大変であることを示しています。
総合BPIは、全ての☆12の単曲BPIと以下の計算式で求められます。
総合BPIは単純な単曲BPIの平均ではありません。 他の曲が未プレイでも歴代を一つだけ持っている人を考えた際に、 平均を取ると著しく低い値となってしまいます。
ですが、歴代を持っているという人はそれだけで非常にうまいのではないかと考えることができます。 また、全員が☆12を全曲埋めているとは限りません。 単純平均では未プレイの曲があった場合にそれが0とカウントされ、 その人の本来の腕前とは離れた結果になってしまいます。
これらを改善するために、二乗平均平方根を応用した以下の計算式を使っていました。
まず、以下のように定義します。
総合BPI = ( Σ(BPI_a^k / n) )^(1 / k)
k = log₂(n) としたことで、
1曲だけ歴代を持っていて他の曲が未プレイの人の総合BPIが50になる
ようにしていました。
これで、☆12を全て埋めていなくとも、その人の実力をある程度表すことが可能になると考えていました。
単曲BPIが負であった場合には問題が生じるため、負であった場合には 単曲BPIの絶対値をk乗し、その値にマイナスを付ける という手法を取ることにしていました。
ただし、これだと大きく皆伝平均を下回る曲があるとそれに引っ張られてしまうため、 負の単曲BPIを含む総合BPIの計算では次のような工夫が必要だと考えていました。
本来BPIはランカーさんの実力を評価するためのものである、という思想が強かったので、 当時はこの問題を完全には詰めきれていませんでした。
※過去BPIで順位推定をしようと思っていましたが、正直難しかったです。
ただ、思想としては、 順位 = (皆伝取得者数の半分)^((100 - BPI) / 100) です。これは総合も単曲も同じです。
順位 = (皆伝取得者数 / 2)^((100 - BPI) / 100)旧ページでは、単曲BPIと総合BPIで別々の近似式を書いていましたが、 今の感覚としては上の形で統一して考えるのがよいと思っています。
当たり前ですが、全ての人が皆伝平均を超えたスコアを出すことは出来ません。
BPIは元々、IIDXのランカーさんたちの実力をうまく数値化したいという発想の下で作成されました。 そのため、0点が皆伝平均であるという、ある意味かなり厳しい指標になっています。
総合BPIの計算の際に下限を-15(だいたい十段平均)とすることを推奨していたこともありますが、 負の領域に関しては想定とは異なる数値が出力されてしまうこともあると考えています。
この点においては、BPIがこのような経緯で作られたことを踏まえて見ていただけたらと思います。
自分のスコア、皆伝平均、ノーツ数、全1、係数を入れてBPIを計算できます。 目標BPIに必要なスコア、BPI 0 / 10 / ... / 100 の表、係数ごとのカーブも見られます。
理論値: 3668
比較係数:
現在のBPI: -
目標BPIに必要なスコア: -
| BPI | 必要スコア |
|---|
このページのツールで使っているものと同じ考え方の実装例です。 BPIの計算、逆関数による目標BPIからの必要スコア計算をそのまま置いています。
type BpiInput = {
score: number;
average: number;
best: number;
notes: number;
p: number;
};
const theoreticalScore = (notes: number): number => notes * 2;
const pgf = (rate: number, maxScore: number): number => {
if (rate >= 1) return maxScore;
return 1 + (rate - 0.5) / (1 - rate);
};
export const calculateBpi = ({
score,
average,
best,
notes,
p,
}: BpiInput): number => {
const m = theoreticalScore(notes);
const S = pgf(score / m, m);
const K = pgf(average / m, m);
const Z = pgf(best / m, m);
const Sp = S / K;
const Zp = Z / K;
const lnSp = Math.log(Sp);
const lnZp = Math.log(Zp);
if (score >= average) {
return 100 * (Math.pow(lnSp, p) / Math.pow(lnZp, p));
}
return -100 * (Math.pow(-lnSp, p) / Math.pow(lnZp, p));
};
export const scoreForTargetBpi = ({
targetBpi,
average,
best,
notes,
p,
}: {
targetBpi: number;
average: number;
best: number;
notes: number;
p: number;
}): number => {
const m = theoreticalScore(notes);
const K = pgf(average / m, m);
const Z = pgf(best / m, m);
const Zp = Z / K;
const lnZp = Math.log(Zp);
const sign = targetBpi >= 0 ? 1 : -1;
const lnSp = sign * Math.pow(Math.abs(targetBpi) / 100, 1 / p) * lnZp;
const Sp = Math.exp(lnSp);
const S = Sp * K;
const rate = Math.max(0, Math.min(1, 1 - 0.5 / S));
return rate * m;
};
このページは、旧 FC2 ページ aboutBPI.html の内容をベースに、現在の考え方も加えつつ移植したものです。