はじめに
金価格はずっと上がっているイメージがあるが、
他の金融資産とどう連動しているのかを感覚的だけでなく、
定量的に分析してみたいと思いました。
過去10年分の市場データをもとに、金と株式・債券・原油・銀などの商品、
さらにドル指数や円ドル為替との相関関係を分析しました。
あわせてProphetを用いた将来予測も実施し、過去トレンドの延長線上での
価格推移を確認します。
解決したい社会課題
円安・インフレの進行により、現金の実質的な価値が目減りしている現状において、
資産保全の手段として金への関心が高まっています。
しかし金は「安全資産」と言われながらも、株式・原油・為替など他の資産と
どのような相関関係があるのかは一般に広く知られていません。
金融庁のNISA普及施策により個人投資家が増える中、「金と他資産の相関関係」を
データから明らかにすることで、資産運用を考える際の定量的な参考情報を
提供したいと考えました。
分析するデータ
yfinance ライブラリを使ってYahoo Financeから以下のデータを取得します。分析期間は過去10年分です。
| カテゴリ | 資産名 | ティッカー |
|---|---|---|
| 金(メイン) | 金ETF | GLD |
| 株式 | S&P500 | ^GSPC |
| 債券 | 米国債券 | AGG |
| 金利 | 米10年国債利回り | ^TNX |
| 仮想通貨 | ビットコイン | BTC-USD |
| 商品(原油) | 原油先物 | CL=F |
| 商品(貴金属) | 銀先物 | SI=F |
| 商品(貴金属) | プラチナ先物 | PL=F |
| 経済指標 | ドルインデックス | DX-Y.NYB |
| 経済指標 | 円ドル為替 | JPY=X |
実行環境
パソコン:Windows
開発環境:Google Colaboratory
言語:Python
ライブラリ:pandas、numpy、matplotlib、seaborn、yfinance、prophet、holidays、pandas-market-calendars、statsmodels、scikit-learn、tensorflow
分析の流れ
1.データの取得(10資産・過去10年分)
2.欠損値の確認と補完(土日・祝日・NYSE休場日・取得漏れの分類)
3.可視化①:基準化した価格推移の比較
4.可視化②:相関係数ヒートマップ
5.可視化③:散布図(上位6資産)
6.可視化④:Prophet 5年先予測
7.可視化⑤:トレンド・季節性の分解
分析の過程
セットアップ
Google ColabでPythonの日本語表示に必要なフォントをインストールし、分析に使うライブラリを読み込みます。また、欠損値の分類に使う祝日カレンダーと市場カレンダーのライブラリも合わせてインストールします。
実行したコード
# 日本語フォント(IPAゴシック)をインストール
!apt-get -y install fonts-ipafont-gothic > /dev/null 2>&1
# 祝日判定・市場カレンダーライブラリをインストール(初回のみ)
!pip install holidays pandas_market_calendars -q
# 標準ライブラリ
import shutil, os, matplotlib
# グラフ描画
import matplotlib.font_manager as fm
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
# データ処理
import pandas as pd
import numpy as np
# 統計グラフ
import seaborn as sns
# 株価データ取得
import yfinance as yf
# 時系列予測
from prophet import Prophet
# 祝日判定
import holidays
# 市場カレンダー(NYSE・CME等の取引日管理)
import pandas_market_calendars as mcal
# インストールしたフォントをmatplotlibのフォントディレクトリにコピー
src = '/usr/share/fonts/opentype/ipafont-gothic/ipag.ttf'
dst = os.path.join(os.path.dirname(matplotlib.__file__),
'mpl-data', 'fonts', 'ttf', 'ipag.ttf')
shutil.copy(src, dst)
# フォントプロパティを設定(グラフの日本語表示に使用)
fp = fm.FontProperties(fname=dst)
# マイナス記号を正しく表示する設定
plt.rcParams['axes.unicode_minus'] = False
# グラフのスタイルを設定
plt.style.use('seaborn-v0_8')
# グラフをノートブック内に表示する設定
%matplotlib inline
print('準備完了!')
1.データの取得
yfinanceを使ってYahoo Financeから10資産・過去10年分の株価データを取得します。各資産は取引所が異なるため取引日の数が異なります。全資産を結合すると取引日の和集合がインデックスになるため、取引のない日はNaN(欠損)となります。
実行したコード
# ── 各資産が属する市場カレンダーを定義 ──────────────────
# 欠損日が「その資産の市場の休場日かどうか」を判定するために使用
asset_calendars = {
'金(GLD)': 'NYSE', # ETF → NYSE
'S&P500': 'NYSE', # NYSE
'米国債券(AGG)': 'NYSE', # ETF → NYSE
'米金利(TNX)': 'NYSE', # 債券市場 → NYSE準拠
'ビットコイン': None, # 365日取引 → カレンダー不要
'原油': 'CMEGlobex_CL', # 原油先物 → CME
'銀': 'CMEGlobex_SI', # 銀先物 → CME
'プラチナ': 'CMEGlobex_PL', # プラチナ先物 → CME
'ドル指数': 'NYSE', # FX専用カレンダーなし → NYSE代用
'円ドル': 'NYSE', # FX専用カレンダーなし → NYSE代用
}
# 各カレンダーの取引日一覧を取得してキャッシュ(重複取得を避けるためsetで一意化)
calendars = {}
for cal_name in set(asset_calendars.values()):
if cal_name:
cal = mcal.get_calendar(cal_name)
schedule = cal.schedule(start_date='2015-01-01', end_date='2026-12-31')
calendars[cal_name] = pd.DatetimeIndex(schedule.index.normalize())
# ── データ取得 ──────────────────────────────────────────
# 分析対象の資産とティッカーシンボルを定義
tickers = {
'金(GLD)': 'GLD', # 金ETF(メイン分析対象)
'S&P500': '^GSPC', # 米国株式指数
'米国債券(AGG)': 'AGG', # 米国債券ETF
'米金利(TNX)': '^TNX', # 米10年国債利回り
'ビットコイン': 'BTC-USD', # 仮想通貨
'原油': 'CL=F', # 原油先物
'銀': 'SI=F', # 銀先物
'プラチナ': 'PL=F', # プラチナ先物
'ドル指数': 'DX-Y.NYB', # ドルの強さを示す指数
'円ドル': 'JPY=X', # 円ドル為替レート
}
# 取得期間を10年に設定
period = '10y'
# 各資産のデータを取得して辞書に格納
dfs = {}
for name, ticker in tickers.items():
df = yf.Ticker(ticker).history(period=period)
# タイムゾーン情報を除去し、日付のみに統一
df.index = df.index.tz_localize(None).normalize()
# 終値のみを取得
dfs[name] = df['Close']
print(f'{name}: {len(df)}件取得')
# ── 結合(各資産の取引日の和集合がインデックスになる) ──
# 各資産で取引日が異なるため、結合後は取引のない日がNaNになる
prices = pd.DataFrame(dfs)
print(f'\n結合後の総行数: {len(prices)}件')
print(f' ※ビットコイン({len(dfs["ビットコイン"])}件)が最多 + 他資産固有の日付が加算された行数')
実行結果
金(GLD): 2513件取得
S&P500: 2513件取得
米国債券(AGG): 2513件取得
米金利(TNX): 2513件取得
ビットコイン: 3653件取得
原油: 2514件取得
銀: 2513件取得
プラチナ: 2512件取得
ドル指数: 2514件取得
円ドル: 2601件取得
結合後の総行数: 3654件
※ビットコイン(3653件)が最多 + 他資産固有の日付が加算された行数
2.欠損値の確認と補完
欠損値の件数・内訳を確認したうえでffillで補完します。欠損値の補完方法にはいくつかの種類があります。
| 方法 | 内容 | 向いているケース |
|---|---|---|
| ffill(前方補完) | 前日の値で埋める | 株価・為替など |
| bfill(後方補完) | 翌日の値で埋める | 稀に使う |
| 平均値補完 | 全体の平均値で埋める | 株価には不向き |
| 線形補間 | 前後の値の中間値で埋める | 緩やかに変化するデータ |
| 0埋め | 0で埋める | 取引量などのカウントデータ |
今回は株価データを扱うため「休日は取引がなかっただけで価格は変わっていない」という考え方からffill(前方補完)を採用しています。ffillで補完できない先頭行(取得期間の最初で前日値がない行)はdropnaで削除しています。
実行したコード
# ── 補完前の欠損確認 ────────────────────────────────────
# 欠損件数と欠損率を資産ごとに表示
print('\n【補完前の欠損値】')
missing = prices.isnull().sum()
missing_pct = (missing / len(prices) * 100).round(2)
print(pd.DataFrame({'欠損件数': missing, '欠損率(%)': missing_pct}).to_string())
print(' ※欠損率~31%はビットコイン(365日)のインデックスに株式・商品(平日のみ)が合わせられたため')
# ── 欠損日の分類(土日/祝日/NYSE休場日/未分類) ───────
# 米国・日本の祝日一覧を取得
us_holiday_dates = pd.DatetimeIndex(holidays.US(years=range(2015, 2027)).keys())
jp_holiday_dates = pd.DatetimeIndex(holidays.JP(years=range(2015, 2027)).keys())
# NYSEの取引日一覧を取得(グッドフライデー等の特別休場日も含む)
nyse = mcal.get_calendar('NYSE')
nyse_schedule = nyse.schedule(start_date='2015-01-01', end_date='2026-12-31')
nyse_trading_days = pd.DatetimeIndex(nyse_schedule.index.normalize())
# 欠損が1件以上ある行の日付を取得
missing_rows = prices[prices.isnull().any(axis=1)]
missing_dates = missing_rows.index
# 各欠損日を分類するフラグを作成
is_saturday = missing_dates.day_of_week == 5
is_sunday = missing_dates.day_of_week == 6
is_us_holiday = missing_dates.isin(us_holiday_dates)
is_jp_holiday = missing_dates.isin(jp_holiday_dates)
is_nyse_closed = ~missing_dates.isin(nyse_trading_days) # NYSE非取引日
is_weekday_holiday = (~is_saturday & ~is_sunday) & (is_us_holiday | is_jp_holiday | is_nyse_closed)
is_unexplained = ~is_saturday & ~is_sunday & ~is_weekday_holiday # 上記いずれにも該当しない
# 欠損日の内訳をサマリー表示
summary = pd.DataFrame({
'件数': [
is_saturday.sum(),
is_sunday.sum(),
is_weekday_holiday.sum(),
is_unexplained.sum(),
len(missing_dates),
]
}, index=['土曜日', '日曜日', '平日休場日(祝日・NYSE特別休場含む)', '未分類', '合計'])
summary['割合(%)'] = (summary['件数'] / len(missing_dates) * 100).round(1)
print('\n【欠損日の内訳】')
print(summary.to_string())
# ── 未分類の欠損日を個別に詳細表示 ─────────────────────
# 未分類が残る場合、欠損している資産ごとに所属カレンダーで休場かどうかを判定
if is_unexplained.sum() > 0:
today = pd.Timestamp.today().normalize()
print(f'\n【未分類の欠損日 {is_unexplained.sum()}件】')
for d in missing_dates[is_unexplained]:
# その日に欠損している資産名を取得
missing_assets = prices.columns[prices.loc[d].isna()].tolist()
if d >= today:
reason = '本日以降(データ未収録)'
else:
asset_reasons = []
for asset in missing_assets:
cal_name = asset_calendars.get(asset)
if cal_name is None:
# ビットコインは365日取引のため欠損はデータ取得漏れ
asset_reasons.append(f'{asset}:取得漏れ')
elif d not in calendars[cal_name]:
# 所属カレンダー上の休場日 → 正常な欠損
asset_reasons.append(f'{asset}:{cal_name}休場日')
else:
# 取引日なのに欠損 → yfinanceの取得漏れ
asset_reasons.append(f'{asset}:取引日だが欠損(取得漏れ)')
reason = ' / '.join(asset_reasons)
print(f' {d.date()} ({d.day_name()}) → {reason}')
# ── ffillで補完(前日終値で埋める) → 先頭行など補完不能な行をdropna ──
# 土日・祝日・休場日の欠損を前日終値で補完
prices = prices.ffill()
remaining_missing = prices.isnull().sum().sum()
print(f'\nffill後の残り欠損数: {remaining_missing}件(DataFrameの先頭行など前日値がない行)')
# ffillで補完できなかった先頭付近の行を削除
prices = prices.dropna()
print(f'dropna後の総行数: {len(prices)}件')
print(f' → 削除されたのは先頭付近の{remaining_missing}件のみで、分析への影響はほぼなし')
実行結果
欠損の内訳を確認すると、全1,149件のうち土日だけで90.8%を占めており、大半が週末による正常な欠損です。平日休場日8.4%はNYSEのグッドフライデーや祝日など市場カレンダー上の休場日で、こちらも想定内の欠損です。
未分類8件については、2016-04-15の全資産欠損はYahoo Finance側のデータ欠落、円ドル・銀・プラチナの散発的な欠損はyfinanceの取得漏れと考えられます。また2026-04-16は本日分のデータがまだ収録されていないためです。いずれもffillで前日終値を用いて補完されており、分析への影響はほぼありません。
ffill後も残る25件の欠損はデータ取得期間の先頭行(前日値が存在しない行)であり、dropnaで削除しています。最終的なデータは3,651件となりました。
【補完前の欠損値】
欠損件数 欠損率(%)
金(GLD) 1141 31.23
S&P500 1141 31.23
米国債券(AGG) 1141 31.23
米金利(TNX) 1141 31.23
ビットコイン 1 0.03
原油 1140 31.20
銀 1141 31.23
プラチナ 1142 31.25
ドル指数 1140 31.20
円ドル 1053 28.82
※欠損率~31%はビットコイン(365日)のインデックスに株式・商品(平日のみ)が合わせられたため
【欠損日の内訳】
件数 割合(%)
土曜日 522 45.4
日曜日 522 45.4
平日休場日(祝日・NYSE特別休場含む) 97 8.4
未分類 8 0.7
合計 1149 100.0
【未分類の欠損日 8件】
2016-04-15 (Friday) → 金(GLD):取引日だが欠損(取得漏れ) / S&P500:取引日だが欠損(取得漏れ) / 米国債券(AGG):取引日だが欠損(取得漏れ) / ビットコイン:取得漏れ / 原油:取引日だが欠損(取得漏れ) / 銀:取引日だが欠損(取得漏れ) / プラチナ:取引日だが欠損(取得漏れ) / ドル指数:取引日だが欠損(取得漏れ) / 円ドル:取引日だが欠損(取得漏れ)
2016-05-27 (Friday) → プラチナ:取引日だが欠損(取得漏れ)
2017-07-11 (Tuesday) → 円ドル:取引日だが欠損(取得漏れ)
2017-11-16 (Thursday) → 円ドル:取引日だが欠損(取得漏れ)
2018-01-29 (Monday) → 銀:取引日だが欠損(取得漏れ) / プラチナ:取引日だが欠損(取得漏れ)
2019-05-22 (Wednesday) → 円ドル:取引日だが欠損(取得漏れ)
2025-04-21 (Monday) → 円ドル:取引日だが欠損(取得漏れ)
2026-04-16 (Thursday) → 本日以降(データ未収録)
ffill後の残り欠損数: 25件(DataFrameの先頭行など前日値がない行)
dropna後の総行数: 3651件
→ 削除されたのは先頭付近の25件のみで、分析への影響はほぼなし
3.可視化①:基準化した価格推移の比較
全資産の価格を「取得開始時点=100」に基準化して比較します。各資産の絶対価格はバラバラなため、基準化することで「どれだけ上がったか・下がったか」を同じ軸で比較できます。ビットコインは値動きが大きすぎて他の資産が見えなくなるため、対数スケールで別グラフに分けて表示します。
実行したコード
# 全資産を取得開始時点=100に基準化(相対的な価格変動を比較するため)
normalized = prices / prices.iloc[0] * 100
# 上下2段のグラフを作成
fig, axes = plt.subplots(2, 1, figsize=(16, 12))
# ── 上段:ビットコイン除く全資産の価格推移 ──────────────
ax1 = axes[0]
for name, col in normalized.items():
if name == 'ビットコイン':
continue # ビットコインは下段に別途表示
if name == '金(GLD)':
# 金は分析のメインのため太線・金色で強調表示
ax1.plot(normalized.index, col, color='#FFD700',
linewidth=3, label=name, zorder=5)
else:
ax1.plot(normalized.index, col, linewidth=1.2,
alpha=0.7, label=name)
# 基準線(100)をグレーの破線で表示
ax1.axhline(100, color='gray', linestyle='--', linewidth=0.8)
# X軸を年単位(主目盛)・4月/7月/10月(補助目盛)で設定
ax1.xaxis.set_major_locator(mdates.YearLocator())
ax1.xaxis.set_major_formatter(mdates.DateFormatter('%Y年'))
ax1.xaxis.set_minor_locator(mdates.MonthLocator(bymonth=[4, 7, 10]))
ax1.xaxis.set_minor_formatter(mdates.DateFormatter('%m月'))
ax1.tick_params(axis='x', which='major', labelsize=10, pad=15)
ax1.tick_params(axis='x', which='minor', labelsize=8, labelcolor='gray')
ax1.set_title('金と各資産の価格推移比較(ビットコイン除く・取得開始時点=100)',
fontproperties=fp, fontsize=13)
ax1.set_ylabel('基準化価格', fontproperties=fp, fontsize=12)
ax1.legend(prop=fp, fontsize=9, loc='upper left', ncol=2)
ax1.grid(True, alpha=0.3, which='major')
ax1.grid(True, alpha=0.15, which='minor', linestyle=':')
# ── 下段:ビットコイン vs 金(対数スケール) ────────────
ax2 = axes[1]
ax2.plot(normalized.index, normalized['ビットコイン'],
color='#7B68EE', linewidth=1.5, label='ビットコイン')
ax2.plot(normalized.index, normalized['金(GLD)'],
color='#FFD700', linewidth=2, label='金(GLD)')
# ビットコインの値動きが極端に大きいため対数スケールで表示
ax2.set_yscale('log')
ax2.xaxis.set_major_locator(mdates.YearLocator())
ax2.xaxis.set_major_formatter(mdates.DateFormatter('%Y年'))
ax2.xaxis.set_minor_locator(mdates.MonthLocator(bymonth=[4, 7, 10]))
ax2.xaxis.set_minor_formatter(mdates.DateFormatter('%m月'))
ax2.tick_params(axis='x', which='major', labelsize=10, pad=15)
ax2.tick_params(axis='x', which='minor', labelsize=8, labelcolor='gray')
ax2.set_title('ビットコイン vs 金(対数スケール)',
fontproperties=fp, fontsize=13)
ax2.set_xlabel('年', fontproperties=fp, fontsize=12)
ax2.set_ylabel('基準化価格(対数)', fontproperties=fp, fontsize=12)
ax2.legend(prop=fp, fontsize=10)
ax2.grid(True, alpha=0.3, which='major')
ax2.grid(True, alpha=0.15, linestyle=':', which='minor')
plt.tight_layout()
plt.show()
実行結果
上段グラフでは、金(GLD)が10年間で約3.5倍に上昇しており、S&P500に次ぐ高いリターンを示しています。原油・プラチナは低迷が続いており、資産によってリターンに大きな差があることがわかります。下段の対数スケールグラフでは、ビットコインが桁違いの上昇率を示す一方、暴落も激しく金とは別次元の値動きをしていることが確認できます。
4.可視化②:相関係数ヒートマップ
各資産の日次リターンの対数収益率を計算し、資産間の相関係数をヒートマップで可視化します。1に近いほど同じ方向、-1に近いほど逆方向に動く傾向があることを示します。価格そのままではなく対数収益率を使うことで、価格水準の差による影響を除いた「日々の動きの連動性」を測ることができます。
実行したコード
# 対数収益率を計算(前日比の変化率を対数で表現)
# prices.shift(1)で前日の価格にずらし、その比の対数をとる
log_returns = np.log(prices / prices.shift(1)).dropna()
# 全資産間の相関係数行列を計算
corr = log_returns.corr()
# ── ヒートマップ+ランキングを1枚の図にまとめる ──────────
# 上段(63%)にヒートマップ、下段(28%)にランキングを配置
fig = plt.figure(figsize=(14, 13))
ax_heat = fig.add_axes([0.05, 0.32, 0.90, 0.63]) # [左, 下, 幅, 高さ](図全体を0〜1とした相対位置)
ax_rank = fig.add_axes([0.05, 0.01, 0.90, 0.28]) # ランキング領域
# ── ヒートマップ ────────────────────────────────────────
sns.heatmap(
corr,
annot=True, # 各セルに相関係数の値を表示
fmt='.2f', # 小数点2桁で表示
vmin=-1, vmax=1, # カラースケールを-1〜+1に固定
cmap='coolwarm_r', # 正相関=赤、負相関=青のカラーマップ
square=True, # セルを正方形に統一
linewidths=0.5, # セル間の区切り線
annot_kws={'size': 10},
ax=ax_heat
)
ax_heat.set_title('金と各資産の相関係数ヒートマップ(対数収益率)',
fontproperties=fp, fontsize=14)
ax_heat.set_xticklabels(ax_heat.get_xticklabels(), fontproperties=fp, fontsize=9)
ax_heat.set_yticklabels(ax_heat.get_yticklabels(), fontproperties=fp, fontsize=9)
# ヒートマップ右下にカラーの読み方を小さく追記
fig.text(0.95, 0.305,
'赤:正の相関(同方向) 白:無相関 青:負の相関(逆方向)',
fontproperties=fp, fontsize=8, color='gray',
ha='right', va='bottom')
# ── 相関ランキング(棒グラフ) ───────────────────────────
# 金との相関係数を高い順に並べる
gold_corr = corr['金(GLD)'].drop('金(GLD)').sort_values(ascending=False)
# 軸を非表示にしてテキスト・棒グラフの描画エリアとして使用
ax_rank.axis('off')
ax_rank.text(0.10, 1.0, '【金との相関係数ランキング】',
fontproperties=fp, fontsize=11,
transform=ax_rank.transAxes, va='top')
for i, (name, r) in enumerate(gold_corr.items()):
# 行のY座標(上から順に並べる)
y = 0.85 - i * 0.092
bar_len = abs(r)
# 正相関=赤・負相関=青でヒートマップの色と統一
color = '#CC3333' if r > 0 else '#3355CC'
# 資産名
ax_rank.text(0.10, y, f'{name}',
fontproperties=fp, fontsize=9,
transform=ax_rank.transAxes, va='center')
# 相関係数と正負の表示
ax_rank.text(0.28, y, f': {r:+.3f} {"正" if r > 0 else "負"}相関',
fontproperties=fp, fontsize=9,
transform=ax_rank.transAxes, va='center')
# 相関係数の絶対値に比例した長さの棒グラフ
ax_rank.barh(y, bar_len * 0.45, left=0.45, height=0.07,
color=color, alpha=0.7,
transform=ax_rank.transAxes)
# 画像として保存(Qiita掲載用)
plt.savefig('gold_可視化②_相関係数ヒートマップ.png',
dpi=150, bbox_inches='tight')
plt.show()
実行結果
金との相関が最も高いのは銀(+0.703)で、同じ貴金属として強く連動しています。プラチナも+0.532と比較的高い正の相関を示しています。一方、ドル指数(-0.432)と米金利(-0.264)は負の相関を示しており、ドル高・金利上昇局面では金価格が下がりやすい傾向があることがわかります。S&P500やビットコインとの相関は0.1未満と非常に低く、株式市場と独立した値動きをすることが確認できました。
5.可視化③:散布図(上位6資産)
ヒートマップで相関の全体像を把握したうえで、特に相関の強い上位6資産を散布図で深掘りします。相関係数は1つの数値に情報を圧縮するため、外れ値の影響や非線形な関係は見えません。散布図と回帰直線を合わせることで、「どの程度の強さで・どんな形で連動しているか」をより具体的に確認します。
実行したコード
# 相関係数の絶対値が大きい上位6資産を抽出
top_assets = gold_corr.abs().sort_values(ascending=False).head(6).index.tolist()
# 2行3列のサブプロットを作成
fig, axes = plt.subplots(2, 3, figsize=(15, 9))
axes = axes.flatten() # 2次元配列を1次元に変換してループしやすくする
for ax, asset in zip(axes, top_assets):
# 金と対象資産の対数収益率を取得(欠損行を除外)
valid = log_returns[['金(GLD)', asset]].dropna()
x = valid[asset].values
y = valid['金(GLD)'].values
# 1次回帰(直線)の係数を計算
reg = np.polyfit(x, y, 1)
# 散布図を描画(半透明・小さい点で密度を視覚化)
ax.scatter(x, y, alpha=0.3, s=10, color='gray')
# 回帰直線を赤線で描画
x_line = np.linspace(x.min(), x.max(), 100)
ax.plot(x_line, np.polyval(reg, x_line), 'r', lw=2)
# タイトルに相関係数を表示
r = corr.loc['金(GLD)', asset]
ax.set_title(f'金 × {asset}\n相関={r:.3f}',
fontproperties=fp, fontsize=11)
ax.set_xlabel(asset, fontproperties=fp, fontsize=9)
ax.set_ylabel('金(GLD)', fontproperties=fp, fontsize=9)
ax.grid(True, alpha=0.3)
plt.suptitle('金と相関の高い資産の散布図(上位6資産)',
fontproperties=fp, fontsize=13)
plt.tight_layout()
plt.show()
実行結果
銀・プラチナとの散布図では点が右上がりに集まっており、回帰直線の傾きも急で連動性の強さが視覚的に確認できます。一方、ドル指数との散布図は右下がりの回帰直線となっており、ドル高になると金価格が下がる負の相関関係がはっきりと現れています。米国債券との正の相関は散布図では点の広がりが大きく、ヒートマップの数値(+0.318)通り中程度の連動性であることがわかります。
6.可視化④:Prophet 5年先予測
Prophetは、トレンド・季節性・信頼区間を自動で扱えるため、今回のような「長期トレンドの可視化」に適しています。ARIMAやLSTMなど他のモデルも存在しますが、コードのシンプルさと結果の解釈しやすさからProphetを採用しました。なお予測はあくまで過去トレンドの延長であり、地政学リスクや金融政策の転換などは考慮されません。
ここでいうトレンドとは短期的な上下を除いた長期的な方向性、季節性とは年単位で繰り返す周期的なパターン、信頼区間とは予測値がどの範囲に収まるかの確率的な幅を指します。予測期間が長いほど信頼区間は広がり、不確実性が高まります。
実行したコード
# Prophetの入力形式に変換(ds=日付列、y=予測対象の値列)
prophet_df = pd.DataFrame({
'ds': prices.index,
'y': prices['金(GLD)'].values
})
# Prophetモデルの設定
m = Prophet(
yearly_seasonality=True, # 年間の季節性パターンを考慮する
weekly_seasonality=False, # 週次の季節性は金価格に不要なためオフ
daily_seasonality=False, # 日次の季節性も不要なためオフ
changepoint_prior_scale=0.05 # トレンド変化の感度(小さいほど滑らか)
)
# 過去10年のデータでモデルを学習
m.fit(prophet_df)
# 5年分(365日×5)の将来日付を生成
future = m.make_future_dataframe(periods=365*5)
# 予測を実行(過去+将来の予測値・信頼区間を返す)
forecast = m.predict(future)
# グラフ描画
fig, ax = plt.subplots(figsize=(16, 6))
# 実績データを金色で表示
ax.plot(prophet_df['ds'], prophet_df['y'],
label='実績', color='#FFD700', linewidth=1.5)
# 予測値をオレンジの破線で表示
ax.plot(forecast['ds'], forecast['yhat'],
label='予測', color='#FF8C00', linewidth=2, linestyle='--')
# 95%信頼区間を塗りつぶしで表示
ax.fill_between(forecast['ds'],
forecast['yhat_lower'], forecast['yhat_upper'],
alpha=0.2, color='#FF8C00', label='予測範囲(95%信頼区間)')
# 予測開始点を赤い縦線で表示
ax.axvline(prophet_df['ds'].max(), color='red',
linestyle=':', linewidth=1.5, label='予測開始点')
ax.set_title('金(GLD)10年実績+5年先予測(Prophet)',
fontproperties=fp, fontsize=14)
ax.set_xlabel('年', fontproperties=fp, fontsize=12)
ax.set_ylabel('価格(USD)', fontproperties=fp, fontsize=12)
ax.legend(prop=fp, fontsize=11)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
実行結果
過去10年の上昇トレンドをそのまま延長する形で予測が出ており、5年後には現在の約2.4倍にあたる水準が中央予測値となっています。ただし信頼区間(オレンジの塗り部分)は年数が経つほど急激に広がっており、予測の不確実性が高いことを示しています。
参考:複数モデルによる5年先予測の比較
Prophetの予測結果を検証するため、他の代表的な時系列予測モデルと比較してみます。
各モデルの特徴
| モデル | アプローチ | 特徴 |
|---|---|---|
| Prophet | トレンド+季節性の分解 | 長期トレンドの可視化に強い・信頼区間あり |
| ARIMA | 統計的自己回帰モデル | 保守的な予測・平均回帰しやすい・信頼区間あり |
| XGBoost | ラグ特徴量+勾配ブースティング | 直近パターンを重視・信頼区間なし |
| LSTM | 深層学習(再帰型ニューラルネット) | 複雑なパターンを学習・信頼区間なし |
実行したコード
!pip install statsmodels scikit-learn tensorflow # 初回のみ
from statsmodels.tsa.arima.model import ARIMA
from sklearn.preprocessing import MinMaxScaler
from sklearn.ensemble import GradientBoostingRegressor
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense
import warnings
warnings.filterwarnings('ignore')
# ── Prophet予測(可視化④と同じ処理) ────────────────────
prophet_df = pd.DataFrame({
'ds': prices.index,
'y': prices['金(GLD)'].values
})
m = Prophet(
yearly_seasonality=True,
weekly_seasonality=False,
daily_seasonality=False,
changepoint_prior_scale=0.05
)
m.fit(prophet_df)
future = m.make_future_dataframe(periods=365*5)
forecast = m.predict(future)
future_only = forecast[forecast['ds'] > prophet_df['ds'].max()]
# ── 月次データを準備(ARIMA・XGBoost・LSTM共通) ─────────
# ARIMAとXGBoostは日次データだと計算が重いため月次に変換
monthly = prices['金(GLD)'].resample('MS').mean()
# ── ARIMA予測 ────────────────────────────────────────────
# p=2: 過去2期分の自己回帰、d=1: 1階差分でトレンド除去、q=2: 移動平均2期
arima_model = ARIMA(monthly, order=(2, 1, 2))
arima_result = arima_model.fit()
arima_fc = arima_result.get_forecast(steps=60) # 60ヶ月=5年先を予測
arima_mean = arima_fc.predicted_mean
arima_ci = arima_fc.conf_int() # 95%信頼区間
# ── XGBoost予測 ──────────────────────────────────────────
# 時系列をラグ特徴量に変換して回帰問題として解く
def make_lag_features(series, n_lags=12):
df_lag = pd.DataFrame({'y': series})
for i in range(1, n_lags + 1):
# 過去n_lagsヶ月分の価格を特徴量として追加
df_lag[f'lag_{i}'] = df_lag['y'].shift(i)
return df_lag.dropna()
lag_df = make_lag_features(monthly, n_lags=12)
X = lag_df.drop('y', axis=1)
y_train = lag_df['y']
xgb_model = GradientBoostingRegressor(
n_estimators=200, # 決定木の数
learning_rate=0.05, # 学習率(小さいほど慎重に学習)
max_depth=3 # 木の深さ(深すぎると過学習)
)
xgb_model.fit(X, y_train)
# 直前12ヶ月のラグを使って1ヶ月ずつ逐次予測(60ヶ月分)
xgb_preds = []
last_values = list(monthly.values[-12:])
for _ in range(60):
x_input = np.array(last_values[-12:][::-1]).reshape(1, -1)
pred = xgb_model.predict(x_input)[0]
xgb_preds.append(pred)
last_values.append(pred) # 予測値を次の入力に使う
xgb_index = pd.date_range(
start=monthly.index[-1] + pd.DateOffset(months=1),
periods=60, freq='MS'
)
xgb_series = pd.Series(xgb_preds, index=xgb_index)
# ── LSTM予測 ─────────────────────────────────────────────
# 価格を0〜1にスケーリング(ニューラルネットは正規化が必要)
scaler = MinMaxScaler()
scaled = scaler.fit_transform(monthly.values.reshape(-1, 1))
# 過去24ヶ月を入力として次の1ヶ月を予測するシーケンスを作成
n_steps = 24
X_lstm, y_lstm = [], []
for i in range(n_steps, len(scaled)):
X_lstm.append(scaled[i - n_steps:i])
y_lstm.append(scaled[i])
X_lstm = np.array(X_lstm)
y_lstm = np.array(y_lstm)
# LSTMモデルの構築
lstm_model = Sequential([
LSTM(64, return_sequences=True, input_shape=(n_steps, 1)), # 64ユニットのLSTM層
LSTM(32), # 32ユニットのLSTM層
Dense(1) # 出力層(1ヶ月先の価格)
])
lstm_model.compile(optimizer='adam', loss='mse')
# 学習(verbose=0で進捗表示を非表示)
lstm_model.fit(X_lstm, y_lstm, epochs=50, batch_size=16, verbose=0)
# 直前24ヶ月を入力として60ヶ月先まで逐次予測
lstm_preds = []
input_seq = scaled[-n_steps:].copy()
for _ in range(60):
x_input = input_seq[-n_steps:].reshape(1, n_steps, 1)
pred = lstm_model.predict(x_input, verbose=0)[0, 0]
lstm_preds.append(pred)
input_seq = np.append(input_seq, [[pred]], axis=0)
# スケールを元の価格水準に戻す
lstm_preds = scaler.inverse_transform(
np.array(lstm_preds).reshape(-1, 1)).flatten()
lstm_series = pd.Series(lstm_preds, index=xgb_index)
# ── 4モデル比較グラフ ────────────────────────────────────
fig, axes = plt.subplots(2, 2, figsize=(18, 12))
axes = axes.flatten()
# 各モデルの設定(タイトル・予測index・予測値・信頼区間・色)
configs = [
('Prophet', forecast['ds'], forecast['yhat'], forecast['yhat_lower'], forecast['yhat_upper'], '#FF8C00'),
('ARIMA(2,1,2)', arima_mean.index, arima_mean, arima_ci.iloc[:, 0], arima_ci.iloc[:, 1], '#4488CC'),
('XGBoost', xgb_index, xgb_series, None, None, '#44AA66'),
('LSTM', lstm_index, lstm_series, None, None, '#AA44CC'),
]
for ax, (title, fc_index, fc_mean, fc_lower, fc_upper, color) in zip(axes, configs):
# 実績データ(月次)
ax.plot(monthly.index, monthly.values,
color='#FFD700', linewidth=1.5, label='実績')
# 予測値
ax.plot(fc_index, fc_mean,
color=color, linewidth=2, linestyle='--', label='予測')
# 信頼区間(ProphetとARIMAのみ)
if fc_lower is not None:
ax.fill_between(fc_index, fc_lower, fc_upper,
alpha=0.2, color=color, label='95%信頼区間')
# 予測開始点を赤い縦線で表示
ax.axvline(monthly.index[-1], color='red',
linestyle=':', linewidth=1.5, label='予測開始点')
ax.set_title(title, fontproperties=fp, fontsize=13)
ax.set_ylabel('価格(USD)', fontproperties=fp, fontsize=10)
ax.legend(prop=fp, fontsize=9)
ax.grid(True, alpha=0.3)
plt.suptitle('金価格5年先予測 4モデル比較',
fontproperties=fp, fontsize=15)
plt.tight_layout()
plt.savefig('gold_4モデル比較予測.png', dpi=150, bbox_inches='tight')
plt.show()
# ── 予測値の数値比較 ─────────────────────────────────────
last_price = monthly.iloc[-1]
print('【4モデル予測サマリー比較】')
print(f'現在の価格: ${last_price:,.2f}')
print()
print(f'{"期間":<8} {"Prophet":>18} {"ARIMA":>18} {"XGBoost":>18} {"LSTM":>18}')
print('-' * 76)
for years in [1, 3, 5]:
# 各モデルの該当期間のインデックスを取得して予測値と変化率を計算
idx_p = min(years * 365 - 1, len(future_only) - 1)
p_pred = future_only['yhat'].iloc[idx_p]
p_chg = (p_pred - last_price) / last_price * 100
idx_a = min(years * 12 - 1, len(arima_mean) - 1)
a_pred = arima_mean.iloc[idx_a]
a_chg = (a_pred - last_price) / last_price * 100
idx_x = min(years * 12 - 1, len(xgb_series) - 1)
x_pred = xgb_series.iloc[idx_x]
x_chg = (x_pred - last_price) / last_price * 100
idx_l = min(years * 12 - 1, len(lstm_series) - 1)
l_pred = lstm_series.iloc[idx_l]
l_chg = (l_pred - last_price) / last_price * 100
print(f'{years}年後{"":<4}'
f'${p_pred:>7,.0f}({p_chg:>+5.1f}%)'
f'${a_pred:>7,.0f}({a_chg:>+5.1f}%)'
f'${x_pred:>7,.0f}({x_chg:>+5.1f}%)'
f'${l_pred:>7,.0f}({l_chg:>+5.1f}%)')
実行結果
結果の読み方
4モデルの予測方向は「上昇」で概ね一致していますが、上昇幅・タイミング・不確実性の表現はモデルごとに大きく異なります。
| モデル | 予測の傾向 | 特徴 |
|---|---|---|
| Prophet | 過去トレンドを延長して上昇 | 信頼区間が適切な広さで実用的 |
| ARIMA | 上昇するが信頼区間が極端に広い | 不確実性が大きすぎて判断しにくい |
| XGBoost | 完全に横ばい | 直近パターンへの過適合が疑われる |
| LSTM | 横ばい後に急上昇するS字カーブ | 過去の上昇パターンを別の形で再現 |
なぜ今回はProphetが適しているのか
今回の分析目的は「金価格の長期トレンドと季節性を可視化する」ことです。この目的に対してProphetが最も適している理由は3点あります。
1つ目はトレンドと季節性を分解して可視化できる点です。可視化⑤のコンポーネント分解グラフのように「長期トレンドがいつ変化したか」「年間でどの時期に上がりやすいか」を人間が解釈できる形で出力できるのはProphetだけです。ARIMAは分解グラフが出せず、XGBoostとLSTMは予測はできても「なぜそう予測したか」が見えません。
2つ目は信頼区間が実用的な広さである点です。ARIMAの信頼区間は5年後に現在水準を大きく下回るほど広がっており、投資の参考としては「何でもあり」に近い状態です。Prophetの信頼区間は広がりながらも上昇方向に収まっており、トレンドの継続を前提とした判断材料として使いやすいです。
3つ目は金融データの特性に合った設計である点です。Prophetは株式・為替などの休場日による欠損や外れ値(暴落・急騰)を前提に設計されています。XGBoostが横ばい予測になったのは直近の急上昇を「異常値」として処理した可能性があり、金融データへの適用には工夫が必要です。
いずれのモデルも過去データのパターンをもとに予測しており、地政学リスクや金融政策の転換など外部要因は考慮されません。複数モデルの結果はあくまで参考情報として活用し、投資判断は自己責任で行ってください。
7.可視化⑤:トレンド・季節性の分解
可視化④の予測は「トレンド」と「季節性」の組み合わせで構成されています。この2つを分解して表示することで、Prophetが何を根拠に予測しているかを確認します。予測結果をそのまま信じるのではなく、根拠となるパターンが過去データから妥当に読み取れているかを検証することが目的です。
実行したコード
# Prophetの内部コンポーネントを分解して可視化
# trend(長期トレンド)・yearly(年間季節性)などが自動で表示される
fig = m.plot_components(forecast)
plt.tight_layout()
plt.show()
# 予測サマリーの数値出力
last_actual = prophet_df['y'].iloc[-1] # 直近の実績価格
# 予測開始点以降のデータのみ抽出
future_only = forecast[forecast['ds'] > prophet_df['ds'].max()]
print('\n【金(GLD)予測サマリー】')
print(f'現在の価格 : ${last_actual:,.2f}')
print()
# 1年後・2年後・3年後・5年後の予測値と信頼区間を表示
for years in [1, 2, 3, 5]:
idx = min(years * 365 - 1, len(future_only) - 1)
pred = future_only['yhat'].iloc[idx] # 予測中央値
lower = future_only['yhat_lower'].iloc[idx] # 信頼区間下限
upper = future_only['yhat_upper'].iloc[idx] # 信頼区間上限
chg = (pred - last_actual) / last_actual * 100 # 現在からの変化率
print(f'{years}年後の予測: ${pred:,.2f} ({chg:+.1f}%)'
f' 範囲: ${lower:,.2f} ~ ${upper:,.2f}')
実行結果
トレンド分解グラフでは、2020年以降に上昇傾斜が急になっていることが確認できます。これはコロナショック後の金融緩和・インフレ懸念・地政学リスクの高まりを反映したものと考えられます。
季節性パターンでは、年初から春にかけて上昇しやすく、夏から秋にかけて下落しやすい傾向が見られます。
予測サマリーでは5年後に+137.3%($1,045)という強気な数値が出ていますが、信頼区間が$754〜$1,362と非常に広く、あくまで過去トレンドの延長線上での予測であることに注意が必要です。
分析したデータから得られた情報
傾向
金(GLD)は10年間で約3.5倍に上昇しており、S&P500に次ぐ高いリターンを示している
銀も金を上回る上昇率を見せているが、値動きが荒く金より高リスクであることがわかる
ビットコインは桁違いの上昇率を示す一方、暴落も激しく別次元の資産であることが確認できる
相関関係
銀(SI=F)との相関が最も高い:金と銀は同じ貴金属として連動して動く傾向がある
ドル指数(DX-Y.NYB)とは負の相関:ドルが弱くなると金価格が上がる傾向がある
S&P500・ビットコインとの相関は低い:株式市場が暴落しても金は下がりにくく、分散投資の効果が期待できる
米金利(TNX)とは負の相関:金利が上がると金の魅力が下がる傾向がある
課題
Prophetは過去のトレンドの延長線上で予測するため、地政学的リスクや突発的な経済ショックは考慮されない
5年後の予測範囲が$754〜$1,362と非常に広く、不確実性が高い
考察
分析してみて興味深かったのが、金とドル指数・米金利の関係です。「ドルが弱くなると金が上がる」「金利が上がると金が下がる」というのは知識としては知っていましたが、実際にデータで確認すると、なんとなく理解していたことが数字として裏付けられた感覚がありました。
意外だったのはS&P500との相関の低さです。株が下がっても金は独自の動きをする傾向があるなら、株式中心のNISAポートフォリオに少し組み込んでおく意味は十分あると感じました。円安・インフレが続く今の日本では特にそうかもしれません。
ただ、今から買うかと言われると少し慎重になります。すでに史上最高値圏にあり、割高感は否めないのが正直なところです。それでも一括購入ではなく積立(ドルコスト平均法)で少しずつ買い増していくなら、高値掴みのリスクはある程度抑えられると考えています。
金は配当も利息も生まない資産なので、ポートフォリオのメインにはなりません。ただ5〜10%程度をリスクヘッジとして保有しておく選択肢は、データを見た後だと以前より現実的に映っています。
まとめ
- 高いリターン:金は過去10年で約3.5倍に上昇し、S&P500に次ぐリターン
- 貴金属との連動:銀(+0.703)・プラチナ(+0.532)と強い正の相関
- ドル・金利との逆相関:ドル指数(-0.432)・米金利(-0.264)と負の相関
- 株式・仮想通貨との独立性:S&P500・ビットコインとの相関は0.1未満と相関が低く、分散先として有効
- 将来予測:Prophetの5年先予測は+137.3%だが、不確実性は高い
- 注意事項:本記事はあくまで過去データにもとづく分析です。投資判断は自己責任でお願いします
参考にした資料・文献
Yahoo Finance(yfinanceライブラリ経由でデータ取得)
Prophet公式ドキュメント:https://facebook.github.io/prophet/
世界ゴールドカウンシル:https://www.gold.org/jp





