📊

合っているのかわからない割合・上位%・標準偏差・基準値・偏差値の求め方

2023/06/14に公開

偏差値の計算順序

  • 偏差値を求めるには → 基準値が必要
  • 基準値をを求めるには → 標準偏差が必要
  • 標準偏差を求めるには → 分散が必要
  • 分散を求めるには → 平均が必要

なので「平均 → 分散 → 標準偏差 → 基準値 → 偏差値」の順で求める。

割合・ヒストグラムの計算順序

  • 割合やヒストグラムを求めるには → 相対度数が必要
  • 相対度数を求めるには → 度数が必要

なので「度数 → 相対度数 → 割合 or ヒストグラム」の順で求める。

デバッグ方法 (重要)

  • 相対度数の合計 = 1
  • 基準値平均 = 0[1]
  • 基準値標準偏差 = 1[2]

と、決まっているので結果が不自然な場合は上の公式に当てはまっているか確認する。

得点の羅列から偏差値を求める例

得点 = [38, 73, 86, 90, 111, 124]
include Math
平均 = 得点.sum.fdiv(得点.size)                         # => 87.0
分散 = 得点.sum { (_1 - 平均)**2 } / 得点.size            # => 758.6666666666666
標準偏差 = sqrt(分散)                                   # => 27.543904346818127
基準値 = 得点.collect { ((_1 - 平均) / 標準偏差).round(1) }  # => [-1.8, -0.5, -0.0, 0.1, 0.9, 1.3]
偏差値 = 基準値.collect { (_1 * 10 + 50).round(0) }     # => [32, 45, 50, 51, 59, 63]
得点.zip(偏差値).to_h                                  # => {38=>32, 73=>45, 86=>50, 90=>51, 111=>59, 124=>63}
分散
758.6666666666666
得点 偏差値
38 32
73 45
86 50
90 51
111 59
124 63

最後に計算が正しいか確認する。

基準値平均 = 基準値.sum.fdiv(基準値.size).round(10)                # => 0.0
基準値標準偏差 = sqrt(基準値.sum { (_1 - 基準値平均)**2 } / 基準値.size)  # => 1.0

これで公式通りなので合っているのがわかる。

  • 当初、偏差値は満点が100点のデータにしか適用できないのかと考えていたが実際は基準値から判断するので満点はいくつだろうが関係ない
  • 単位の異なるデータでも基準値または偏差値で比較できる

ところが Numo::NArray で確認すると結果が異なる

require "numo/narray"
a = Numo::DFloat[*得点]
# 分散
a.var     # => 910.4
# 標準偏差
a.stddev  # => 30.172835465033774

どういうことだ……?

R でも結果が異なる

> v = c(38, 73, 86, 90, 111, 124)
> var(v)
[1] 910.4
> sd(v)
[1] 30.17284

ここでいろいろググってみると var の計算は次のようになっているらしい。

> sum((v - mean(v))^2) / (length(v) - 1)
[1] 910.4

なぜか分母が -1 されている。

ChatGPT に聞いてみる

分散を求めるとき分母を -1 する理由を小学校低学年でもわかるように教えてください

分散を求める式には、平均値から各データの値を引いたあと、その結果を二乗し、それらを合計したものをデータ数で割るという計算が含まれています。この計算を簡単にするために、平均値から各データの値を引いた際に、正負が入れ替わることに注意が必要です。つまり、平均値より大きいデータだった場合は、引いた結果がマイナスになります。同様に、平均値より小さいデータだった場合は、引いた結果がプラスになります。

この引いた結果を二乗すると、正の数になります。しかし、平均値と各データの値の差がマイナスになってしまったデータがある場合、二乗してもプラスの数にはなりません。そこで、この場合はマイナスを考慮するために、分母に-1をかけることで、結果を正確に求めることができます。具体的には、以下のように計算します。

分散 = (平均値から各データの値を引いた結果を二乗したものの合計) ÷ (データ数 - 1)

この式において、分母をデータ数-1とした場合、平均値との差がマイナスとなるデータがあった場合でも、分散を正確に求めることができます。したがって、分母を-1とすることによって、分散を正確に求めることができます。

二乗してマイナスになる値ってどんな値?

2乗してマイナスになる値は、実数では存在しません。2乗すると常に0以上になるため、マイナスにはなりません。しかし、複素数の場合は、2乗してマイナスになる値が存在します。具体的には、虚数単位iをかけた値です。i^2=-1となります。

その方法で再度計算してみる

得点     = [38, 73, 86, 90, 111, 124]
平均     = 得点.sum.fdiv(得点.size)                           # => 87.0
分散     = 得点.sum { (_1 - 平均)**2 } / 得点.size.pred         # => 910.4
標準偏差 = sqrt(分散)                                         # => 30.172835465033774
基準値   = 得点.collect { ((_1 - 平均) / 標準偏差).round(1) }      # => [-1.6, -0.5, -0.0, 0.1, 0.8, 1.2]
偏差値   = 基準値.collect { (_1 * 10 + 50).round(0) }         # => [34, 45, 50, 51, 58, 62]
tp 得点.zip(偏差値).to_h                                     # => {38=>34, 73=>45, 86=>50, 90=>51, 111=>58, 124=>62}

基準値平均     = 基準値.sum.fdiv(基準値.size).round(10)            # => 0.0
基準値標準偏差 = sqrt(基準値.sum { (_1 - 基準値平均)**2 } / 基準値.size)  # => 0.903696114115064

偏差値が変わってしまった。基準値標準偏差も 0 ではなくなってしまった。

ざっくり調べた結果

  • 母集団がはっきりしている場合 → 分母を -1 しない
  • 標本の場合 → 分母を -1 する
  • 基準値標準偏差 0 がになるのは分母を -1 しないときだけ

ということらしい。

カテゴリーデータを単純集計表にする

次のような度数分布は項目同士を比較しようがない。

list = {
  "りんご" => 823,
  "ゴリラ" => 198,
  "めがね" => 621,
  "その他" => 468,
}

りんごとゴリラを比較しても優劣をつけようがないということ。また一峰性[3]ではないため平均や分散を出しても意味がない。このような場合は単純集計表とヒストグラムを出すのがわかりやすい。

list = list.collect { |k, v| { "カテゴリ" => k, "度数" => v } }
度数合計 = list.sum { |e| e["度数"] }  # => 2110
list = list.collect { |e| e.merge("相対度数" => e["度数"].fdiv(度数合計).round(3) ) }
list = list.collect { |e| e.merge("割合" => (e["相対度数"] * 100).round.to_s + " %" ) }
list = list.collect { |e| e.merge("ヒストグラム" => "*" * (e["相対度数"] * 30).round) }
カテゴリ 度数 相対度数 割合 ヒストグラム
りんご 823 0.39 39 % ************
ゴリラ 198 0.094 9 % ***
めがね 621 0.294 29 % *********
その他 468 0.222 22 % *******

これがあっているかどうかは相対度数の合計が 1 になっているかを確認する。

相対度数合計 = list.sum { |e| e["相対度数"] }  # => 1.0

数量データ化できそうなカテゴリーデータ

これは架空の腕前ランクの度数分布になる。

counts = {
  "D-" => 95,
  "D"  => 157,
  "D+" => 316,
  "C-" => 764,
  "C"  => 1695,
  "C+" => 3202,
  "B-" => 5780,
  "B"  => 8428,
  "B+" => 9626,
  "A-" => 7682,
  "A"  => 4834,
  "A+" => 3040,
  "S-" => 1209,
  "S"  => 573,
  "S+" => 420,
  "X-" => 138,
  "X"  => 34,
  "X+" => 21,
}

階級は架空だけど度数は実際に存在するゲームの一定期間から一部を採取した。これをたぶん標本という。

相対度数とヒストグラム

普通のカテゴリーデータのときと同様にヒストグラムを出す場合は、

相対度数・ヒストグラム
list = counts.collect { |k, v| { "階級" => k, "度数" => v } }
度数合計 = list.sum { |e| e["度数"] }  # => 48014
list = list.collect { |e| e.merge("相対度数" => e["度数"].fdiv(度数合計).round(6) ) }
list = list.collect { |e| e.merge("ヒストグラム" => "*" * (e["相対度数"] * 40).round) }

となる。プレイヤーはヒストグラムを見れば自分がどのあたりにいるのかわかる。しかし他者に伝えたいときはどう言えばいいだろう? 階級 A- の人が「全体的に見てちょっとうまいほう」と伝えないといけないとしたらそれはイケてない。つまりこれは運営視点での度数分布がよりわかりやすくなったにすぎない。

プレイヤーが本当に望む情報は?

プレイヤーだったら自分が上位ナンパーセントに含まれるのかを知りたい。その場合は累計相対度数を求めればよいのだが、小数で出してもピンとこないので「1 - 累計相対度数」で「上位何%」に含まれるのかを示せばプレイヤーが望む情報になる。

累積相対度数・上位
t = 0; list = list.collect { |e| t += e["相対度数"]; e.merge("累積相対度数" => t.round(6)) }
t = 0; list = list.collect { |e| e.merge("上位" => "%.2f %%" % ((1 - t) * 100)).tap { t += e["相対度数"] } }

これで階級 A- の人たちは「上位 37 %」と伝えることができる。

  • 累積相対度数はその階級の相対度数を含める
  • (昇順の場合)上位%にはその階級を相対度数を含めない
  • 含めてしまうと D- が上位100%にならない

上位%を求める際の階級の向きに注意

はまりやすいポイントとして階級の向きがある。向きによって相対度数を足す順番が少し変わる。もし階級が降順の場合は次のようになる。

# -  t = 0; list = list.collect { |e| e.merge("上位" => "%.2f %%" % ((1 - t) * 100)).tap { t += e["相対度数"] } }
# +  t = 0; list = list.collect { |e| t += e["相対度数"]; e.merge("上位" => "%.2f %%" % (t * 100)) }

偏差値

また上位何%ではなく偏差値が知りたいという場合もある。ここで価格帯の度数分布をイメージすると階級は価格帯を表し、その価格帯が「500円以上600円未満」であれば階級値は 550 になる。ということは階級 "D-" などにも適当に階級値を自分で割り振ってやればいいのではないかと考えてやってみたら計算できた。

下の例では 0 から 17 まで割り振ったけど別にどの値から始めても基準値に変化はなかった。なので階級値自体には意味がなくてそれよりも増減の方向と等間隔が重要っぽい。

あとこれは標本なので分散の分母を -1 してみた。が、してもしなくても偏差値に変化はなかった。

階級値・基準値・偏差値
list = list.collect.with_index { |e, i| e.merge("階級値" => i) }
得点合計 = list.sum { |e| e["度数"] * e["階級値"] }                        # => 378281
得点平均 = 得点合計.fdiv(度数合計)                                            # => 7.878556254425792
分散 = list.sum { |e| (e["階級値"] - 得点平均)**2 * e["度数"] } / 度数合計.pred  # => 5.099303553611663
標準偏差 = sqrt(分散)                                                   # => 2.2581637570405877
list = list.collect { |e| e.merge("基準値" => (e["階級値"] - 得点平均).fdiv(標準偏差).round(2) ) }
list = list.collect { |e| e.merge("偏差値" => (e["基準値"] * 10 + 50).round) }
階級 度数 相対度数 ヒストグラム 累積相対度数 上位 階級値 基準値 偏差値
D- 95 0.001979 0.001979 100.00 % 0 -3.49 15
D 157 0.00327 0.005249 99.80 % 1 -3.05 20
D+ 316 0.006581 0.01183 99.48 % 2 -2.6 24
C- 764 0.015912 * 0.027742 98.82 % 3 -2.16 28
C 1695 0.035302 * 0.063044 97.23 % 4 -1.72 33
C+ 3202 0.066689 *** 0.129733 93.70 % 5 -1.27 37
B- 5780 0.120382 ***** 0.250115 87.03 % 6 -0.83 42
B 8428 0.175532 ******* 0.425647 74.99 % 7 -0.39 46
B+ 9626 0.200483 ******** 0.62613 57.44 % 8 0.05 51
A- 7682 0.159995 ****** 0.786125 37.39 % 9 0.5 55
A 4834 0.100679 **** 0.886804 21.39 % 10 0.94 59
A+ 3040 0.063315 *** 0.950119 11.32 % 11 1.38 64
S- 1209 0.02518 * 0.975299 4.99 % 12 1.83 68
S 573 0.011934 0.987233 2.47 % 13 2.27 73
S+ 420 0.008747 0.99598 1.28 % 14 2.71 77
X- 138 0.002874 0.998854 0.40 % 15 3.15 82
X 34 0.000708 0.999562 0.11 % 16 3.6 86
X+ 21 0.000437 0.999999 0.04 % 17 4.04 90

最後に計算が正しいか確認する。

相対度数合計 = list.sum { |e| e["相対度数"] }                                       # => 0.999999
基準値平均 = list.sum { |e| e["基準値"] } / list.count                            # => 0.27555555555555555
基準値標準偏差 = sqrt(list.sum { |e| (e["基準値"] - 基準値平均)**2 } / list.count.pred)  # => 2.364495275634259

と、基準値平均が 0 にならないのはどうしてだろう? このくらいの誤差は無視していいのだろうか。誤差にしては大きすぎる気もする。度数分布の場合は 0 にならないのか、それとも計算が間違っているのか。

ChatGPTに聞いてみる

本には基準値平均は必ず0になると書いてありましたが、度数分布の基準値平均が0にならないケースはありますか?

はい、度数分布の基準値平均が0にならないケースはあります。例えば、負の値しか取らないデータの場合、基準値平均は負の値になります。また、偏りがある分布の場合、基準値平均が0に近くなる場合もありますが、必ずしも0になるわけではありません。基準値平均が0になるのは、データが対称的であり、分布の左右対称性がある場合に限られます。

基準値平均が0にならなかった理由は山が左右対称じゃないかららしい。ということは「基準値平均 = 0」と「基準値標準偏差 = 1」の公式は最初の「得点の羅列から偏差値を求める例」のように度数分布でない場合にしか当てはまらない……と思われる。

参照

脚注
  1. 左右対象な山になる場合に限る ↩︎

  2. 基準値平均が 0 である必要があるので結局、左右対象な山になる場合に限る ↩︎

  3. ヒストグラムにしたとき山が一つできる形のもの ↩︎

Discussion