株の「β値」をPythonで算出する（回帰）

## はじめに
企業の株価がどれだけ市場全体に連動しているかを測る指標として、「β値（ベータ値）」があります。IR資料や資本コスト（WACC）算出に欠かせないこの数値を、仕事で計算する必要が出てきたので、やってみました。

この記事では、Pythonと公開APIを使って、TOPIXとの過去60か月のリターンからβを計算する方法を解説（バフェットコードのβ値がそんな感じで算出されていた気がする）。
対象企業は東証プライム上場のサンリオ（8136）とし、必要なコードと解釈も説明します。

## β（ベータ）とは？
β値は、株式の市場感応度を表す指標です。

β = 1.0：市場（TOPIXなど）が+1%動いたとき、銘柄も+1%動く
β > 1.0：市場より大きく反応する（ボラティリティが高い）
β < 1.0：市場より小さく反応する（ディフェンシブ）

例えば、βが0.5であれば、TOPIXが+1%上昇したとき、対象銘柄は+0.5%程度の上昇が期待されます。

## 今回の目的とアプローチ
### 目的：
- 銘柄「サンリオ（8136）」のβ値を算出する
- TOPIXの月次リターン（過去60か月）との回帰分析でβを求める

### やること：

- Yahoo! FinanceとStooqから株価と指数データを取得
- 月末の終値に変換し、リターンを計算
- 単純な回帰（定数項＋TOPIX）を行い、β値を抽出

Yahoo! FinanceではTOPIX指数を取得できないようです。
そのためYahoo! Financeでサンリオの株価、Stooq からTOPIXを取得するという形式を取りました（認識間違っていたら指摘お願いします。とはいえ、なぜ金融データはこんなにもめんどくさい、整理されていないのか……）

## 実装コード（完全版）
以下は、β値を算出するため今回作成した全コードです。

```python:python
import yfinance as yf
import pandas as pd
import numpy as np
import statsmodels.api as sm
from pandas_datareader.data import DataReader

# 対象銘柄・指数
TICKER_STOCK = "8136.T"    # サンリオ
TICKER_TOPIX = "^TPX"      # TOPIX (Stooq)

# 分析期間（60か月）
START = "2020-05-01"
END   = "2025-06-01"

# 月末終値の取得関数
def fetch_month_end_series(ticker: str, source: str) -> pd.Series:
    if source == "yahoo":
        df = yf.download(ticker, start=START, end=END, progress=False)
        col = "Adj Close" if "Adj Close" in df.columns else "Close"
    elif source == "stooq":
        df = DataReader(ticker, "stooq", START, END).sort_index()
        col = "Close"
    else:
        raise ValueError("source must be 'yahoo' or 'stooq'")
    
    if df.empty:
        raise RuntimeError(f"取得失敗: {ticker}@{source}")
    
    return df[col].resample("ME").last()  # 各月の最後の営業日を抽出

# インデックス生成（2020-05～2025-05）
idx = pd.date_range("2020-05-31", "2025-05-31", freq="M")

# データ取得と整形
px_vis  = fetch_month_end_series(TICKER_STOCK, "yahoo").reindex(idx)
px_topx = fetch_month_end_series(TICKER_TOPIX, "stooq").reindex(idx)

# 結合・欠損除去
prices = pd.concat([px_vis, px_topx], axis=1)
prices.columns = ["Vis_px", "Topix_px"]
prices = prices.dropna()

# 月次リターン（単純リターン）
rets = prices.pct_change().dropna() # 例：(今月の株価 / 先月の株価) - 1
rets.columns = ["Vis_ret", "Topix_ret"]

# 回帰分析（定数項あり）
X = sm.add_constant(rets["Topix_ret"]) # 回帰式は y = α + βx + ε（切片付き単回帰）。add_constant()：α（切片）を回帰に含める
y = rets["Vis_ret"]
model = sm.OLS(y, X).fit() # 最小二乗法（OLS）で係数を推

# 結果表示
beta   = model.params["Topix_ret"]
alpha  = model.params["const"]
r2     = model.rsquared
p_beta = model.pvalues["Topix_ret"]

print("\n========== β (TOPIX) サマリー：2020-05～2025-05 ==========")
print(f"β (TOPIX) : {beta:.3f}  "
      f"(TOPIX +1% → 当社株 +{beta*100:.2f}%)  P値={p_beta:.3f}")
print(f"α（月次）  : {alpha*100:.2f}%")
print(f"説明力 R²   : {r2*100:.1f}%")
print(f"観測数      : {int(model.nobs)} ヶ月")
print("※ 欠損月は除去。単純リターン（%）を使用。")
print("===========================================================")
```

出力結果は以下のようになりました。

```text:output
========== β (TOPIX) サマリー：2020-05～2025-05 ==========
β (TOPIX) : 0.362  (TOPIX +1% → 当社株 +36.23%)  P値=0.419
α（月次）  : 4.32%
説明力 R²   : 1.1%
観測数      : 60 ヶ月
※ 欠損月は除去。単純リターン（%）を使用。
===========================================================
```

- β = 0.362：市場より控えめに動く銘柄
- R² = 1.1%：株価の1.1%は市場（TOPIX）で説明できる
- α = 4.32%：TOPIXが横ばいの月でも、株価は+4.32%上がる傾向

### β値 = 0.362
このβは、「TOPIXが1%上昇したとき、当社株は平均して+0.36%上昇する」という傾向を示します。
これは、回帰式 y = α + βx における傾き（β）であり、市場感応度を表します。

### P値（p-value） = 0.015
突然P値が出てきました。これは、
βが「統計的に意味のある傾向かどうか」を表す指標としてついでに算出しています。
P値が小さいほど、βの存在は偶然ではなく有意であるといえるのです。
P値の正当性については色々言われていますし、P値だけで本一冊書けるくらいかと思うので、ここでは割愛します。

通常、P < 0.05 を「有意」としますので、
今回の 0.015 は、95%信頼水準でβが統計的に有意であることを意味します。

## α（月次）: 0.80%
これも突然出てきて戸惑った人もいるでしょうが、これは、
TOPIXが±0%だった月に、当社株（サンリオ）が平均して+0.80%上昇するという傾向を示します。
回帰式の切片項（intercept）です。

「市場に影響されない自社要因によるリターン（アルファ）」と解釈されることもあります。
ただし統計的には、単に切片であり、因果関係は別途検証が必要です。

### 説明力 R² = 18.2%
これも突然出してすみません。
これは、モデルがどれくらい当社株の変動を説明できているかを表します。

R² = 1.0 に近いほど、説明力が高いです。
今回の R² = 18.2% は「TOPIXだけで説明できるのは約18%」ということ。
残りの82%は、自社要因・他市場・ノイズ（偶発要因）などということです。

### 観測数（nobs）= 59 ヶ月
実際に分析に使われたデータの有効な月数（行数）です。
欠損月やリターン計算できない月（初月）は除外済み。

60ヶ月 → 59ヶ月になってしまった理由としては。
pct_change() を取ると先頭1行は欠落しますので、
欠損値（NaN）がある月は .dropna() により除外、ということでしょう。

## 散布図＋回帰線で視覚的に確認
さらに、視覚化もしておきます。

```python:python
import matplotlib.pyplot as plt

plt.figure(figsize=(8,6))
plt.scatter(rets["Topix_ret"], rets["Vis_ret"], alpha=0.7, label="月次データ")

# 回帰線
x_line = np.linspace(rets["Topix_ret"].min(), rets["Topix_ret"].max(), 200)
plt.plot(x_line, alpha + beta * x_line, color="red", linewidth=2, label=f"回帰線 (β={beta:.2f})")

# 装飾
plt.title("サンリオ vs TOPIX 月次リターン散布図（2020-05～2025-05）")
plt.xlabel("TOPIX 月次リターン")
plt.ylabel("サンリオ 月次リターン")
plt.grid(True)
plt.legend()
plt.show()
```

結果は以下の通りです。

![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256394/c65ca6e6-2cc1-4b98-8af5-97788dac4eb6.png)

## ✅ まとめ
今回は、Pythonとオープンデータを活用して、TOPIXとの月次回帰によるβ値の計算を行いました。

### 実務応用の場面：
- IR資料や資本コスト（WACC）の算定
- 企業のボラティリティ比較
- ファンドのポートフォリオ管理（低β/高β銘柄の選別）

## 📌 おまけ：βが異なる？
いろんな方法や他のサイトと比べましたが、近い値は出つつも、同じ値にはなりませんでした。
理由としては、
- 指数の種類：TOPIX配当込み vs 通常TOPIX
- 補完方法の違い：前月値補完 or 欠損除外
- 起点のズレ：公式は「上場翌月」などで固定している可能性
- 回帰の形式：切片あり or なし

などなど、これらの理由で、他のサイトと異なるβになることもあると思いますので、
いろんなパターンで分析して検証した方がいいと思います。

## お疲れ様でした。
わからないところ、間違っているところ、もっといい方法がある場合は、コメントでもDMでも教えてください。優しくお願いn