※本記事のコードや情報は執筆時点の仕様に基づいています。投資は自己責任であり、必ずデモ環境や少額資金でテストした上で運用してください。
「日本株の平均リターンは年間◯%です」——こうした説明を見たとき、その数字を鵜呑みにしてよいかどうかを判断できるでしょうか。結論から書くと、株式のリターンについて「平均」だけを見るのは、かなり危険です。
理由は単純で、株のリターン分布は右に長い裾を引いた、大きく歪んだ形をしているからです。株価は最大でも−100%までしか下がりませんが、上は理論上無限に伸びます。この非対称性のせいで、ごく一部の大化け銘柄が平均値を上に引っ張り上げてしまいます。
この記事では、平均値・中央値・最頻値という3つの代表値をPythonで計算し、実際の日本株データで3つがどれだけズレるのかを確認します。そのうえで、投資判断の場面ごとにどれを見るべきかを整理します。統計の教科書的な説明ではなく、「銘柄選びで騙されないための道具」として扱っていきます。
📘 外部参考:代表値(Wikipedia) / Mean(Investopedia)
3つの代表値は「真ん中」の定義が違う
代表値とは、たくさんの数字を1つの値で要約したものです。よく使われるのは次の3つですが、それぞれ「真ん中」の定義が異なります。
| 代表値 | 定義 | 外れ値の影響 | 投資での使いどころ |
|---|---|---|---|
| 平均値 | 全部を足して個数で割る | 受けやすい | 期待値の計算、ポートフォリオ全体 |
| 中央値 | 並べたときの真ん中の値 | 受けにくい | 典型的な銘柄の姿を知る |
| 最頻値 | 最も多く現れる値 | 受けない | いちばんありがちな結果を知る |
データが左右対称なら、3つはほぼ同じ値になります。問題は、株式のリターンが対称ではないことです。分布が歪んでいると、3つは次のような並び方をします。
- 右に裾が長い(正の歪み): 最頻値 < 中央値 < 平均値 ——株式リターンはこちら
- 左右対称: 最頻値 ≒ 中央値 ≒ 平均値
- 左に裾が長い(負の歪み): 平均値 < 中央値 < 最頻値 ——日次リターンはこちらに寄る
興味深いのは、期間の取り方で歪みの向きが変わる点です。年単位の長期リターンは大化け銘柄の影響で右に歪みますが、日次リターンは暴落日の影響で左に歪みます。同じ株価データでも、見る時間軸で分布の姿が変わるわけです。
なぜこの違いが生まれるのかというと、短期と長期では効いてくる非対称性の種類が違うからです。1日単位では、パニック売りによる急落のほうが買い上がりより激しくなりやすい。一方で数年単位では、株価が10倍になる銘柄は存在しても、−200%になる銘柄は存在しません。時間が経つほど「上は青天井」という性質が効いてくるわけです。
この2つの歪みは、投資家として気をつけるポイントも変わってきます。
- 短期(左の歪み): 想定より大きな下落が起こりうる。損切りとポジションサイズで備える
- 長期(右の歪み): 平均リターンは少数の銘柄に依存する。分散しないと平均に届かない
Pythonで3つの代表値を計算する
環境を準備します。
pip install yfinance pandas numpy scipy matplotlib
まずは1銘柄の日次リターンで、3つを並べてみます。
import yfinance as yf
import numpy as np
import pandas as pd
df = yf.download("7203.T", period="3y", auto_adjust=True, progress=False)
ret = df["Close"].pct_change().dropna() * 100 # %表示
mean = ret.mean()
median = ret.median()
# 最頻値は連続データなので、区間に区切って最も多い階級を採用する
counts, bins = np.histogram(ret, bins=50)
idx = counts.argmax()
mode = (bins[idx] + bins[idx + 1]) / 2
print(f"データ件数 : {len(ret):>8,d} 日")
print(f"平均値 : {float(mean):>8.4f} %")
print(f"中央値 : {float(median):>8.4f} %")
print(f"最頻値 : {mode:>8.4f} %")
print(f"平均-中央値: {float(mean - median):>8.4f} ポイント")
ここで押さえておきたいのが最頻値の扱いです。日次リターンのような連続値では、まったく同じ数字が2回現れることはほぼありません。そのため scipy.stats.mode() をそのまま使っても意味のある結果は返ってきません。ヒストグラムの階級に区切って、最も度数が多い区間の中心を最頻値とするのが実務的な方法です。
階級の数で最頻値は変わる
この方法には弱点があり、区切り方によって答えが変わります。実際に確認してみましょう。
for n_bins in (10, 20, 30, 50, 100):
counts, bins = np.histogram(ret, bins=n_bins)
i = counts.argmax()
center = (bins[i] + bins[i + 1]) / 2
print(f"階級数 {n_bins:>3d} → 最頻値 {center:>7.4f} % "
f"(階級幅 {bins[1] - bins[0]:.3f})")
階級数を変えると最頻値がぶれることが分かります。だからこそ、投資判断の主役として最頻値を使うのはおすすめしません。「だいたいこのあたりに集中している」という補助的な情報として捉えるのが適切です。主役は平均値と中央値の2つで十分です。
日経225全銘柄で「平均の嘘」を確認する
ここが本記事の核心です。1銘柄の日次リターンでは差が小さくて実感しづらいので、多数の銘柄の年間リターンを集めて分布を見ます。「日本株の平均リターンは◯%」という表現が何を隠しているかが、はっきり見えてきます。
import yfinance as yf
import numpy as np
import pandas as pd
# 主要銘柄のサンプル(実際は日経225の全構成銘柄で試すとより明確になります)
TICKERS = [
"7203.T", "6758.T", "6501.T", "9432.T", "8306.T", "6098.T",
"9983.T", "4063.T", "6367.T", "8035.T", "7974.T", "6902.T",
"4568.T", "8058.T", "6503.T", "7267.T", "9433.T", "4502.T",
"6702.T", "7751.T", "6301.T", "8001.T", "4661.T", "6981.T",
]
returns = {}
for code in TICKERS:
try:
px = yf.download(code, period="1y", auto_adjust=True,
progress=False)["Close"].dropna()
if len(px) < 200:
continue
returns[code] = (float(px.iloc[-1]) / float(px.iloc[0]) - 1) * 100
except Exception:
continue
s = pd.Series(returns)
counts, bins = np.histogram(s, bins=12)
i = counts.argmax()
print(f"対象銘柄数 : {len(s)} 銘柄")
print(f"平均値 : {s.mean():>7.1f} % ← よく引用される数字")
print(f"中央値 : {s.median():>7.1f} % ← 真ん中の銘柄の実力")
print(f"最頻値 : {(bins[i] + bins[i + 1]) / 2:>7.1f} % ← いちばんありがちな結果")
print(f"\n平均を上回った銘柄: {(s > s.mean()).sum()} / {len(s)} 銘柄 "
f"({(s > s.mean()).mean() * 100:.0f}%)")
print(f"最も上がった銘柄 : {s.idxmax()} {s.max():>7.1f} %")
print(f"最も下がった銘柄 : {s.idxmin()} {s.min():>7.1f} %")
注目すべきは最後から3行目、「平均を上回った銘柄の割合」です。分布が右に歪んでいるとき、この割合は50%を大きく下回ります。つまり平均は「半数の銘柄が達成できる水準」ではないということです。
海外の調査でも同じ傾向が報告されています。ある年の豪州株指数の構成銘柄では、平均リターン34%に対し中央値21%、最頻値10%という結果でした。平均だけを見て「34%取れるのが普通」と考えると、実態とはまるで違う期待を持つことになります。
分布を目で見る
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(11, 5))
ax.hist(s, bins=12, edgecolor="white", alpha=0.85)
ax.axvline(s.mean(), color="crimson", linestyle="--", linewidth=2,
label=f"平均 {s.mean():.1f}%")
ax.axvline(s.median(), color="seagreen", linestyle="-", linewidth=2,
label=f"中央値 {s.median():.1f}%")
ax.set_xlabel("年間リターン (%)")
ax.set_ylabel("銘柄数")
ax.set_title("銘柄別の年間リターン分布")
ax.legend()
ax.grid(alpha=0.3, axis="y")
plt.tight_layout()
plt.savefig("return_distribution.png", dpi=120)
グラフにすると、平均線が山の頂上より右にあることが視覚的に分かります。右端に少数の大勝ち銘柄がいて、それが平均を引き上げている構図です。
この歪みの大きさそのものを数値化したのが歪度という指標です。詳しくは歪度・尖度で「暴落しやすさ」を数値化する方法で扱っています。
この事実が投資判断に効いてくる場面
個別株を選ぶことの難しさ
「平均を上回る銘柄は半分より少ない」という事実は、ランダムに銘柄を選ぶと平均以下になる確率のほうが高いことを意味します。市場全体の上昇は、一部の大化け銘柄が牽引しているためです。
インデックス投資が推奨される統計的な根拠は、まさにここにあります。指数を買えば、その少数の大化け銘柄を必ず保有できるからです。個別株で勝負するなら、その少数を引き当てる必要があります。
バックテスト結果の読み方
戦略の成績を評価するときも、平均だけでは危険です。1回の大勝ちが平均を押し上げているケースがあるからです。
def trade_summary(pnl):
"""トレード損益から、平均に頼らない要約を作る"""
pnl = pd.Series(pnl)
top = pnl.nlargest(1).iloc[0]
print(f"トレード数 : {len(pnl):>10,d}")
print(f"平均損益 : {pnl.mean():>10,.0f} 円")
print(f"中央値 : {pnl.median():>10,.0f} 円")
print(f"勝率 : {(pnl > 0).mean() * 100:>10.1f} %")
print(f"最大の1勝ち : {top:>10,.0f} 円")
print(f"最大勝ちを除く平均: {pnl.drop(pnl.idxmax()).mean():>8,.0f} 円")
if pnl.median() <= 0 < pnl.mean():
print("\n⚠ 中央値がマイナス。少数の大勝ちに依存した戦略です")
# 例: 少数の大勝ちに支えられた損益系列
sample = [-3000, -2500, -1800, -1200, 800, 1500, -2000, -900, 95000, -1500]
trade_summary(sample)
この例では平均こそプラスですが、中央値はマイナスです。つまり「ほとんどのトレードは負けているが、1回の大勝ちで帳尻が合っている」状態を意味します。
こうした戦略が悪いわけではありません。トレンドフォロー系は本質的にこの形になります。ただしその大勝ちを取り逃すと一気に赤字転落するという脆さを抱えていることは、認識しておく必要があります。「最大勝ちを除く平均」を必ず併記するようにすると、この依存度が見えるようになります。
戦略評価の指標としては、プロフィットファクターを計算する方法やシャープレシオを使って投資戦略を評価する方法もあわせて使うと、多面的に判断できます。
配当利回りや指標のスクリーニング
PERやPBR、配当利回りといった指標でスクリーニングするときも、業種平均には極端な値が混ざっていることがあります。赤字転落でPERが異常値になった銘柄が1社あるだけで、業種平均は使い物にならなくなります。
# 外れ値に強い要約を作る
def robust_summary(series, name=""):
s = pd.Series(series).dropna()
q1, q3 = s.quantile(0.25), s.quantile(0.75)
iqr = q3 - q1
outliers = s[(s < q1 - 1.5 * iqr) | (s > q3 + 1.5 * iqr)]
print(f"--- {name} ---")
print(f" 平均 : {s.mean():>8.2f} 中央値 : {s.median():>8.2f}")
print(f" 第1四分位: {q1:>6.2f} 第3四分位: {q3:>6.2f}")
print(f" 外れ値 : {len(outliers)} 件 {list(outliers.round(1))[:5]}")
if len(outliers):
print(f" 外れ値を除いた平均: {s.drop(outliers.index).mean():.2f}")
robust_summary([12.3, 14.1, 11.8, 13.5, 15.2, 210.0, 12.9, 13.1], "PER")
四分位範囲(IQR)を使った外れ値検出は、実務でよく使う手法です。中央値と四分位数をセットで見る習慣をつけておくと、極端な値に振り回されなくなります。
スクリーニング条件を作るときも、固定値より分位点で指定するほうが頑健です。「PER15倍以下」と決め打ちすると相場全体が高くなった局面で候補がゼロになりますが、「下位25%」なら常に一定数が残ります。
# 固定値ではなく分位点でスクリーニングする
per = pd.Series({
"A社": 12.3, "B社": 14.1, "C社": 11.8, "D社": 13.5,
"E社": 15.2, "F社": 210.0, "G社": 12.9, "H社": 13.1,
})
fixed = per[per <= 13.0] # 固定値: 相場次第で候補が消える
relative = per[per <= per.quantile(0.25)] # 相対: 常に下位25%が残る
print(f"固定値 (PER13以下) : {list(fixed.index)}")
print(f"分位点 (下位25%) : {list(relative.index)}")
print(f"※F社(PER210)は平均を{per.mean() - per.drop('F社').mean():.1f}倍押し上げている")
結局どれを見ればよいのか
場面ごとに使い分けるのが正解です。目的別に整理すると次のようになります。
| 知りたいこと | 見るべき代表値 | 理由 |
|---|---|---|
| ポートフォリオ全体の期待収益 | 平均値 | 合計を反映するため。分散投資が前提 |
| 典型的な1銘柄の姿 | 中央値 | 外れ値に引きずられない |
| 戦略の安定性 | 平均値と中央値の両方 | 差が大きければ少数依存を疑える |
| いちばんありがちな結果 | 最頻値 | ただし階級の切り方で変わる |
実務的には、平均値と中央値を必ず並べて出すのが最も費用対効果の高い習慣です。差が小さければ分布は素直で、平均を信じて構いません。差が大きければ、そこに何かが隠れています。
差がどの程度なら「大きい」と言えるのかは、対象によって変わります。おおまかな目安は次のとおりです。
- 日次リターン: 平均と中央値の差は通常ごくわずか。目に見えて開いていたら期間内に急変があった証拠
- 銘柄別の年間リターン: 平均が中央値より5〜15ポイント高いのはごく普通。歪みは織り込んで読む
- トレード損益: 中央値がマイナスで平均がプラスなら要注意。少数依存を疑う
def describe_returns(series, label=""):
"""平均と中央値の乖離から、分布の素直さを判定する"""
s = pd.Series(series).dropna()
mean, median = s.mean(), s.median()
gap = (mean - median) / (abs(median) + 1e-9)
print(f"[{label}] 平均 {mean:.2f} / 中央値 {median:.2f}")
if abs(gap) < 0.10:
print(" → 分布は素直。平均をそのまま使ってよい")
elif gap > 0:
print(" → 右に歪んでいる。少数の大勝ちが平均を押し上げている")
else:
print(" → 左に歪んでいる。少数の大負けが平均を押し下げている")
代表値だけでは足りない——四分位数で分布を要約する
平均値と中央値を並べれば分布の歪みには気づけますが、「どこからどこまで散らばっているか」までは分かりません。そこで併用したいのが四分位数です。データを小さい順に並べ、4等分する位置の値を見ます。
| 指標 | 位置 | 読み方 |
|---|---|---|
| 最小値 | 0% | 最悪だった銘柄・期間 |
| 第1四分位(Q1) | 25% | 下位4分の1の境目 |
| 中央値(Q2) | 50% | ちょうど真ん中 |
| 第3四分位(Q3) | 75% | 上位4分の1の境目 |
| 最大値 | 100% | 最も良かった銘柄・期間 |
pandasなら .describe() 1つで全部出ます。ただし既定では四分位しか出ないため、裾の情報を追加すると実用性が上がります。
import pandas as pd
import numpy as np
def full_summary(series, label=""):
"""代表値+四分位+裾の情報をまとめて出す"""
s = pd.Series(series).dropna()
q = s.quantile([0.01, 0.05, 0.25, 0.50, 0.75, 0.95, 0.99])
print(f"===== {label} (n={len(s):,}) =====")
print(f" 平均値 : {s.mean():>8.2f}")
print(f" 中央値 : {s.median():>8.2f}")
print(f" 標準偏差 : {s.std():>8.2f}")
print(" --- 分位点 ---")
for p, v in q.items():
mark = " ←中央値" if p == 0.50 else ""
print(f" {p * 100:>5.0f}% : {v:>8.2f}{mark}")
print(f" --- 幅 ---")
print(f" IQR (Q3-Q1) : {q[0.75] - q[0.25]:>8.2f}")
print(f" 下位5%〜上位5%: {q[0.05]:>8.2f} 〜 {q[0.95]:.2f}")
px = yf.download("7203.T", period="3y", auto_adjust=True,
progress=False)["Close"]
full_summary(px.pct_change().dropna() * 100, "トヨタ日次リターン(%)")
ここで見るべきは1%点と99%点の非対称性です。左右対称な分布なら、この2つは中央値からほぼ等距離になります。実際の株価データでは下側のほうが遠いことが多く、これは「悪い日は良い日より激しい」という現実を反映しています。
箱ひげ図で複数銘柄を比較する
四分位数を可視化したものが箱ひげ図です。複数銘柄を並べると、中心・ばらつき・外れ値を一度に比較できます。
import matplotlib.pyplot as plt
CODES = {"7203.T": "トヨタ", "6758.T": "ソニー",
"9432.T": "NTT", "6098.T": "リクルート"}
data, labels = [], []
for code, name in CODES.items():
r = yf.download(code, period="2y", auto_adjust=True,
progress=False)["Close"].pct_change().dropna() * 100
data.append(r.values.ravel())
labels.append(name)
fig, ax = plt.subplots(figsize=(9, 5))
ax.boxplot(data, tick_labels=labels, showfliers=True)
ax.axhline(0, color="gray", linewidth=0.8)
ax.set_ylabel("日次リターン (%)")
ax.set_title("銘柄別の日次リターン分布")
ax.grid(alpha=0.3, axis="y")
plt.tight_layout()
plt.savefig("boxplot_returns.png", dpi=120)
箱(Q1〜Q3)の高さがそのまま値動きの荒さを表し、ひげの外側の点が外れ値です。箱は小さいのに外れ値が多い銘柄は、普段は静かなのにたまに急変するタイプで、油断すると痛い目に遭います。標準偏差だけを見ていると、この性格の違いは見えてきません。
計算でつまずきやすいところ
リターンの平均には2種類ある
意外な落とし穴がこれです。年ごとのリターンを単純に平均した値(算術平均)は、実際に手元に残る成績とは一致しません。複利で効いてくるためです。
yearly = [0.50, -0.40] # 1年目 +50%、2年目 -40%
arithmetic = np.mean(yearly)
geometric = np.prod([1 + r for r in yearly]) ** (1 / len(yearly)) - 1
print(f"算術平均 : {arithmetic * 100:>6.1f} % ← 見た目はプラス")
print(f"幾何平均 : {geometric * 100:>6.1f} % ← 実際の年平均リターン")
print(f"100万円が最終的に: {100 * 1.5 * 0.6:>6.1f} 万円")
算術平均では+5%とプラスに見えますが、実際には100万円が90万円に減っています。複数年のリターンを要約するときは幾何平均を使ってください。この差は変動が大きいほど広がります。
欠損値と極端値の扱い
pandasの .mean() は既定でNaNを除外して計算しますが、NumPyの np.mean() はNaNがあると結果全体がNaNになります。データを結合したあとは .dropna() を挟む習慣をつけておくと安全です。
また、株式分割を調整していないデータには−50%や+100%といった見せかけの極端値が混ざります。これがあると平均も中央値も狂うため、auto_adjust=True で調整済み株価を取得することが前提になります。
生存者バイアスに注意する
現在の構成銘柄だけを対象に過去のリターンを集計すると、途中で上場廃止になった銘柄が最初から存在しないことになります。倒産した企業は当然リストにいないため、平均も中央値も実態より良い数字になります。
厳密な検証をするなら、その時点で実際に存在した銘柄リスト(ヒストリカル・コンスティチュエント)が必要です。個人が入手するのは簡単ではないため、まずは「自分の集計は上振れしている可能性がある」と自覚しておくことが現実的な対処になります。
よくある疑問
中央値のほうが優れているなら、平均は不要では
そうはなりません。平均には「足し合わせられる」という決定的な長所があります。10銘柄を等金額で持ったときのポートフォリオ全体のリターンは、各銘柄のリターンの平均になります。中央値ではこの計算ができません。
- すでに分散して持っている → 平均が実態に近い
- これから1銘柄選ぼうとしている → 中央値が実態に近い
この使い分けが本質です。「平均は嘘つき」ではなく、1銘柄しか持たない人にとって平均は無関係というだけの話です。
外れ値は除外して計算すべきか
安易に除外してはいけません。株価データにおける外れ値は、入力ミスではなく実際に起きた出来事だからです。暴落日を「異常値だから」と削除したデータで計算したリスク指標は、本当に必要なときに役立ちません。
除外してよいのは、明らかなデータ不備のときだけです。株式分割の未調整による見せかけの−50%、休場日に紛れ込んだ0円の終値——こうしたものは削除すべき異常値です。「実際に起きた極端な値」と「データの誤り」は、はっきり区別してください。
サンプル数はどれくらい必要か
目的によりますが、目安は次のとおりです。データが少ないほど、中央値のほうが安定した値を返します。
| 件数 | 信頼度 | コメント |
|---|---|---|
| 〜30 | 低い | 参考程度。1件の増減で結果が変わる |
| 30〜100 | そこそこ | 傾向は見える。断定は避ける |
| 100〜250 | 実用的 | 日次データなら半年〜1年分 |
| 250〜 | 十分 | ただし古すぎる期間は環境が違う |
まとめ
平均値・中央値・最頻値という3つの代表値を、株価データで実際に比べてきました。要点を整理します。
- 株のリターン分布は右に歪んでいる。下は−100%止まりだが上は青天井のため
- そのため平均を上回る銘柄は半数より少ない。平均は「普通に取れる水準」ではない
- 典型的な姿を知りたいなら中央値。ポートフォリオ全体の期待値なら平均値
- 最頻値は階級の切り方で変わるため、補助的な情報として扱う
- バックテストで中央値がマイナスなのに平均がプラスなら、少数の大勝ちに依存している
- 複数年のリターンを要約するときは幾何平均を使う。算術平均は実態より良く見える
統計を投資に持ち込むときの第一歩は、「平均を見たら、必ず中央値も出す」という習慣です。コードで言えば1行増えるだけですが、この1行があるかないかで、データから読み取れる情報量はまるで変わります。
もう1つ付け加えるなら、四分位数まで出しておくと分布の全体像がつかめます。平均・中央値・Q1・Q3の4つが揃えば、そのデータについて語るべきことのほとんどは語れます。pandasの .describe() を打つ習慣をつけるだけでも、見落としは大きく減ります。
次はばらつきの大きさに進みます。代表値が「どこが中心か」を教えるのに対し、標準偏差は「どれだけ広がっているか」を教えてくれます。この2つが揃って、はじめてデータの姿が見えてきます。

