【統計②】Pythonで作りながら理解する分散と標準偏差の数値化

Python

「平均利回り5%の投資信託AとB。あなたなら、どちらに資産を預けますか?」

そう聞かれたら、多くの人は「平均が同じなら、どちらでも結果は同じでは?」と答えるかもしれません。

しかし、もしAが「毎日0.1%ずつ着実に増える商品」で、Bが「ある日は+10%だが、翌日は-5%と激しく上下する商品」だとしたらどうでしょう。
たとえ1ヶ月後の平均利回りが同じ5%に落ち着いたとしても、その運用の中身は全く別物です。
Bのような激しい値動きは、売却のタイミング一つで大きな損失を出す不確実性を孕んでおり、投資家にとっては無視できない「危うさ(=リスク)」となります。

前回の記事では、データの中心を示す「代表値」を学びました。
平均・中央・最頻値を使い分ければ、データの「真ん中」を正しく射抜くことができます。
しかし、今回の投資信託の例のように、代表値が同じであっても、データの「ふり幅(バラつき)」によってその性質は決定的に異なるのです。

この「手元の数値が、平均からどれくらい離れてふらついているか」という不確実性を客観的な数字にしたものが、今回のテーマである「分散」と「標準偏差」です。

どれほど代表値が立派でも、バラつきの正体を掴めていなければ、そのデータは「制御不能」なままです。
今回はPythonを使い、曖昧なバラつきを明確な指標へと変換するプロセスを体験します。

第1章:代表値は同じでも性質が真逆なデータを定義する

バラツキを数値化するプロセスを体験するために、ここでは「代表値(平均・中央・最頻値)のすべてが同じでありながら、性質が決定的に異なる」2つの運用データを定義します。

比較するのは、以下の2つのデータセットです。

  • ケースA(安定型):すべての値が代表値の周辺に集まっている。
  • ケースB(波乱型):代表値は同じだが、極端に離れた値が含まれている。

代表値だけをどれほど多角的に分析しても、この2つのデータの「安定感」や「リスク」の差を説明することはできません。
この代表値の限界を突破するために、Pythonでシミュレーションを行いながら、新たな指標である「分散」と「標準偏差」を導き出していきましょう。

1.1. データの具体的な仕様

代表値(平均・中央・最頻)がすべて「50」で一致するように設計します。
こうすることで、「統計学的な中心はまったく同じなのに、データとしてのリスク(不確実性)がこれほど違う」という事実が浮き彫りになります。

では、それぞれの数値の具体的な構成を表形式で定義しましょう。

項目ケースA:安定型運用ケースB:波乱型運用
データ構成[48, 49, 50, 50, 50, 51, 52][10, 30, 50, 50, 50, 70, 90]
平均値5050
中央値5050
最頻値5050
データ範囲48 〜 52(幅:4)10 〜 90(幅:80)
直感的特徴ほぼ確実に50前後になる50から大きく外れる可能性あり

第2章:Pythonで「性質の違い」を可視化する

第1章で定義した仕様をPythonで実装し、可視化してみましょう。本章では、代表値が完全に一致することを計算で証明したあと、グラフを用いて「見た目の違い」を明確にします。

PythonやIDE(統合開発環境)をまだお持ちでない方は、こちらの記事を参考に環境構築を完了させてください。

2.1. データの定義と代表値の算出

まず、必要なライブラリをインストールします。
次のコマンドをターミナルで実行してください。

pip install scipy numpy matplotlib japanize_matplotlib

続いてデータをリスト形式で定義し、それぞれの平均値・中央値・最頻値を算出します。
前回記事でも使用した日本語化ライブラリ japanize_matplotlib を読み込み、グラフの準備も同時に行っていますが、英語のままでも大丈夫であればインポートは不要です。

visualize_dispersion.py
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib  # 日本語化ライブラリ
from scipy import stats

# 1. データの定義(仕様通り、中心50に設定)
data_a = [48, 49, 50, 50, 50, 51, 52]
data_b = [10, 30, 50, 50, 50, 70, 90]

# 2. 代表値を確認するための関数
def print_representative_values(name, data):
    print(f"--- {name} ---")
    print(f"平均値: {np.mean(data):.1f}")
    print(f"中央値: {np.median(data):.1f}")
    # modeの結果から数値のみを取り出す
    mode_val = stats.mode(data, keepdims=True).mode[0]
    print(f"最頻値: {mode_val}")

print_representative_values("ケースA(安定型)", data_a)
print_representative_values("ケースB(波乱型)", data_b)

このプログラムを実行すると、コンソールにはどちらも「平均50.0、中央50.0、最頻値50」と表示されます。
代表値3種だけを見れば、両者は全く同じデータであると判断できますね。

実行結果
--- ケースA(安定型) ---
平均値: 50.0
中央値: 50.0
最頻値: 50
--- ケースB(波乱型) ---
平均値: 50.0
中央値: 50.0
最頻値: 50

2.2. グラフによる散らばりの比較

次に、データの「散らばり」を視覚的に確認するためにドットプロットを作成します。

2.1. で作成した visualize_dispersion.py に続けて、以下のコードを追加してください。

visualize_dispersion.py(続き)
# 3. 可視化(データの散らばりを比較)
plt.figure(figsize=(10, 4))
plt.scatter(data_a, [1]*len(data_a), color="blue", label="ケースA(安定型)", alpha=0.6, s=100)
plt.scatter(data_b, [2]*len(data_b), color="red", label="ケースB(波乱型)", alpha=0.6, s=100)

plt.ylim(0, 3)
plt.xlabel("リターンの値")
plt.yticks([1, 2], ["ケースA", "ケースB"])
plt.title("データの散らばり具合の比較")
plt.legend()
plt.grid(axis='x', linestyle='--')
plt.show()

実行結果のグラフを見ると、代表値は同じでも、データが分布している「幅」には圧倒的な差があることが分かります。

  • ケースA:中心の50に密集しており、値が予測しやすい。
  • ケースB:中心は50だが、10や90といった極端に離れた値が混ざっている。
実行結果

第3章:「平均との差」を数値化する

「データのバラつき」を数値化するための第一歩は、「それぞれのデータが平均値からどれくらい離れているか」の差を測ることです。
この「平均との差」のことを統計学では偏差へんさと呼びます。

3.1. 偏差を計算して合計してみる

各データから平均値を引いた「偏差」を計算し、その合計を出してみましょう。
新しく calculate_variance.py というPythonファイルを作成して実装します。

calculate_variance.py
import numpy as np

# データの再定義
data_a = [48, 49, 50, 50, 50, 51, 52]
data_b = [10, 30, 50, 50, 50, 70, 90]

# 平均値の取得
mean_a = np.mean(data_a)
mean_b = np.mean(data_b)

# 1. 偏差(データ - 平均値)を計算
deviation_a = [x - mean_a for x in data_a]
deviation_b = [x - mean_b for x in data_b]

print(f"ケースAの偏差: {deviation_a}")
print(f"ケースBの偏差: {deviation_b}")

# 2. 偏差を合計してみる
print(f"ケースAの偏差合計: {sum(deviation_a)}")
print(f"ケースBの偏差合計: {sum(deviation_b)}")

3.2. 「偏差の合計」が抱える致命的な問題

上記のプログラムを実行すると、以下のような結果が表示されます。
なんと、ケースAもケースBも、偏差の合計は「0」になってしまいました。

実行結果
ケースAの偏差: [np.float64(-2.0), np.float64(-1.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(1.0), np.float64(2.0)]
ケースBの偏差: [np.float64(-40.0), np.float64(-20.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(20.0), np.float64(40.0)]
ケースAの偏差合計: 0.0
ケースBの偏差合計: 0.0

この結果は意外に感じられるかもしれませんが、こうなる理由があります。
平均値とはデータの「重心」です。
そのため、平均より大きいプラスの差と、平均より小さいマイナスの差をすべて足し合わせると、必ず打ち消し合ってゼロになります。
考えてみれば、「そうだよな~」と納得できますよね。

とは言え、これではバラツキが激しいデータであっても、合計すると「バラツキなし(ゼロ)」と評価されてしまい、比較の役に立ちません。

3.3. 打開策は「2乗」

「プラスとマイナスが打ち消し合う」という問題を解決するために登場するのが、「2乗」という数学的トリックです。

数値を2乗すれば、すべての偏差は強制的にプラスの値になります。

  1. 各データの偏差を出す
  2. 偏差を2乗する
  3. その合計をデータの個数で割る(=2乗した偏差の平均をとる)

この手順で得られる数値が、今回のメインテーマのひとつである 分散ぶんさん です。

3.4. Pythonで分散を実装する

ここでは、新しく計算用のファイルを作るのではなく、先ほどの calculate_variance.py に追記して、分散を導き出してみましょう。

calculate_variance.py(続き)
# 3. 偏差を2乗する(すべての値をプラスにする)
squared_deviation_a = [x**2 for x in deviation_a]
squared_deviation_b = [x**2 for x in deviation_b]

print(f"ケースAの偏差2乗: {squared_deviation_a}")
print(f"ケースBの偏差2乗: {squared_deviation_b}")

# 4. 分散(2乗した偏差の平均)を計算する
variance_a = np.mean(squared_deviation_a)
variance_b = np.mean(squared_deviation_b)

print(f"--- 分散の計算結果 ---")
print(f"ケースAの分散: {variance_a:.1f}")
print(f"ケースBの分散: {variance_b:.1f}")

このコードを実行すると、ケースAとケースBの「差」を明確に示す 0.0 ではない数値が現れます。

実行結果で「分散の計算結果」を確認してみてください。
以下のように、ケースBの分散はケースAに比べてかなり大きな数値になっているはずです。

実行結果
ケースAの偏差2乗: [np.float64(4.0), np.float64(1.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(1.0), np.float64(4.0)]
ケースBの偏差2乗: [np.float64(1600.0), np.float64(400.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(400.0), np.float64(1600.0)]
--- 分散の計算結果 ---
ケースAの分散: 1.4
ケースBの分散: 571.4

第4章:分散の弱点と「標準偏差」の登場

第3章の実行結果で、ケースBの分散はケースAの何百倍も大きな数字になったはずです。
これによりバラつきの比較はできるようになりましたが、一つの疑問が浮かびます。

4.1. 分散の弱点:単位のズレ

私たちは、元々「リターンの値(%)」という単位でデータを考えていました。
しかし必要に迫られ、計算の途中で数値を2乗してしまったため、分散の単位は「%の2乗」という、現実には存在しない不自然な数値に変わってしまっています。

  • 元のデータ:50 (%)
  • ケースBの分散:1600 (%の2乗)

「平均50点に対して、バラツキは1600です」と言われても、数字が大きすぎてピンときませんし、元のデータ(点数)と単位が揃っておらず直接比較することもできません。

4.2. 標準偏差:平方根で単位を元に戻す

この「2乗して大きくなりすぎた数字」を、元のデータのスケールに戻すために行うのが、平方根(ルート)をとる操作です。

こうして得られた数値を 標準偏差ひょうじゅんへんさ と呼びます。

  1. 分散を計算する(2乗の平均)
  2. ルートをかける(元の単位に戻す)

このひと手間を加えることで、ようやく「平均値から見て、平均的にどれくらいのズレ(点数の幅)があるのか」を、元の単位と同じ感覚で把握できるようになります。

4.3. Pythonで標準偏差まで一気に計算する

それでは、第3章で作成した calculate_variance.py をさらに更新して、分散から標準偏差までを算出してみましょう。

ここでは、自分で計算する工程と、NumPyの関数を使って計算する方法の両方を確認します。

calculate_variance.py(最終更新)
# 5. 標準偏差(分散の平方根)を計算する
# --- 手順を追った計算(理解を深めるためのステップ) ---
# 2乗した偏差の平均をとり、さらに平方根(sqrt)をとる
std_a = np.sqrt(variance_a)
std_b = np.sqrt(variance_b)

print(f"--- 標準偏差の計算結果 ---")
print(f"ケースAの標準偏差: {std_a:.1f}")
print(f"ケースBの標準偏差: {std_b:.1f}")

# (補足) NumPyの関数を使えば一行で計算可能です
# 途中の計算(偏差・2乗・平均・ルート)をすべて内部で自動処理してくれます
print(f"\n[NumPy関数による確認]")
print(f"ケースAの標準偏差(再): {np.std(data_a):.1f}")
print(f"ケースBの標準偏差(再): {np.std(data_b):.1f}")

このプログラムを実行すると、次のような数値が出るはずです。
「標準偏差の計算結果」と「NumPy関数による確認」が最終更新で表示された情報です。

実行結果
--- 標準偏差の計算結果 ---
ケースAの標準偏差: 1.2
ケースBの標準偏差: 23.9

[NumPy関数による確認]
ケースAの標準偏差(再): 1.2
ケースBの標準偏差(再): 23.9

私の環境での実行結果では、ケースAの標準偏差は「1.2」、ケースBは「23.9」といった数値になりました。
この結果は、「平均50に対して、±1.3程度のズレしかない安定型」と「±25.8もズレる可能性がある波乱型」があることを明確に示しています。

これで、グラフで見た「見た目の違い」を、「標準偏差という共通の指標(ものさし)」を使って数値化することに成功しました。

まとめ

本記事では、代表値(平均・中央値・最頻値)だけでは捉えきれない「データの奥行き」を、Pythonを使って明らかにするプロセスを体験しました。

最後に、今回学んだ数値化のステップを振り返りましょう。

  1. 代表値の限界を知る
    • 平均・中央・最頻値がすべて同じでも、データの「広がり(散らばり)」が違えば、その性質は真逆になります。代表値はあくまで「データの中心」を示すものであり、「データの安定性やリスク」を説明するには不十分です。
  2. 偏差の合計は必ず「0」になる
    • 「平均からのズレ(偏差)」を単純に合計するだけでは、プラスとマイナスが打ち消し合い、バラツキの情報は消えてしまいます。この「合計すると0になる」という壁を突破するために、2乗という数学的処理が必要でした。
  3. 分散と標準偏差の使い分け
    • 分散:バラツキを計算するための土台。2乗しているため、単純な比較のみに使うには強いが、単位が現実離れする。
    • 標準偏差:分散の平方根(ルート)をとり、単位を元に戻したもの。平均値と同じ感覚で「平均的なズレの大きさ」を把握できる。

次なる一歩:事実がルールに近づく「大数の法則」へ

データの中心(代表値)を学び、データの広がり(分散・標準偏差)を数値化できるようになりました。
これで、目の前にあるデータの「特徴」を説明する準備は万端です。

しかし、まだ大きな疑問が残っています。
「いま手元にある少数のデータから得られた平均や分散は、本当にそのデータの源泉(母集団)と言えるのか?」

次回、第7弾は【架け橋】大数の法則がメインテーマです。
偶然に支配された「確率の世界」と、目の前の事実を整理する「統計の世界」。
この2つが重なり合い、無秩序なはずの事象が積み重なることで、背後に潜む「データの源泉(母集団)」の姿を現していくプロセスを可視化します。

「個別の偶然」が「集団の必然」へと姿を変える——。
統計学が単なる記録から、見えない背景を読み解く「予測の科学」へと進化する、最重要回に突入します。

コメント

タイトルとURLをコピーしました