※本記事のコードや情報は執筆時点の仕様に基づいています。投資は自己責任であり、必ずデモ環境や少額資金でテストした上で運用してください。
移動平均線やボリンジャーバンドでトレンドを判断する方法はすでに試している段階でしょう。しかし、それらのテクニカル指標は「過去N日間の平均」に基づくものであり、トレンドの「傾き」を定量的に示してはくれません。
単回帰分析(Simple Linear Regression)を使えば、株価の時系列データに直線を当てはめ、1日あたり何円上昇(または下降)しているかを数値で把握できます。
多くの初中級者が「回帰分析は難しそう」「機械学習の知識が必要では」と敬遠しています。しかし実際には、最小二乗法(Ordinary Least Squares:OLS)の基本を理解すれば、数行のコードでトレンドラインを引けます。
つまずきの原因は、統計学の教科書的な説明と実際の株価データへの適用方法が結びついていないことです。日付データの数値変換や、残差(Residual)の解釈方法が不明瞭なまま進めてしまうケースが大半です。
本記事では、単回帰分析の理論を株価分析の文脈で解説し、トレンドラインの描画から「現在の株価が回帰直線に対して割安か割高か」を判定するPythonコードまでを一気に提示します。
コードはすべてコピペで動く構成です。銘柄コードと期間を変えるだけで、任意の銘柄に適用できます。
単回帰分析の理論と株価分析への応用
最小二乗法の基本
単回帰分析は「y = a + bx」という直線モデルをデータに当てはめる手法です。ここでyは株価、xは時間(日数)、bが傾き(1日あたりの変化量)、aが切片です。
最小二乗法(OLS)は、すべてのデータ点と直線の距離(残差)の二乗和を最小化するようにaとbを決定します。この計算はPythonのscipyやscikit-learnで1行で実行できます。
株価分析で回帰直線が示すもの
回帰直線の傾きbがプラスなら上昇トレンド、マイナスなら下降トレンドです。傾きの絶対値がトレンドの強さを表します。
さらに重要なのは残差です。現在の株価が回帰直線より上にあれば「直線に対して割高」、下にあれば「割安」と判断できます。この乖離幅を標準偏差で正規化すれば、統計的にどの程度異常な位置にいるかを数値化できます。
決定係数R²の解釈
決定係数(Coefficient of Determination:R²)は、回帰直線がデータのばらつきをどの程度説明できているかを示す指標です。値は0〜1の範囲を取ります。
株価データではR²が0.7以上あればトレンドの方向性に一定の信頼が置けます。0.3未満の場合はレンジ相場(横ばい)の可能性が高く、回帰直線によるトレンド判断は機能しません。R²の値を必ず確認してから結果を解釈してください。
【コピペOK】単回帰分析によるトレンド判定コード
まず必要なライブラリをインストールしてください。
pip install pandas numpy yfinance matplotlib scipy scikit-learn
以下がメインコードです。
import numpy as np
import pandas as pd
import yfinance as yf
import matplotlib.pyplot as plt
from scipy import stats
from sklearn.linear_model import LinearRegression
from typing import Dict, Tuple
# ==============================
# 設定エリア
# ==============================
TICKER: str = "7203.T" # 銘柄コード(トヨタ自動車)
START_DATE: str = "2023-01-01" # データ取得開始日
END_DATE: str = "2024-12-31" # データ取得終了日
DEVIATION_THRESHOLD: float = 2.0 # 乖離判定の標準偏差倍率
USE_LOG_PRICE: bool = False # 対数価格を使用するか
# ==============================
# データ取得
# ==============================
def fetch_ohlcv(ticker: str, start: str, end: str) -> pd.DataFrame:
'指定銘柄のOHLCVデータを取得する'
df: pd.DataFrame = yf.download(ticker, start=start, end=end, auto_adjust=True)
df = df.droplevel("Ticker", axis=1) if isinstance(df.columns, pd.MultiIndex) else df
df.columns = [c.lower() for c in df.columns]
return df
# ==============================
# 回帰分析用のデータ準備
# ==============================
def prepare_regression_data(
df: pd.DataFrame, use_log: bool,
) -> Tuple[np.ndarray, np.ndarray, pd.DatetimeIndex]:
'日付を数値に変換し、説明変数と目的変数を返す'
dates: pd.DatetimeIndex = df.index
x: np.ndarray = np.arange(len(df)).reshape(-1, 1).astype(float)
y: np.ndarray = df["close"].values.astype(float)
if use_log:
y = np.log(y)
return x, y, dates
# ==============================
# 単回帰分析の実行
# ==============================
def run_linear_regression(
x: np.ndarray, y: np.ndarray,
) -> Dict[str, float]:
'最小二乗法で回帰直線を算出し、統計量を返す'
model: LinearRegression = LinearRegression()
model.fit(x, y)
y_pred: np.ndarray = model.predict(x)
residuals: np.ndarray = y - y_pred
slope: float = model.coef_[0]
intercept: float = model.intercept_
r_squared: float = model.score(x, y)
residual_std: float = float(np.std(residuals))
# scipy で p値を算出
slope_sp, intercept_sp, r_value, p_value, std_err = stats.linregress(
x.flatten(), y,
)
return {
"slope": slope,
"intercept": intercept,
"r_squared": r_squared,
"p_value": p_value,
"std_err": std_err,
"residual_std": residual_std,
"y_pred": y_pred,
"residuals": residuals,
}
# ==============================
# 乖離度の算出と判定
# ==============================
def evaluate_current_deviation(
y: np.ndarray,
result: Dict,
threshold: float,
) -> Dict[str, float]:
'現在の株価が回帰直線からどれだけ乖離しているかを判定する'
current_price: float = y[-1]
predicted_price: float = result["y_pred"][-1]
deviation: float = current_price - predicted_price
z_score: float = deviation / result["residual_std"] if result["residual_std"] > 0 else 0.0
if z_score > threshold:
status: str = "割高(上方乖離)"
elif z_score < -threshold:
status = "割安(下方乖離)"
else:
status = "適正範囲"
return {
"current_price": current_price,
"predicted_price": predicted_price,
"deviation": deviation,
"z_score": z_score,
"status": status,
}
# ==============================
# レポート表示
# ==============================
def show_report(
reg: Dict, dev: Dict, use_log: bool, ticker: str,
) -> None:
'回帰分析と乖離判定の結果を表示する'
print("=" * 55)
print(f" 単回帰分析レポート : {ticker}")
print("=" * 55)
price_label: str = "対数価格" if use_log else "価格"
print(f" 傾き (1日あたり) : {reg['slope']:>12.4f} ({price_label})")
print(f" 切片 : {reg['intercept']:>12.4f}")
print(f" 決定係数 R² : {reg['r_squared']:>12.4f}")
print(f" p値 : {reg['p_value']:>12.6f}")
print(f" 残差の標準偏差 : {reg['residual_std']:>12.4f}")
print(f" ─────────────────────────────────")
print(f" 現在{price_label} : {dev['current_price']:>12.4f}")
print(f" 回帰直線上の値 : {dev['predicted_price']:>12.4f}")
print(f" 乖離幅 : {dev['deviation']:>12.4f}")
print(f" Zスコア : {dev['z_score']:>12.4f}")
print(f" ─────────────────────────────────")
print(f" ★ 判定 : {dev['status']}")
print("=" * 55)
if reg["r_squared"] < 0.3:
print(" ⚠ R²が0.3未満です。トレンドが不明瞭なため判定の信頼性は低いです。")
if reg["p_value"] > 0.05:
print(" ⚠ p値が0.05超です。傾きが統計的に有意ではありません。")
# ==============================
# グラフ描画
# ==============================
def plot_regression(
dates: pd.DatetimeIndex,
y: np.ndarray,
result: Dict,
dev: Dict,
threshold: float,
use_log: bool,
ticker: str,
) -> None:
'回帰直線・バンド・残差をグラフに描画する'
y_pred: np.ndarray = result["y_pred"]
residual_std: float = result["residual_std"]
upper_band: np.ndarray = y_pred + threshold * residual_std
lower_band: np.ndarray = y_pred - threshold * residual_std
fig, axes = plt.subplots(2, 1, figsize=(14, 9), height_ratios=[3, 1])
# --- 上段:価格 + 回帰直線 + バンド ---
price_label: str = "Log Close" if use_log else "Close"
axes[0].plot(dates, y, label=price_label, color="steelblue", linewidth=1.2)
axes[0].plot(dates, y_pred, label="Regression Line", color="tomato", linewidth=1.5)
axes[0].fill_between(
dates, upper_band, lower_band, alpha=0.15, color="tomato",
label=f"±{threshold}σ Band",
)
axes[0].set_title(f"{ticker} - Linear Regression (R²={result['r_squared']:.4f})")
axes[0].set_ylabel(price_label)
axes[0].legend(loc="upper left")
axes[0].grid(alpha=0.3)
# --- 下段:残差 ---
colors: list = ["steelblue" if r >= 0 else "tomato" for r in result["residuals"]]
axes[1].bar(dates, result["residuals"], color=colors, width=1.5, alpha=0.7)
axes[1].axhline(y=0, color="black", linewidth=0.8)
axes[1].axhline(y=threshold * residual_std, color="tomato", linestyle="--", alpha=0.5)
axes[1].axhline(y=-threshold * residual_std, color="tomato", linestyle="--", alpha=0.5)
axes[1].set_title("Residuals (Actual - Predicted)")
axes[1].set_ylabel("Residual")
axes[1].grid(alpha=0.3)
plt.tight_layout()
plt.show()
# ==============================
# メイン処理
# ==============================
if __name__ == "__main__":
data: pd.DataFrame = fetch_ohlcv(TICKER, START_DATE, END_DATE)
x, y, dates = prepare_regression_data(data, USE_LOG_PRICE)
reg_result: Dict = run_linear_regression(x, y)
dev_result: Dict = evaluate_current_deviation(y, reg_result, DEVIATION_THRESHOLD)
show_report(reg_result, dev_result, USE_LOG_PRICE, TICKER)
plot_regression(dates, y, reg_result, dev_result, DEVIATION_THRESHOLD, USE_LOG_PRICE, TICKER)
コードの処理フロー解説
上記のコードは、以下の6ステップで構成されています。
* ステップ1(データ取得):yfinanceで指定銘柄の日足データをダウンロードし、カラム名を小文字に統一する
* ステップ2(データ準備):日付インデックスを0始まりの連番に変換し、説明変数(x)と目的変数(y:終値)をnumpy配列で返す。USE_LOG_PRICEがTrueの場合は対数変換を適用する
* ステップ3(回帰分析実行):scikit-learnのLinearRegressionで傾き・切片・R²を算出し、scipy.stats.linregressでp値を補完する
* ステップ4(乖離度判定):最新の株価と回帰直線上の値の差をZスコア(標準偏差の何倍か)に変換し、閾値と比較して「割高・割安・適正」を判定する
* ステップ5(レポート出力):傾き・R²・p値・乖離判定をコンソールに表示し、信頼性の低い場合は警告を出す
* ステップ6(グラフ描画):上段に株価・回帰直線・±σバンド、下段に残差の棒グラフを描画する
DEVIATION_THRESHOLDを1.5や3.0に変えれば判定の厳しさを調整できます。
回帰分析結果の正しい読み解き方
傾きとR²の組み合わせで判断する
傾きがプラスでもR²が低ければ、「上昇トレンドっぽいがノイズが大きくて信頼できない」状態です。以下の組み合わせで判断してください。
| 傾き | R² | 解釈 |
|---|---|---|
| プラス | 0.7以上 | 明確な上昇トレンド |
| プラス | 0.3〜0.7 | 弱い上昇傾向(要注意) |
| プラス | 0.3未満 | トレンドなし(回帰分析が不適切) |
| マイナス | 0.7以上 | 明確な下降トレンド |
R²が0.3未満のときに乖離判定の結果を信じるのは危険です。その場合は分析期間を短くするか、別のアプローチを検討してください。
p値が示す統計的有意性
p値(P-value)は「傾きが実際にはゼロである確率」を意味します。p値が0.05以下であれば、傾きは統計的に有意(偶然ではない)と判断できます。
p値が0.05を超えている場合、観測された傾きは偶然の産物である可能性が高いです。この場合、トレンドが存在するという前提自体を疑ってください。
【コピペOK】対数回帰と複数期間の比較分析
株価は複利的に成長するため、長期データでは対数変換(Log Transform)した方が直線にフィットしやすくなります。以下のコードをメインコードの末尾に追加してください。
# ==============================
# 複数期間の回帰比較
# ==============================
PERIODS: list = [
("短期(3ヶ月)", 63),
("中期(6ヶ月)", 126),
("長期(1年)", 252),
]
def run_multi_period_analysis(
df: pd.DataFrame, periods: list, use_log: bool, ticker: str,
) -> None:
'複数期間で回帰分析を実行し、結果を比較表示する'
print("n" + "=" * 65)
print(f" 複数期間 回帰分析比較 : {ticker}")
print("=" * 65)
print(f" {'期間':<16} {'傾き':>10} {'R²':>8} {'p値':>12} {'Zスコア':>8} {'判定':<14}")
print(f" {'-'*16} {'-'*10} {'-'*8} {'-'*12} {'-'*8} {'-'*14}")
fig, axes = plt.subplots(1, len(periods), figsize=(6 * len(periods), 5))
if len(periods) == 1:
axes = [axes]
for idx, (label, days) in enumerate(periods):
if len(df) < days:
print(f" {label:<16} データ不足({len(df)}行 < {days}日)")
continue
subset: pd.DataFrame = df.iloc[-days:]
x, y, dates = prepare_regression_data(subset, use_log)
reg: Dict = run_linear_regression(x, y)
dev: Dict = evaluate_current_deviation(y, reg, DEVIATION_THRESHOLD)
print(
f" {label:<16} {reg['slope']:>10.4f} "
f"{reg['r_squared']:>8.4f} {reg['p_value']:>12.6f} "
f"{dev['z_score']:>8.2f} {dev['status']:<14}"
)
price_label: str = "Log Close" if use_log else "Close"
axes[idx].plot(dates, y, color="steelblue", linewidth=1.0)
axes[idx].plot(dates, reg["y_pred"], color="tomato", linewidth=1.5)
upper: np.ndarray = reg["y_pred"] + DEVIATION_THRESHOLD * reg["residual_std"]
lower: np.ndarray = reg["y_pred"] - DEVIATION_THRESHOLD * reg["residual_std"]
axes[idx].fill_between(dates, upper, lower, alpha=0.12, color="tomato")
axes[idx].set_title(f"{label} (R²={reg['r_squared']:.3f})")
axes[idx].set_ylabel(price_label)
axes[idx].grid(alpha=0.3)
axes[idx].tick_params(axis="x", rotation=30)
print("=" * 65)
plt.tight_layout()
plt.show()
if __name__ == "__main__":
run_multi_period_analysis(data, PERIODS, USE_LOG_PRICE, TICKER)
コードの処理フロー解説
上記のコードは、以下の3ステップで構成されています。
* ステップ1(期間ごとのデータ抽出):直近63日・126日・252日のサブセットをそれぞれ切り出す
* ステップ2(回帰分析の繰り返し実行):各期間に対してrun_linear_regressionとevaluate_current_deviationを呼び出し、傾き・R²・Zスコア・判定を算出する
* ステップ3(比較表示と可視化):結果を横並びのテーブルとグラフで出力し、期間による判定の違いを一目で確認できるようにする
短期では割高でも長期では適正範囲ということは頻繁に起きます。複数期間を並べて見ることで、一つの時間軸に偏った判断を避けられます。
よくあるエラーと対処法
ValueError: Input contains NaN が発生する
データに欠損値(NaN)が含まれています。株価データでは休場日の穴埋めや、取得期間の先頭に発生しやすい問題です。
以下を試してください。
* データ取得後にdf.dropna()を追加して欠損行を除去する
* df.isnull().sum()で各列の欠損数を確認する
* START_DATEを少し前倒しにして、必要な期間のデータが揃うようにする
R²が極端に低い(0.01未満など)
対象期間がレンジ相場(横ばい)であり、直線モデルが当てはまらない状態です。これはエラーではなく、「回帰分析が不適切な相場環境」を正しく示しています。
以下を試してください。
* 分析期間を短縮して、トレンドが出ている区間に絞る
* USE_LOG_PRICEをTrueに変更し、対数変換で直線適合度が改善するか確認する
* レンジ相場ではボリンジャーバンド等の別指標に切り替える
グラフの日付軸が重なって読めない
データ期間が長い場合にmatplotlibの日付ラベルが重なります。描画の問題であり、データ自体には影響しません。
plot_regression関数内のaxes[0]設定後に以下を追加してください。fig.autofmt_xdate(rotation=30)と記述すれば、日付ラベルが30度傾いて重なりが解消されます。
まとめ
この記事では、Pythonの単回帰分析を用いて株価のトレンド傾向を定量化し、現在の価格が回帰直線に対して割安か割高かを判定する方法を解説しました。
要点を整理します。
* 単回帰分析の傾き(Slope)は1日あたりの株価変化量を表し、トレンドの方向と強さを数値化する
* 決定係数R²は0.7以上でトレンド判断の信頼性が高く、0.3未満では回帰分析自体が不適切と判断する
* p値は0.05以下で傾きが統計的に有意であり、超える場合は傾きが偶然の産物である可能性を疑う
* Zスコア(残差÷標準偏差)による乖離判定は、±2σを超えた場合に割高・割安シグナルとして活用する
* 複数期間(短期・中期・長期)で比較することで、単一の時間軸に偏った誤判断を防止する
次のステップとして、回帰分析を重回帰(Multiple Regression)に拡張し、出来高やVIXなどの変数を追加することを検討してください。説明変数を増やすことで、株価変動の説明力が向上し、より精度の高いトレンド判定が可能になります。
また、回帰直線の傾きの時系列変化を追跡すれば、「トレンドが加速しているか減速しているか」の判定にも応用できます。ローリングウィンドウ(Rolling Window)で日々の傾きを算出し、その変化率を監視するアプローチが有効です。
