株価リターンは正規分布に従わない?歪度・尖度で「暴落しやすさ」を数値化してみた

基礎知識・戦略

「株価のリターンは正規分布に従う」——教科書にはそう書いてあります。ブラック・ショールズもVaRも、多くの金融理論がこの前提の上に立っています。ところが実際のデータで確かめてみると、この前提はかなり大きく外れています

問題は「少し外れている」程度で済まないことです。理論上は数千年に一度しか起きないはずの暴落が、実際には数年に一度やってきます。しかもその外れ方には明確な方向性があります。上げより下げのほうが激しいのです。

この記事では、その「正規分布からのズレ」を歪度(わいど)尖度(せんど)という2つの数値で捉える方法を扱います。Pythonで日本株のデータを実際に計算し、銘柄ごとの「暴落しやすさ」を数値で比較できるところまで持っていきます。

※本記事のコードや情報は執筆時点の仕様に基づいています。投資は自己責任であり、必ずデモ環境や少額資金でテストした上で運用してください。

平均と標準偏差だけでは分布は決まらない

まず前提の整理です。データの姿を語るとき、多くの人は平均と標準偏差で止まります。しかしこの2つが同じでも、まったく違う形の分布になり得ます

統計量何を表すか正規分布の値
平均(1次)中心の位置
標準偏差(2次)広がりの大きさ
歪度(3次)左右の偏り0
尖度(4次)裾の厚さ0(超過尖度)

投資家にとって重要なのは3番目と4番目です。歪度は「上げと下げ、どちらが激しいか」を、尖度は「極端な日がどれくらい多いか」を教えてくれます。どちらもリスク管理に直結する情報です。

身近な例で考えてみます。年率ボラティリティが同じ25%の銘柄が2つあったとして、片方は毎日コツコツ1.5%ずつ揺れる銘柄、もう片方は普段ほとんど動かないのに月に一度10%動く銘柄だとします。標準偏差では両者を区別できませんが、投資家にとってはまるで別物です。

この差を捉えるのが尖度です。後者のような「静かだが時々暴れる」銘柄は尖度が高く出ます。そして損切りが機能しにくいのは、決まって後者のタイプです。逆指値を置いていても、一気に飛ばれてしまうためです。

同じように、歪度は「上と下、どちらに飛びやすいか」を教えてくれます。両方を合わせて見ることで、「この銘柄はどちら側にどう裏切ってくるか」が事前に分かるようになります。

📘 外部参考Skewness(Investopedia)ファット・テール(野村證券)

歪度:上げと下げ、どちらが荒いか

歪度は分布の左右対称性を測ります。値の符号で、次のように解釈します。

  • 歪度がマイナス: 左(下落側)に長い裾。じわじわ上げて急に落ちる
  • 歪度が0: 左右対称。上げ下げが同じ性格
  • 歪度がプラス: 右(上昇側)に長い裾。じわじわ下げて急に上がる

株価指数の日次リターンは、たいていマイナスの歪度を示します。これは相場格言の「上げ100日、下げ3日」を数値で裏づけたようなもので、恐怖による売りのほうが楽観による買いより急激だという市場の性質を反映しています。

pip install yfinance pandas numpy scipy matplotlib
import yfinance as yf
import numpy as np
import pandas as pd
from scipy import stats

px = yf.download("^N225", period="10y", auto_adjust=True,
                 progress=False)["Close"].dropna()
ret = px.pct_change().dropna()

skew = float(stats.skew(ret))
print(f"歪度 : {skew:>7.3f}")

if skew < -0.3:
    print("→ 下落方向に裾が長い。急落が起きやすい形です")
elif skew > 0.3:
    print("→ 上昇方向に裾が長い。急騰が起きやすい形です")
else:
    print("→ ほぼ左右対称です")

# 実際に上位・下位の日を比べて確かめる
worst = ret.nsmallest(10)
best = ret.nlargest(10)
print(f"\n下落トップ10の平均 : {float(worst.mean()) * 100:>6.2f} %")
print(f"上昇トップ10の平均 : {float(best.mean()) * 100:>6.2f} %")
print(f"最悪の1日          : {float(ret.min()) * 100:>6.2f} %")
print(f"最良の1日          : {float(ret.max()) * 100:>6.2f} %")

下落トップ10の絶対値が上昇トップ10を上回っていれば、それが歪度のマイナスとして数値化されているわけです。「悪い日は良い日より悪い」——感覚的に知っていたことが、1つの数字で確認できます。

歪度が投資判断に効いてくる場面

歪度がマイナスの資産には、実務上いくつかの注意点があります。

  • 損切りが間に合わない: 急落は一気に来るため、想定価格で約定しないことがある
  • レバレッジと相性が悪い: 一度の急落で証拠金維持率を割り込みやすい
  • 標準偏差では危険が見えない: 上下を同じに扱うため、下方向の激しさが埋もれる

逆に歪度がプラスの資産は、「小さく負けて、たまに大きく勝つ」という形になります。トレンドフォロー戦略の損益がまさにこの形で、勝率は低いのに期待値はプラス、という特徴的な性格を持ちます。

尖度:極端な日はどれくらい多いのか

尖度は分布の裾の厚さを測ります。正規分布との比較で考えるため、超過尖度(正規分布を0とした値)を使うのが一般的です。

kurt = float(stats.kurtosis(ret))   # 既定で超過尖度を返す

print(f"超過尖度 : {kurt:>7.3f}   (正規分布なら 0)")
if kurt > 3:
    print("→ 明確なファットテール。極端な日が理論よりずっと多い")
elif kurt > 1:
    print("→ やや裾が厚い")

# 正規分布であるという仮説を検定する
jb_stat, jb_p = stats.jarque_bera(ret)
print(f"\nJarque-Bera検定 p値: {jb_p:.3e}")
print("→ 正規分布という仮説は棄却されます" if jb_p < 0.05
      else "→ 正規分布と矛盾しません")

日経平均で計算すると、超過尖度は5〜10程度になります。正規分布なら0のはずですから、かなり大きく外れていることになります。Jarque-Bera検定のp値も極めて小さくなり、「正規分布である」という仮説は明確に棄却されます。

どれくらいズレるのか、回数で数える

尖度という数値だけでは実感が湧きにくいので、「何回起きたか」で確認します。ここが本記事でいちばん見てほしい部分です。

from scipy.stats import norm

z = (ret - ret.mean()) / ret.std()
n = len(z)

print(f"検証期間: {ret.index[0].date()} 〜 {ret.index[-1].date()} "
      f"({n:,}営業日)\n")
print(f"{'閾値':>6s} {'実際':>7s} {'理論値':>10s} {'倍率':>9s} {'理論上の頻度':>16s}")

for k in (2, 3, 4, 5, 6):
    actual = int((z.abs() > k).sum())
    expected = 2 * norm.sf(k) * n
    ratio = actual / expected if expected > 0 else float("inf")
    # 正規分布ならこの頻度で起きるはず
    years = 1 / (2 * norm.sf(k) * 252) if norm.sf(k) > 0 else float("inf")
    freq = f"{years:,.0f}年に1回" if years < 1e7 else "事実上ゼロ"
    print(f"±{k}σ {actual:>7d} {expected:>10.3f} {ratio:>8.1f}倍 {freq:>16s}")

この表を見ると、正規分布の仮定がいかに現実離れしているかが分かります。±5σは正規分布なら数千年に一度の出来事ですが、10年程度のデータでも実際に何度か観測されます。

「1987年のブラックマンデーは正規分布では宇宙の年齢より長い間隔でしか起きない」という有名な指摘がありますが、それが起きた以上、間違っているのは現実ではなくモデルのほうだということになります。

この事実がリスク計算にどう影響するかは、正規分布でVaR計算したら暴落で当たらなかった話で具体的に検証しています。

分布を目で見て確かめる

数値だけでなくグラフでも確認しておきます。ヒストグラムに正規分布を重ねると、ズレの正体が見えてきます。

import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 2, figsize=(13, 5))

# 左: ヒストグラムと正規分布の重ね合わせ
r = ret * 100
axes[0].hist(r, bins=120, density=True, alpha=0.65, edgecolor="none")
x = np.linspace(float(r.min()), float(r.max()), 400)
axes[0].plot(x, norm.pdf(x, float(r.mean()), float(r.std())),
             linewidth=2, label="正規分布")
axes[0].set_yscale("log")          # 対数軸にすると裾の差が見える
axes[0].set_xlabel("日次リターン (%)")
axes[0].set_title("分布の比較(縦軸は対数)")
axes[0].legend()

# 右: Q-Qプロット
stats.probplot(ret, dist="norm", plot=axes[1])
axes[1].set_title("Q-Qプロット")
axes[1].grid(alpha=0.3)

plt.tight_layout()
plt.savefig("distribution_check.png", dpi=120)

ポイントは縦軸を対数にしているところです。通常のスケールでは中央付近の高い山に隠れて、いちばん見たい裾の部分が潰れてしまいます。対数にすると、実データの裾が正規分布の曲線より明らかに上にあることが分かります。

右のQ-Qプロットも有効です。データが正規分布に従っていれば点は直線上に並びますが、両端が直線から外れてS字を描く——これがファットテールの視覚的なサインです。

銘柄ごとの「暴落しやすさ」を比較する

ここからが実用パートです。歪度と尖度を銘柄ごとに計算すれば、どの銘柄が急落を起こしやすい性格なのかを横並びで比較できます。

WATCHLIST = {
    "7203.T": "トヨタ自動車", "6758.T": "ソニーG", "6501.T": "日立製作所",
    "9432.T": "NTT", "8306.T": "三菱UFJ", "8035.T": "東エレク",
    "6098.T": "リクルート", "4568.T": "第一三共", "^N225": "日経平均",
}

rows = []
for code, name in WATCHLIST.items():
    try:
        r = yf.download(code, period="5y", auto_adjust=True,
                        progress=False)["Close"].pct_change().dropna()
        if len(r) < 500:
            continue
        z = (r - r.mean()) / r.std()
        rows.append({
            "銘柄": name,
            "年率ボラ": round(float(r.std()) * np.sqrt(252) * 100, 1),
            "歪度": round(float(stats.skew(r)), 2),
            "尖度": round(float(stats.kurtosis(r)), 1),
            "-3σ超え回数": int((z < -3).sum()),
            "最悪日%": round(float(r.min()) * 100, 1),
        })
    except Exception:
        continue

df = pd.DataFrame(rows).sort_values("尖度", ascending=False)
print(df.to_string(index=False))

この表の読み方が重要です。年率ボラティリティが同じくらいでも、尖度が大きく違う銘柄があります。尖度が高い銘柄は、普段は穏やかなのに突然大きく動くタイプ——つまり油断しやすく、その分だけ危険です。

歪度尖度性格注意点
0付近低い素直で穏やか標準偏差での管理が有効
マイナス大高い急落型損切りが滑る。レバレッジ注意
プラス大高い急騰型空売りが危険
0付近高い両方向に急変イベントドリブンな値動き

標準偏差だけを見て銘柄を選んでいると、この違いはまったく見えません。標準偏差を使って投資リスクを数値化する方法と組み合わせて、「広がり」と「裾の形」の両方を見るようにすると精度が上がります。

尖度は時期によって変化する

ここも見落としやすいポイントです。歪度も尖度も固定値ではなく、相場環境によって変わります。ローリング計算で推移を追うと、その変化が見えてきます。

WINDOW = 250   # 約1年

roll = pd.DataFrame({
    "歪度": ret.rolling(WINDOW).skew(),
    "尖度": ret.rolling(WINDOW).kurt(),
    "ボラ": ret.rolling(WINDOW).std() * np.sqrt(252) * 100,
}).dropna()

print(roll.tail(5).round(2).to_string())
print(f"\n歪度の範囲 : {float(roll['歪度'].min()):>6.2f} 〜 "
      f"{float(roll['歪度'].max()):.2f}")
print(f"尖度の範囲 : {float(roll['尖度'].min()):>6.2f} 〜 "
      f"{float(roll['尖度'].max()):.2f}")

# 尖度が高まっている局面は「不安定化のサイン」として使える
recent = float(roll["尖度"].iloc[-1])
avg = float(roll["尖度"].mean())
print(f"\n現在の尖度 {recent:.2f} / 平均 {avg:.2f}")
if recent > avg * 1.5:
    print("⚠ 平常時より裾が厚くなっています。急変への備えを厚めに")

暴落が起きた直後は、当然ながら尖度が跳ね上がります。興味深いのはその状態がしばらく続くことで、これは「荒れた相場は荒れたまま続きやすい」というボラティリティのクラスタリング性と同じ現象です。

実務的な使い方としては、尖度が平常時より大きく高まっている局面ではポジションを小さくするという運用が考えられます。「いつもより何が起きてもおかしくない状態」を数値で検知できるわけです。

計算するときの注意点

サンプル数が少ないと当てにならない

歪度は3乗、尖度は4乗の計算を含むため、外れ値1つで値が大きく動きます。データが少ないと、たまたま入っていた急落日1つで結論が変わってしまいます。

# 期間の長さで値がどう変わるか
for period in ("6mo", "1y", "2y", "5y", "10y"):
    r = yf.download("^N225", period=period, auto_adjust=True,
                    progress=False)["Close"].pct_change().dropna()
    print(f"{period:>5s} (n={len(r):>5,d}) → "
          f"歪度 {float(stats.skew(r)):>6.2f} / "
          f"尖度 {float(stats.kurtosis(r)):>6.2f}")

短い期間では値が大きくぶれることが確認できます。最低でも2年、できれば5年以上のデータで計算してください。

「超過尖度」か「そのままの尖度」か

紛らわしい点です。正規分布の尖度は数学的には3ですが、そこから3を引いた「超過尖度」を使うのが一般的で、その場合は正規分布が0になります。

print(f"scipy (既定)      : {float(stats.kurtosis(ret)):.3f}  ← 超過尖度")
print(f"scipy (fisher=False): {float(stats.kurtosis(ret, fisher=False)):.3f}"
      "  ← そのままの尖度")
print(f"pandas .kurt()    : {float(ret.kurt()):.3f}  ← 超過尖度")

scipyもpandasも既定では超過尖度を返します。他の資料の数値と比べるときは、どちらの定義かを必ず確認してください。3の差は決して小さくありません。

日次と月次では結果が変わる

集計の頻度を落とすと、極端な1日が平均化されて尖度は小さくなります。「月次で見れば正規分布に近い」というのは事実ですが、日々のリスク管理をしている人にとっては慰めになりません。

daily = px.pct_change().dropna()
weekly = px.resample("W").last().pct_change().dropna()
monthly = px.resample("ME").last().pct_change().dropna()

for label, r in [("日次", daily), ("週次", weekly), ("月次", monthly)]:
    print(f"{label} 歪度 {float(stats.skew(r)):>6.2f} / "
          f"尖度 {float(stats.kurtosis(r)):>6.2f} (n={len(r):,})")

自分の保有期間に合った頻度で計算するのが原則です。デイトレードをしているのに月次で分布を見ても意味がありません。

まとめ

歪度と尖度で「正規分布からのズレ」を数値化する方法を見てきました。要点を整理します。

  • 平均と標準偏差だけでは分布の姿は決まらない。歪度と尖度で形が分かる
  • 歪度がマイナス=下落方向に裾が長い。株価指数はほぼこの形
  • 超過尖度が5〜10(正規分布は0)。日経平均は明確なファットテール
  • ±5σの暴落は理論上ほぼ起きないはずだが、実際には数年に一度起きる
  • ボラティリティが同じでも尖度は銘柄ごとに違う。高尖度=油断しやすい銘柄
  • 尖度は時期によって変化する。平常時の1.5倍を超えたら警戒水準
  • 計算は最低2年分。超過尖度かどうかの定義の違いにも注意する

この記事で伝えたかったのは、「正規分布は便利だが、いちばん知りたい部分で嘘をつく」ということです。平常時の値動きを語るには十分ですが、資産を守るために本当に知りたい「最悪の日」については、体系的に過小評価します。

とはいえ、正規分布を捨てる必要はありません。「便利な近似だが、テールでは信用しない」と線を引いて付き合えば十分です。ブラック・ショールズもVaRも、その限界を理解した上で使えば依然として有用な道具です。

まずは自分の保有銘柄で歪度と尖度を計算してみてください。尖度が10を超えるような銘柄を持っているなら、ポジションサイズを一段下げるだけで、想定外の事態に遭う確率をかなり減らせます。計算自体は stats.skew()stats.kurtosis() の2行で終わるので、試すコストはほぼゼロです。

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