『Python数値計算ノート』ではアフィリエイトプログラムを利用して商品を紹介しています。

分散と標準偏差、偏差値

【Python統計学】分散と標準偏差、偏差値

この記事ではデータのバラつき具合を表す分散と標準偏差、そして偏差値について解説します。最初に必要なモジュールをまとめてインポートして、numpy.set_printoptions() で表示形式を設定しておきます。

# math_score

# In[1]

# モジュールをインポート
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import zscore

# 要素は小数点以下3桁まで表示
# 10要素を超える配列は省略表示
# 省略する場合は先頭と末尾に5要素を表示
np.set_printoptions(precision=3, threshold=10, edgeitems=5)

数学の模擬試験が実施されたと仮定して、1000 人分の得点データを作成します (正規分布乱数を用いて生成される仮想データです)。

# In[2]

# 数学の模擬試験の得点データの生成
np.random.seed(0)
x = np.random.normal(63, 15, 1000)
data = x.astype(np.int64)
data[data < 0] = 0
data[data > 100] = 100

data には 1000 人の得点分布が格納されています。

# In[3]

# dataの中身を表示
print(data)

# [89 69 77 96 91 ... 69 60 64 45 57]

set_printoptions() で大きなデータは省略表示されるように設定してあるので、すべてのデータは見られません。敢えて設定を変えれば確認できますが、1000 個の数字の羅列を眺めても分布の特徴は掴めません。そこで Matplotlib を使って、得点分布をヒストグラムで表してデータの概観を把握することにします。

# In[4]

# FigureとAxes
fig = plt.figure(figsize=(5, 5))
ax = fig.add_subplot(111)

# 軸ラベルと軸範囲
ax.set_xlabel("Score", fontsize = 15)
ax.set_ylabel("Frequency", fontsize = 15)
ax.set_xlim(0, 100)

# ヒストグラムを描画
ax.hist(data, range=(0,100), color = "blue")

# グラフを描画
plt.show()

[Python] 数学の模擬試験の得点分布のヒストグラム
ヒストグラムを眺めると、40 ~ 80 点の範囲に大多数が収まっていることがわかります。以下のコードを実行すると、この範囲内にある人数が 827人であることを確認できます。

# In[5]

# 40点以上80点以下の範囲にある人数をカウント
x = np.count_nonzero(data[(40 <= data) & (data <= 80)])

print(x)
# 827

100 点をとった人はいるでしょうか?

# In[6]

# 100点をとった人数をカウント
x = np.count_nonzero(data[data==100])

print(x)
# 4

満点が 4 人いました。逆に 0 点だった人はいるでしょうか?

# In[7]

# 0点をとった人数をカウント
x = np.count_nonzero(data[data==0])

print(x)
# 0

0 点をとった人はいないようです。numpy.min() を使って調べると最低点は 17 点であることがわかります。いずれにしても極端に低い点数や高い点数をとる人、すなわち平均値から遠く離れた人の割合が少ないことは、ヒストグラムからも一目瞭然です。

分散 (variance)

とはいえ、平均値に近いところにどれだけの人数が集中しているのか (逆に言えば平均値から遠いところにある人数がどの程度まばらになるか) はデータの種類ごとに異なるので、バラつきの程度を表す指標があると便利です。
 
そこで、与えられたデータ $x_1,\ x_2,\ ...,\ x_n$ の平均値が $\mu$ であるとき、データの 分散 (variance) とよばれる量を次式で定義します。
 \[\sigma^2=\frac{(x_1-\mu)^2+(x_2-\mu)^2+\cdots+(x_n-\mu)^2}{n}=\frac{\sum_{i=1}^{n}(x_i-\mu)^2}{n}\tag{1}\]
平方和 (2乗和) で定義されるので、分散は常に正の値をとります。平均値から大きく離れたデータが多いほど分散の値が大きくなります。逆にデータ全体が平均値付近に集中している傾向にあれば分散は小さくなります。
 
numpy.mean() で得点の平均値を計算してみましょう。

# In[8]

# 得点の平均値
data_mean = np.mean(data)

print("{:.3f}".format(data_mean))
# 61.815

分散は numpy.var() にデータ (配列) を渡して計算できます。

# In[9]

# 得点の分散
data_var = np.var(data)

print("{:.3f}".format(data_var))
# 218.897

標準偏差 (standard deviation)

分散は元のデータの 2 乗のオーダーとなってしまうので、バラつきの程度が実感しにくいという欠点があります。先ほどの例では分散が 219.534 であることがわかりましたが、100 点満点の試験を考えているので、バラつきの程度を直感的に把握できません。そこで分散の平方根をとった 標準偏差 (standard deviation) とよばれる値
 \[\sigma=\sqrt{\frac{\sum_{i=1}^{n}(x_i-\mu)^2}{n}}\tag{2}\]
を定義します。標準偏差のオーダーは元のデータと同じ程度なので、直感的にバラつきの程度を把握しやすくなります。データ分析や機械学習の分野では、どちらかといえば分散よりも標準偏差のほうが好んで使われます。
 
標準偏差は numpy.std() を使って計算できます。

# In[10]

# 得点の標準偏差
data_std = np.std(data)

print("{:.3f}".format(data_std))
# 14.795

得点分布の標準偏差は 14.795 です。
この値は、どの程度のバラつきを表しているのでしょうか?
一般に、試験の得点分布や人間の身長分布などは正規分布で近似できて、正規分布では全体の約 68% が $\mu-\sigma$ と $\mu+\sigma$ の範囲に収まることが知られています。確めてみましょう。

# in[11]

# 平均値-標準偏差
a = data_mean - data_std

# 平均値+標準偏差
b = data_mean + data_std

# [a,b]の範囲にある人数をカウント
x = np.count_nonzero(data[(a <= data) & (data <= b)])

print(x)
# 677

1000 人のうち 677 人が平均点から ±14.795 の範囲に収まっています。

偏差値 (standard score)

かつて受験生だった人、あるいは現在の受験生の皆さんは全国模試の結果を点数ではなく、偏差値に着目していたはずです。点数そのものは試験の難易度によって変化するので、全体の中の順位を示す指標としては意味がないからです。
 
受験生を一喜一憂させる 偏差値 (standard score) は、得点 $x_i$ と平均値 $\mu$、そして標準偏差 $\sigma$ を使って次のように定義されます。
 \[T_i=50+10\ \frac{x_i-\mu}{\sigma}\tag{3}\]
50 を基準値として、点数が平均値より 1 標準偏差だけ高く (低く) なるごとに 10 ポイントが加算 (減算) される仕組みになっています。得点が 45, 90 であった受験生の偏差値を計算してみましょう。

# In[12]

# 45点の偏差値を計算
t_45 = 50 + 10 * (45 - data_mean) / data_std

# 90点の偏差値を計算
t_90 = 50 + 10 * (90 - data_mean) / data_std

print("45点をとった受験生の偏差値: {:.3f}".format(t_45))
print("90点をとった受験生の偏差値: {:.3f}".format(t_90))

# 45点をとった受験生の偏差値: 38.635
# 90点をとった受験生の偏差値: 69.050

データの標準化 (standardization)

偏差値の定義式の中で
 \[\frac{x_i-\mu}{\sigma}\]
という操作を行ないました。分子と分母は同じ単位なので無名数 (次元をもたない数) です。データに含まれるすべての値について、平均値との差分をとってから標準偏差で割る操作を データの標準化 (standardization) とよび、標準化されたデータは z-score とよばれます。
 \[z=\frac{x-\mu}{\sigma}\tag{4}\]
scipy.stats.zscore() にデータ (配列) をわたすと、データが標準化されます。

# In[13]

# データを標準化
z = zscore(data)

print(z)
# [1.837  0.486  1.026  2.311  1.973 ...  0.486 -0.123  0.148 -1.137 -0.325]

配列 z に 10 を掛けて 50 を加えれば、すべての受験生の偏差値を格納したデータを得ます。

# In[14]

# 全データの偏差値を計算
data_t = 50 + 10 * z

print(data_t)
# [68.374 54.856 60.263 73.106 69.726 ... 54.856 48.773 51.477 38.635 46.746]

z-score の平均値は常に 0 です。ただし、数値計算では若干の誤差が生じます

# In[15]

# 標準化されたデータの平均値
z_mean = np.mean(z)

print(z_mean)
# 1.5631940186722203e-16

z-score の標準偏差は 1 となります。

# In[16]

# 標準化されたデータの標準偏差
z_std = np.std(z)

print(z_std)
# 1.0

不偏分散と標本標準偏差

$n$ の代わりに $n-1$ で除した分散を不偏分散 (unbiased variance) とよびます:
 \[\nu^2=\frac{\sum_{i=1}^{n}(x_i-\mu)^2}{n-1}\tag{5}\]
一般に不偏分散は標本 (母集団の一部を切り出した集団) から母集団の分散を推測するために用いられます。あくまで推定値であって、必ずしも一致するわけではありませんが、普通の分散よりも不偏分散のほうが推定精度が高くなることが知られています。
 
例として、模擬試験の得点データからランダムに 100 個のデータを取り出して標本としてみます。

# In[17]

np.random.seed(1)

# 母集団から100個のデータをランダムに選択
sample = np.random.choice(data, 100)

print(sample)
# [81 74 77 80 63 ... 35 53 42 52 68]

numpy.var() で ddof=1 を指定すると不偏分散を返します。

# In[18]

# 不偏分散を計算
sample_uvar = np.var(sample, ddof=1)

print("{:.3f}".format(sample_uvar))
# 223.987

不偏分散の平方根を標本標準偏差とよびます。
 \[\nu=\sqrt{\frac{\sum_{i=1}^{n}(x_i-\mu)^2}{n-1}}\tag{6}\]
ただし、標本標準偏差は標本のバラつきの程度を示すもので、母集団の標準偏差の推定量ではありません。numpy.std() で ddof=1 を指定すると標本標準偏差を返します。

# In[19]

# 標本標準偏差を計算
sample_ustd = np.std(sample, ddof=1)

print("{:.3f}".format(sample_ustd))
# 14.966

statistics.pvariance()

statistics.pvariance() はデータの母分散 (母集団の分散) を返します。

statistics.pvariance(data, mu=None)

第 2 引数 mu には data の平均値を渡します。
この引数を省略した場合、平均値は自動計算されます。

# PYTHON_STATISTICS_PVARIANCE

# In[1]

import statistics

data = [90,  2, 48,  7, 41, 97,  3, 58, 65, 82]

# 母分散を計算
pvar = statistics.pvariance(data)

print("{:.3f}".format(pvar))
# 1156.410

statistics.variance()

statistics.variance() はデータの不偏分散を返します。

statistics.variance(data, xbar=None)

xbar にはデータの平均値を渡します。この引数が省略されるか None が指定された場合、平均値は自動計算されます。

# PYTHON_STATISTICS_VARIANCE

# In[1]

import statistics

data = [81, 34, 85, 76, 77,  6, 83, 66, 91, 62]

# 不偏分散を計算
var = statistics.variance(data)

print("{:.3f}".format(var))
# 706.767

statistics.pstdev()

statistics.pstdev() はデータの母標準偏差 (母分散の平方根) を返します。

statistics.pstdev(data, mu=None)

mu には data の平均値を渡します。
この引数を省略した場合、平均値は自動計算されます。

# PYTHON_STATISTICS_PSTDEV

# In[1]

import statistics

data = [39,  8, 10, 39, 75, 86, 33, 82, 86, 20]

# 母標準偏差を計算
pstdev = statistics.pstdev(data)

print("{:.3f}".format(pstdev))
# 29.979

statistics.stdev()

statistics.stdev() はデータの標本標準偏差 (不偏分散の平方根) を返します。

statistics.stdev(data, xbar=None)

xbar には data の平均値を渡します。
この引数を省略すると平均値は自動計算されます。

# PYTHON_STATISTICS_STDEV

# In[1]

import statistics

data = [50, 64, 38, 84, 81, 36, 33, 13, 47, 34]

# 母標準偏差を計算
stdev = statistics.stdev(data)

print("{:.3f}".format(stdev))
# 22.450

numpy.var()

numpy.var() は配列を受け取り、指定軸に沿ったデータの分散 (variance) を返します。

numpy.var(a, axis=None, dtype=None, out=None, ddof=0,
keepdims=<no value>)

たとえば、次のような 2 次元配列に格納されたデータがあるとします。

# NUMPY_VARIANCE

# In[1]

import numpy as np

# 2次元配列を用意
data = np.array([[ 86,  75,  98],
                 [ 93, 109, 112],
                 [106,  96,  75]])

axis を省略した場合は全要素の分散を計算します。

# In[2]

# 全要素を対象に分散を計算
var = np.var(data)

print("{:.3f}".format(var))
# 166.469

axis=0 を指定した場合は列ごとに分散を計算します。

# In[3]

# 列ごとに分散を計算
var = np.var(data, axis=0)

print(var)
# [68.667 196.222 232.667]

axis=1 を指定すると、行ごとに分散を計算します。

# In[4]

# 行ごとに分散を計算
# 次元は保持する
var = np.var(data, axis=1, keepdims=True)

print(var)
# [[ 88.222]
#  [ 69.556]
#  [166.889]]

データに nan が含まれていると、numpy.var() は nan を返します。欠損値を無視して処理したい場合は次節で説明する numpy.nanvar() を使ってください。

numpy.nanvar()

numpy.nanvar() は配列を受け取って、指定軸に沿って分散を計算します。

numpy.nanvar(a, axis=None, dtype=None, out=None, ddof=0,
keepdims=<no value>)

使い方は numpy.var() と同じですが、配列に nan が含まれていても、nan を除外して処理を実行します。

# NUMPY_NANVAR

# In[1]

import numpy as np

# 2次元配列を用意
data = np.array([[97,  95,     123, 112],
                 [75,  np.nan, 116, 106],
                 [80,  78,     134, np.nan]])

# 行ごとに分散を計算
# nanは処理対象に含めない
var = np.nanvar(data, axis=1, keepdims=True)

print(var)
# [[131.188]
#  [304.667]
#  [672.889]]

numpy.std()

numpy.std() は配列を受け取って指定軸に沿った標準偏差を返します (軸を指定しなければ全要素について標準偏差を計算します)。

np.std(a, axis=None, dtype=None, out=None, ddof=0,
keepdims=<no value>)

デフォルト設定では戻り値は 1 次元配列ですが、keepdims に True を渡せば元の配列の次元を保持します。たとえば、a に 2 次元配列を渡して axis=1, keepdims=True を指定すると、行ごとに標準偏差を計算して 2 次元配列を返します。

# NUMPY_STD

# In[1]

import numpy as np

# 2次元配列を用意
data = np.array([[135, 137,  93, 121],
                 [ 71, 117,  98, 111],
                 [ 80,  81,  65, 101]])

# 行ごとに標準偏差を計算して次元は保持する
std = np.std(data, axis=1, keepdims=True)

print(std)
# [[17.571]
#  [17.697]
#  [12.794]]

データに nan が含まれていると、numpy.std() は処理不能と判断して nan を返します。nan を無視して処理を実行したい場合は numpy.nanstd() を使ってください。

numpy.nanstd()

numpy.nanstd() は配列を受け取って、指定軸に沿った要素の標準偏差を返します (軸を指定しない場合は全要素について標準偏差を計算します)。

numpy.nanstd(a, axis=None, dtype=None, out=None, ddof=0,
keepdims=<no value>)

配列に nan が含まれていても、nan を無視して処理します。
デフォルト設定では戻り値は 1 次元配列です (受け取った配列の次元に関わらずフラットな配列を返します)。keepdims=True を指定すると元の配列の次元を保持します。たとえば第 1 引数に 2 次元配列を渡して axis=1, keepdims=True を指定すると、行ごとの標準偏差を格納した 2 次元配列を返します。

# NUMPY_NANSTD

# In[1]

import numpy as np

# 2次元配列を定義
data = np.array([[ 59, np.nan,  86,   105],
                 [105,    141,  99, np.nan],
                 [ 84, np.nan, 108,     71]])

# 行ごとに標準偏差を計算して次元は保持する
std = np.nanstd(data, axis=1, keepdims=True)

print(std)
# [[18.874]
#  [18.547]
#  [15.326]]

 

scipy.stats.zscore()

SciPy の統計分析用サブパッケージ stats の zscore() はデータを標準化して z-score を返します。

scipy.stats.zscore(a, axis=0, ddof=0, nan_policy='propagate')

z-score とは、個々のデータから平均値を引いて、標準偏差で割った値として定義されます。
 \[z=\frac{x-\mathrm{x_{mean}}}{\mathrm{x_{std}}}\tag{5}\]
標準化は axis で指定した軸に沿って実行されます。
axis=0 を指定すれば x は配列 a の列に沿うデータであり、axis=1 を指定すれば x は配列 a の行に沿うデータとなります。

# SCIPY_ZSCORE

# In[1]

import numpy as np
from scipy.stats import zscore

# 2次元配列を用意
data = np.array([[104,  83, 107,  93],
                 [105, 108, 133, 114],
                 [122,  98, 138,  46]])

# 行ごとにデータを標準化
z = zscore(data, axis=1)

print(z)
# [[ 0.763 -1.448  1.079 -0.395]
#  [-0.919 -0.643  1.654 -0.092]
#  [ 0.603 -0.086  1.063 -1.58 ]]

コメント

  1. HNaito より:

    math_score In[6] ~ In[15] プログラムの実行結果 (満点の人数、得点の平均値と分散、標準偏差、偏差値、z-score ) が記事の実行結果と微妙に異なりました。調べた結果、In[2] プログラムのdata[data > 100] = 100 をコメントアウトすると結果が一致するようになったので、ご確認ください。

    下記は誤植と思われますので、ご確認ください。
    math_score In[9] プログラムの上の文で、データ(配列)とデータの平均値を → データ(配列)を
    (6)式で、Σの右上の n-1 → n、分母の n → n-1
    NUMPY_STD In[1] プログラムの上の文で、分散 → 標準偏差 (3ヶ所)
    NUMPY_STD In[1] プログラムのコメントで、# 行ごとに分散を → # 行ごとに標準偏差を
    NUMPY_STD In[1] プログラムで、np.nanstd( ) → np.std( )
    NUMPY_STD In[1] プログラムの下の文で、numpy.nan( ) → numpy.std( )
    NUMPY_NANSTD In[1] プログラムのコメントで、# 行ごとに分散を → # 行ごとに標準偏差を

    • あとりえこばと より:

      大変申し訳ないです。動作を再確認したところ確かに値がずれていました。ずいぶん昔に書いた記事なので、記憶が定かではないのですが、
      data[data < 0] = 0
      data[data > 100] = 100
      の 2 行を除いて実行したコードセルをコピーして記事に載せてしまったのだど思います (0 点以下のデータが存在しなかったので、data[data < 0] = 0 はあってもなくても影響ありません)。100 より大きな点をとった人が存在するとおかしいので、data[data > 100] = 100 は必要です。あらためて実行結果を正しい値に書き直しておきました。本当にご迷惑をおかけしました。m(_ _)m

      • HNaito より:

        修正ありがとうございました。記事と実際の実行結果が一致することを確認できました。
        実行結果の値の修正にともなって、本文も下記の修正が必要と思いますのでご確認ください。
        In[6] プログラムの下の文で、満点が 1 人 → 満点が 4 人
        In[10] プログラムの下の文で、標準偏差は 14.817 → 標準偏差は14.795
        In[11] プログラムの下の文で、±14.817 → ±14.795